Enhancing Your Workflow with Xcode Source Editor Extensions By: Jesse Black Software Engineer stable|kernel
 jesse.black@stablekernel.com
@stablekernel Hi, I’m Jesse Black. • Programming for over eight years • Created a Mac App for my family business • Worked for 3 years with Gramercy Consultants developing iOS and Android apps • Working for stable|kernel for the past 3 years developing iOS apps, Android apps and their supporting APIs
We’re stable|kernel. stable|kernel is an Atlanta-based mobile development company 
 to craft smartly-designed mobile applications that connect brands 
 directly with their users. 
 

Enhancing Your Workflow with Xcode Source Editor Extensions @stablekernel Overview Mac App Extensions Xcode Source Editor Extensions Demo: Create Source Editor Extension and debugging it XC Classes Example: JSON formatter Demo: JSON formatter in action
Mac App Extensions @stablekernel • Mac App Extensions • Affect the user experience of other apps • Need to be lightweight and run fast • Require user interaction in order to be run • Distribute via host app
Why use Xcode Source Editor Extensions @stablekernel • Less context switching • It is too fun to customize your workflow
Xcode Source Editor Extensions @stablekernel • Differences with general Mac App Extensions • Extensions are installed by just putting the host application in your applications folder • Only works with one type of target application, which is Xcode • Doesn’t have UI
Xcode Source Editor Extensions @stablekernel • Allows you to add custom commands under Xcode’s Editor menu • Commands can manipulate text and text selections • Users can put bind keyboard shortcuts to invoke your commands
Limitations to Xcode Source Editor Extensions @stablekernel • Can only start with user interaction • Lack of ability to integrate into the build process and archive process. It is for modifying source code while it is being edited.
Expectations of Xcode Source Editor Extensions @stablekernel • Fast start up • Fast execution • Security and Stability
Demo @stablekernel Overview Create Source Editor Extension How to install How to debug How to uninstall
Defining Commands @stablekernel • Command Identifier • Class Name • Command Name Commands can be defined in the Extension’s plist or by supplying a dictionary at runtime. Commands provided at runtime override commands defined in the plist.
XC Source Editor Extension @stablekernel func extensionDidFinishLaunching() {} var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {}
XC Source Editor Command @stablekernel func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
XC Source Editor Command @stablekernel • Modify text, text selection • Exit early with an error message that is displayed in Xcode
XC Source Editor Command Invocation @stablekernel • Encapsulates all details needed to fulfill the user’s request • contentUTI • usesTabsForIndentation • indentationWidth • tabWidth • buffer
XC Source Text Buffer @stablekernel • completeBuffer • lines • selections • defined in terms of lines and columns
Example: JSON formatter @stablekernel • Define minify and prettify commands • Implement XCSourceEditorCommand’s perform method • Validate requirements for command • Get first text selection • Return error if selected text isn’t valid JSON • Use JSONSerialization class to format text • Replace selected text
XCSourceEditorExtension @stablekernel var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] { return [ [ .classNameKey: "JeSON.SourceEditorCommand", .identifierKey: JeSONInvocation.Minify.rawValue, .nameKey: "Minify" ], [ .classNameKey: "JeSON.SourceEditorCommand", .identifierKey: JeSONInvocation.Prettify.rawValue, .nameKey: "Prettify" ], ] }
XCSourceEditorExtension @stablekernel enum JeSONInvocation: String { case Prettify = "com.jesseblack.HelloWorldProject.JeSON.Prettify" case Minify = "com.jesseblack.HelloWorldProject.JeSON.Minify" }
@stablekernel func textAtSelectionIndex(_ at: Int, buffer: XCSourceTextBuffer) -> String { let textRange = buffer.selections[at] as! XCSourceTextRange let selectionLines = buffer.lines.enumerated().filter { (offset, element) -> Bool in return offset >= textRange.start.line && offset <= textRange.end.line }.map { return $1 } return selectionLines.enumerated().reduce("") { (result, enumeration) -> String in let line = enumeration.element as! String if enumeration.offset == 0 && enumeration.offset == selectionLines.count - 1 { let startIndex = line.index(line.startIndex, offsetBy: textRange.start.column) let endIndex = line.index(line.startIndex, offsetBy: textRange.end.column + 1) return result + line.substring(with: startIndex..<endIndex) } else if enumeration.offset == 0 { let startIndex = line.index(line.startIndex, offsetBy: textRange.start.column) return result + line.substring(from: startIndex) } else if enumeration.offset == selectionLines.count - 1 { let endIndex = line.index(line.startIndex, offsetBy: textRange.end.column + 1) return result + line.substring(to: endIndex) } return result + line } }
@stablekernel func replaceTextAtSelectionIndex(_ at: Int, replacementText: String, buffer: XCSourceTextBuffer) { let textRange = buffer.selections[at] as! XCSourceTextRange let lastLine = buffer.lines[textRange.end.line] as! String let endIndex = lastLine.index(lastLine.startIndex, offsetBy: textRange.end.column+1) let suffix = lastLine.substring(from: endIndex) let firstLine = buffer.lines[textRange.start.line] as! String let startIndex = firstLine.index(firstLine.startIndex, offsetBy: textRange.start.column) let prefix = firstLine.substring(to: startIndex) let range = NSMakeRange(textRange.start.line, textRange.end.line - textRange.start.line + 1) buffer.lines.removeObjects(in: range) buffer.lines.insert(prefix+replacementText+suffix, at: textRange.start.line) let newRange = XCSourceTextRange(start: textRange.start, end: textRange.start) buffer.selections.setArray([newRange]) }
Performing the command @stablekernel func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void { guard let jeSONInvocation = JeSONInvocation(rawValue: invocation.commandIdentifier) else { let errorInfo = [NSLocalizedDescriptionKey: "Command not recognized"] completionHandler(NSError(domain: "", code: 0, userInfo: errorInfo)) return } let buffer = invocation.buffer guard buffer.selections.count == 1 else { let errorInfo = [NSLocalizedDescriptionKey: "Command only handles 1 selection at a time"] completionHandler(NSError(domain: "", code: 0, userInfo: errorInfo)) return }
Performing the command @stablekernel // perform continued let selectedText = textAtSelectionIndex(0, buffer: buffer) let data = selectedText.data(using: .utf8) do { let jsonObject = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions()) switch jeSONInvocation { case .Minify: let miniData = try! JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) let string = String.init(data: miniData, encoding: .utf8)! replaceTextAtSelectionIndex(0, replacementText: string, buffer: buffer) case .Prettify: let miniData = try! JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) let string = String.init(data: miniData, encoding: .utf8)! replaceTextAtSelectionIndex(0, replacementText: string, buffer: buffer) } } catch { completionHandler(error) } completionHandler(nil) }
JSON Formatter Demo @stablekernel Overview See all the hard work pay off
Questions? Business Inquiries: Sarah Woodward Director of Business Development sarah.woodward@stablekernel.com Jesse Black Software Engineer jesse.black@stablekernel.com blog.stablekernel.com @JesseBlack82 http://www.slideshare.net/stablekernel https://github.com/JesseBlack82/HelloExtensions

Connect.Tech- Enhancing Your Workflow With Xcode Source Editor Extensions

  • 1.
    Enhancing Your Workflowwith Xcode Source Editor Extensions By: Jesse Black Software Engineer stable|kernel
 jesse.black@stablekernel.com
  • 2.
    @stablekernel Hi, I’m JesseBlack. • Programming for over eight years • Created a Mac App for my family business • Worked for 3 years with Gramercy Consultants developing iOS and Android apps • Working for stable|kernel for the past 3 years developing iOS apps, Android apps and their supporting APIs
  • 3.
    We’re stable|kernel. stable|kernel isan Atlanta-based mobile development company 
 to craft smartly-designed mobile applications that connect brands 
 directly with their users. 
 

  • 4.
    Enhancing Your Workflowwith Xcode Source Editor Extensions @stablekernel Overview Mac App Extensions Xcode Source Editor Extensions Demo: Create Source Editor Extension and debugging it XC Classes Example: JSON formatter Demo: JSON formatter in action
  • 5.
    Mac App Extensions @stablekernel •Mac App Extensions • Affect the user experience of other apps • Need to be lightweight and run fast • Require user interaction in order to be run • Distribute via host app
  • 6.
    Why use XcodeSource Editor Extensions @stablekernel • Less context switching • It is too fun to customize your workflow
  • 7.
    Xcode Source EditorExtensions @stablekernel • Differences with general Mac App Extensions • Extensions are installed by just putting the host application in your applications folder • Only works with one type of target application, which is Xcode • Doesn’t have UI
  • 8.
    Xcode Source EditorExtensions @stablekernel • Allows you to add custom commands under Xcode’s Editor menu • Commands can manipulate text and text selections • Users can put bind keyboard shortcuts to invoke your commands
  • 9.
    Limitations to XcodeSource Editor Extensions @stablekernel • Can only start with user interaction • Lack of ability to integrate into the build process and archive process. It is for modifying source code while it is being edited.
  • 10.
    Expectations of XcodeSource Editor Extensions @stablekernel • Fast start up • Fast execution • Security and Stability
  • 11.
    Demo @stablekernel Overview Create Source EditorExtension How to install How to debug How to uninstall
  • 12.
    Defining Commands @stablekernel • CommandIdentifier • Class Name • Command Name Commands can be defined in the Extension’s plist or by supplying a dictionary at runtime. Commands provided at runtime override commands defined in the plist.
  • 13.
    XC Source EditorExtension @stablekernel func extensionDidFinishLaunching() {} var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {}
  • 14.
    XC Source EditorCommand @stablekernel func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
  • 15.
    XC Source EditorCommand @stablekernel • Modify text, text selection • Exit early with an error message that is displayed in Xcode
  • 16.
    XC Source EditorCommand Invocation @stablekernel • Encapsulates all details needed to fulfill the user’s request • contentUTI • usesTabsForIndentation • indentationWidth • tabWidth • buffer
  • 17.
    XC Source TextBuffer @stablekernel • completeBuffer • lines • selections • defined in terms of lines and columns
  • 18.
    Example: JSON formatter @stablekernel •Define minify and prettify commands • Implement XCSourceEditorCommand’s perform method • Validate requirements for command • Get first text selection • Return error if selected text isn’t valid JSON • Use JSONSerialization class to format text • Replace selected text
  • 19.
    XCSourceEditorExtension @stablekernel var commandDefinitions: [[XCSourceEditorCommandDefinitionKey:Any]] { return [ [ .classNameKey: "JeSON.SourceEditorCommand", .identifierKey: JeSONInvocation.Minify.rawValue, .nameKey: "Minify" ], [ .classNameKey: "JeSON.SourceEditorCommand", .identifierKey: JeSONInvocation.Prettify.rawValue, .nameKey: "Prettify" ], ] }
  • 20.
    XCSourceEditorExtension @stablekernel enum JeSONInvocation: String{ case Prettify = "com.jesseblack.HelloWorldProject.JeSON.Prettify" case Minify = "com.jesseblack.HelloWorldProject.JeSON.Minify" }
  • 21.
    @stablekernel func textAtSelectionIndex(_ at:Int, buffer: XCSourceTextBuffer) -> String { let textRange = buffer.selections[at] as! XCSourceTextRange let selectionLines = buffer.lines.enumerated().filter { (offset, element) -> Bool in return offset >= textRange.start.line && offset <= textRange.end.line }.map { return $1 } return selectionLines.enumerated().reduce("") { (result, enumeration) -> String in let line = enumeration.element as! String if enumeration.offset == 0 && enumeration.offset == selectionLines.count - 1 { let startIndex = line.index(line.startIndex, offsetBy: textRange.start.column) let endIndex = line.index(line.startIndex, offsetBy: textRange.end.column + 1) return result + line.substring(with: startIndex..<endIndex) } else if enumeration.offset == 0 { let startIndex = line.index(line.startIndex, offsetBy: textRange.start.column) return result + line.substring(from: startIndex) } else if enumeration.offset == selectionLines.count - 1 { let endIndex = line.index(line.startIndex, offsetBy: textRange.end.column + 1) return result + line.substring(to: endIndex) } return result + line } }
  • 22.
    @stablekernel func replaceTextAtSelectionIndex(_ at:Int, replacementText: String, buffer: XCSourceTextBuffer) { let textRange = buffer.selections[at] as! XCSourceTextRange let lastLine = buffer.lines[textRange.end.line] as! String let endIndex = lastLine.index(lastLine.startIndex, offsetBy: textRange.end.column+1) let suffix = lastLine.substring(from: endIndex) let firstLine = buffer.lines[textRange.start.line] as! String let startIndex = firstLine.index(firstLine.startIndex, offsetBy: textRange.start.column) let prefix = firstLine.substring(to: startIndex) let range = NSMakeRange(textRange.start.line, textRange.end.line - textRange.start.line + 1) buffer.lines.removeObjects(in: range) buffer.lines.insert(prefix+replacementText+suffix, at: textRange.start.line) let newRange = XCSourceTextRange(start: textRange.start, end: textRange.start) buffer.selections.setArray([newRange]) }
  • 23.
    Performing the command @stablekernel funcperform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void { guard let jeSONInvocation = JeSONInvocation(rawValue: invocation.commandIdentifier) else { let errorInfo = [NSLocalizedDescriptionKey: "Command not recognized"] completionHandler(NSError(domain: "", code: 0, userInfo: errorInfo)) return } let buffer = invocation.buffer guard buffer.selections.count == 1 else { let errorInfo = [NSLocalizedDescriptionKey: "Command only handles 1 selection at a time"] completionHandler(NSError(domain: "", code: 0, userInfo: errorInfo)) return }
  • 24.
    Performing the command @stablekernel //perform continued let selectedText = textAtSelectionIndex(0, buffer: buffer) let data = selectedText.data(using: .utf8) do { let jsonObject = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions()) switch jeSONInvocation { case .Minify: let miniData = try! JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) let string = String.init(data: miniData, encoding: .utf8)! replaceTextAtSelectionIndex(0, replacementText: string, buffer: buffer) case .Prettify: let miniData = try! JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) let string = String.init(data: miniData, encoding: .utf8)! replaceTextAtSelectionIndex(0, replacementText: string, buffer: buffer) } } catch { completionHandler(error) } completionHandler(nil) }
  • 25.
  • 26.
    Questions? Business Inquiries: Sarah Woodward Directorof Business Development sarah.woodward@stablekernel.com Jesse Black Software Engineer jesse.black@stablekernel.com blog.stablekernel.com @JesseBlack82 http://www.slideshare.net/stablekernel https://github.com/JesseBlack82/HelloExtensions