Xcode Source Editor Extensionの世界(完全版) / 20170916 #iosdc

704056da9a4c4e075ad14479beaebab7?s=47 takasek
September 16, 2017

Xcode Source Editor Extensionの世界(完全版) / 20170916 #iosdc

iOSDC Japan 2017 ( https://iosdc.jp/2017/ ) での発表資料です。

## 発表詳細

https://iosdc.jp/2017/node/1518

## 動画

https://www.youtube.com/watch?v=8OUYTcgVk8Q

## デモExtension

この発表に出てきたテクニックはすべて、以下のリポジトリで動作するデモアプリ(Extension)として確認できます。
takasek/XcodeExtensionSample: Various sample commands to implement Xcode Source Editor Extension
https://github.com/takasek/XcodeExtensionSample

## 他、参考リンク

### To find awesome extensions

tib/awesome-xcode-extensions: Awesome native Xcode extensions.
https://github.com/tib/awesome-xcode-extensions

Search · topic:xcode-extension
https://github.com/search?q=topic%3Axcode-extension&type=Repositories

### Apple WWDC Videos

Using and Extending the Xcode Source Editor - WWDC 2016 - Videos - Apple Developer
https://developer.apple.com/videos/play/wwdc2016/414/

Cocoa Interprocess Communication with XPC - WWDC 2012 - Videos - Apple Developer
https://developer.apple.com/videos/play/wwdc2012/241/

704056da9a4c4e075ad14479beaebab7?s=128

takasek

September 16, 2017
Tweet

Transcript

  1. Xcode Source Editor Extensionͷੈք by. 2017/9/16 iOSDC 2017 1

  2. ͔ͭͯӫ՚Λތͬͨ ϓϥάΠϯɾύοέʔδϚωʔδϟʔ͕͋ͬͨ 2

  3. 3

  4. ɹ ɹ 4 ίʔυͷ੔ܗ 4 ίϯιʔϧϩάͷϦονԽ 4 ૢ࡞ମܥͷVimԽ 4 etc...

    4
  5. ✟ R.I.P ✟ ɹ ɹ 5

  6. 6

  7. 7

  8. 8

  9. 9

  10. … 10

  11. ! 11

  12. WWDC17 ͰԿ΋࿩୊ʹ ͳΒͳ͔ͬͨͷͳΒɺ iOSDC2017Ͱ ࿩୊ʹ͢Δ͔͠ 12

  13. Xcode Source Editor Extensionͷੈք by. 2017/9/16 iOSDC 2017 13

  14. takasek iOS Developer @takasek OSS ActionClosurable Notifwift౳ 14

  15. ୈ1ষ Xcode Source Editor Extension Λ ࢖ͬͯΈΑ͏ 15

  16. Xcode Source Editor Extension Λ࢖ͬͯΈΑ͏ 4 App Store ͳͲ͔Βೖख 4

    extension ͸ඞͣ .app ͱηοτʹͳ͍ͬͯΔ 4 appΛҰ౓࣮ߦ͢ΔͱɺXcodeͰextension͕ར༻Մೳʹ 16
  17. XcodeͰ͸… 4 Editor ϝχϡʔʹίϚϯυ͕ग़ݱ 4 ଞͷϝχϡʔͱಉ͘͡ɺ Xcode ͷ ⌘ઃఆ ͔Β

    ΩʔόΠϯυΛઃఆͰ͖Δ ! 17
  18. ୈ2ষ Xcode Source Editor Extension Λ ୳ͯ͠ΈΑ͏ 18

  19. ! AppStore " "xcode extension" Ͱ ݕࡧͯ͠ΈΔͱ…ʂ 19

  20. 20

  21. େৎ෉ʂ GitHub ʹ͸໺ੜͷ extension ͕͍ͬͺ͍ʂ 4 awesomeϦϙδτϦ͔Β୳͢ https://github.com/tib/awesome-xcode-extensions 4 GitHub

    ͷ topic ͰϦϙδτϦ୳ ࡧ https://github.com/search? o=desc&q=topic%3Axcode- extension&s=stars&type=Repositories 21
  22. େৎ෉ʂ GitHub ʹ͸໺ੜͷ extension ͕͍ͬͺ͍ʂ 4 awesomeϦϙδτϦ͔Β୳͢ https://github.com/tib/awesome-xcode-extensions 4 GitHub

    ͷ topic ͰϦϙδτϦ୳ ࡧ https://github.com/search? o=desc&q=topic%3Axcode- extension&s=stars&type=Repositories 22
  23. ໺ྑ Extension ͷ ಋೖํ๏ 4 Code Signing Λࣗ෼ͷ΋ͷʹม͑ ͯ Archive

    → Export ͢Δ͚ͩ ɹ˞ Release ϏϧυՄೳͳ ɹɹ ։ൃऀΞΧ΢ϯτ͕ඞཁ 4 Archive ͯ͠ग़དྷ্͕ͬͨ .app Λ ࣮ߦ͢Ε͹Πϯετʔϧ׬ྃ 23
  24. ୈ3ষ Xcode Source Editor Extension Λ ࡞ͬͯΈΑ͏ 24

  25. 25

  26. 26

  27. 27

  28. 28

  29. 29

  30. 30

  31. ɹ ɹ ɹɹɹɹɹɹɹɹɹɹɹɹɹɹɹɹɹ !͜ͷҰߦΛՃ͑Δ͚ͩͰ ɹɹɹɹɹɹɹɹɹɹɹɹ Ͱ͖͕͋Γ 31

  32. 32

  33. 33

  34. invocation.buffer.lines.add("hello extension!") ͷ࣮ߦʹ੒ޭʂɹ؆୯ʂ 34

  35. XcodeKit.framework ͷ API 35

  36. XcodeKit.framework ͷ API 4 XCSourceEditorExtension 4 XCSourceEditorCommand 4 XCSourceEditorCommandInvocation 4

    XCSourceTextBuffer 4 XCSourceTextRange 4 XCSourceTextPosition 36
  37. XcodeKit.framework ͷ API 4 XCSourceEditorExtension 4 XCSourceEditorCommand 4 XCSourceEditorCommandInvocation 4

    XCSourceTextBuffer 4 XCSourceTextRange 4 XCSourceTextPosition 37
  38. XCSourceTextBuffer ͷ໾ׂ 4 ΤσΟλͷςΩετόοϑΝͷಡΈॻ͖ 4 completeBuffer: String ɹ˞ શମॻ͖ସ͑ 4

    lines: NSMutableArray 4 selections: NSMutableArray ɹ˞ ཻ౓ͷࡉ͔͍ॻ͖ସ͑ 4 ϝλσʔλͷऔಘ 4 UTI (ϑΝΠϧछผ)ͱ͔ɺλϒɾΠϯσϯτͷ৘ใΛऔಘՄ 38
  39. ͬ͘͟Γݴ͑͹ XcodeKit ͕Ͱ͖Δ͜ͱ͸… 1. XCSourceTextBuffer ͷ lines, selections ΛಡΉ 2.

    ͳΜΒ͔ͷՃ޻ 3. XCSourceTextBuffer ͷ lines, selections ʹ ॲཧ݁ՌΛೖΕ௚͢ 39
  40. 40

  41. ͜Ε͚ͩʁ ΋ͬͱԿ͔ͳ͍ʂʁ ϓϩδΣΫτߏ଄Λ͍͡Δͱ͔ɺ ผͷϑΝΠϧʹΞΫηε͢Δͱ͔… 41

  42. WWDC16 1 ᐌ͘ɺ 1 Using and Extending the Xcode Source

    Editor - WWDC 2016 https://developer.apple.com/videos/play/wwdc2016/414/ 42
  43. 43

  44. 44

  45. 45

  46. Xcode Extensionͷੈք͸ڱ͘ɺ ߴ͍น ʹ્·Ε͍ͯͨ 46

  47. น͕͋ͬͨΒ… Ͳ͏͢Δʁ 47

  48. iOSΤϯδχΞ͸ɺน͕͋ͬͨΒ ɹ ొΔɻ 48

  49. ࠷ऴষ Ͳ͏ొΔͷ͔ 49

  50. น(1) ೖग़ྗͷน ςΩετόοϑΝͷಡΈॻ͖͔͠Ͱ͖ͳ͍ 50

  51. ղܾࡦ AppKit ͷॿ͚ΛआΓΔ 51

  52. NSPasteboard Ͱ ΫϦοϓϘʔυͷೖग़ྗ NSWorkspace Ͱ URL scheme ͱͷ࿈ܞ NSWorkspace Ͱ

    ΠϯετʔϧࡁApp Λ։͘ ౳ʑ 52
  53. NSPasteboard Ͱ ΫϦοϓϘʔυͷೖग़ྗ let pasteboard = NSPasteboard.general // ΫϦοϓϘʔυ͔ΒtextΛऔಘ let

    text = pasteboard.pasteboardItems?.first? .string(forType: NSPasteboard.PasteboardType( rawValue: "public.utf8-plain-text" )) // ΫϦοϓϘʔυʹtextΛೖΕΔ pasteboard.declareTypes([.string], owner: nil) pasteboard.setString(text!, forType: .string) 53
  54. NSWorkspace Ͱ URL scheme ͱͷ࿈ܞ ྫ: TwitterΞϓϦͰ౤ߘը໘Λ։͘ var c =

    URLComponents(string: "twitter://post")! c.queryItems = [ URLQueryItem(name: "message", value: text!) ] NSWorkspace.shared.open(c.url!) 54
  55. NSWorkspace Ͱ ΠϯετʔϧࡁApp Λ։͘ ྫ: ΧϨϯμʔΛ։͘ NSWorkspace.shared.launchApplication("Calendar") 55

  56. 56

  57. น(2) ݴޠػೳͷน 57

  58. ૢ࡞ͷͨΊʹͲ͏ͯ͠΋ LinuxίϚϯυ Λ࢖͍͍ͨΜͩʂ ͱ͍͏৔߹ʹͲ͏ͨ͠Β͍͍͔ 58

  59. ղܾࡦ Process Λ࢖ͬͯ LinuxίϚϯυ࣮ߦ ※ͨͩ͠Ұ෦ͷίϚϯυʹݶΔ 59

  60. func runTask(command: String, arguments: [String], standardInput: Pipe? = nil) throws

    -> Pipe { let task = Process(), standardOutput = Pipe(), standardError = Pipe() task.launchPath = "/usr/bin/env" task.arguments = [command] + arguments task.currentDirectoryPath = NSTemporaryDirectory() task.standardInput = standardInput task.standardOutput = standardOutput task.standardError = standardError task.launch() task.waitUntilExit() guard task.terminationStatus == 0 else { // ҟৗऴྃ let errorOutput = String(data: standardError.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" throw Error.commandFailed(errorOutput) } return standardOutput } let tmpFilePath = NSTemporaryDirectory().appending("inputFile") let catOutput = try runTask( command: "cat", arguments: [tmpFilePath] ) let result = String(data: catOutput.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" 60
  61. 61

  62. น(3) ωοτϫʔΫͷน 62

  63. URLSession iOS։ൃͰ΋͓ͳ͡ΈͰ͢Ͷ 63

  64. 64

  65. ղܾࡦ 65

  66. 66

  67. น(4) Sandbox ͷน 67

  68. !ωοτ͔ΒGETͨ͠΋ͷ͸ ɹΤσΟλʹग़͢ͷ΋ͳΜͩ͠ɺ σεΫτοϓͱ͔ʹ ॻ͖ग़͍ͨ͠ͳʔ 68

  69. 69

  70. ͔͠͠ Xcode Source Editor Extension ͷ Sandbox ͸ OFFෆՄ ※

    OFFʹͯ͠͠·͏ͱ ϝχϡʔʹίϚϯυ͕ දࣔ͞Εͳ͍ʂ 70
  71. 71

  72. खڧ͍ 72

  73. Ͱ΋ 73

  74. extesnionҎ֎ͷXPCαʔϏεͰ͋Ε͹ SandboxΛOFFʹͰ͖Δ 74

  75. extesnionҎ֎ͷXPCαʔϏε ɹ #ͱ͸ 75

  76. XPCͱ͸2 ΞϓϦΛෳ਺ͷαʔϏεʹ෼ׂ ͠ɺϓϩηεؒ௨৴Ͱ࿈ܞ͢Δ ϝΧχζϜɻ֤XPCαʔϏε͝ͱ ʹݖݶΛ෼཭͢Ε͹ɺηΩϡΞ ͳΞϓϦ͕࡞ΕΔɻ ※ Source Editor Extension

    ΋ XPC ͷҰछ 2 Cocoa Interprocess Communication with XPC - WWDC 2012 https://developer.apple.com/videos/play/wwdc2012/241/ 76
  77. Target Λ௥Ճ 77

  78. entitlementͰSandbox͕OFFʹͳͬͯͯ΋ಈ࡞͢Δʂ 78

  79. ͨͩ͠ɺAppStoreʹ͸ग़ͤͳ͘ͳΔ ͜ΕΛ΍Δͱ͖͸ɺGitHub͕φϫόϦͷ ໺ྑExtensionͱͯ͠ ੜ͖Δ֮ޛΛܾΊ͍ͯͩ͘͞ɻ 79

  80. extension ͱ XPC ͷ࿈ܞ ࢀߟʹͳΔ࣮ྫ https://github.com/norio- nomura/SwiftLintForXcode ɹ /usr/local/bin/swiftlint ࣮ߦ࣌ʹ

    ى͜Δಉ༷ͷ permission ໰୊Λɺ XPCΛܦ༝͢Δ͜ͱͰճආ͍ͯ͠Δɻ 80
  81. 81

  82. น(5) GUIͷน extension͸no UI 82

  83. XPC (extensionؚ) ͸ GUI Λ࣋ͨͳ͍ ϑΝΠϧબ୒Ϗϡʔ͢Βग़ͤͳ͍ 83

  84. WWDC16ͷηογϣϯ ʹΑΔͱɺ 4 ʮGUI͸Appʹஔ͚ʯ 4 AppStoreʹ΋ɺ ىಈͨ͠AppΛઃఆը໘ͱͯ͠ ࢖͏extension͕͋Δ (Protocol.app) 84

  85. App ⁶ extensionؒͷσʔλڞ༗ํ๏ app ͱ extension Λ ಉ͡ App Group

    ʹઃఆ͢Δͱɺ UserDefaultsΛڞ༗Ͱ͖Δ 85
  86. ͚Ͳɺ extensionͷίϚϯυ࣮ߦதʹ UIཉ͍͜͠ͱ͋ΔΑͶ ϑΝΠϧબ୒ͤͨ͞Γͱ͔ɺ Alertදࣔͨ͠Γͱ͔… 86

  87. ղܾࡦ ίϚϯυ͔ΒAppΛ্ཱͪ͛ͯσʔλΛ౉͢ AppͰUIΛදࣔ͠ɺ݁ՌΛίϚϯυʹฦ͢ ͱ͍͏͜ͱ͕Ͱ͖Ε͹͍͍ 87

  88. ⛏extension -> App 4 URL Scheme 4 iOSͱ͸URLͷϋϯυϦϯάํ๏͕ҧ͏ͷͰ஫ҙ ⛏App ->

    extension 4 UserDefaults ͷߋ৽Λ observe ͢Δ 4 DistributedNotificationCenter 88
  89. ⛏ extension -> App via URL Scheme ᶃ @ Appͷ

    info.plist 4 URL Types Λઃఆ @ Extension var c = URLComponents(string: "xcextsample://")! c.queryItems = [ URLQueryItem(name: "title", value: "΄͛΄͛") ] NSWorkspace.shared.open(c.url!) 89
  90. ⛏ extension -> App via URL Scheme ᶄ @ App

    class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillFinishLaunching(_ aNotification: Notification) { NSAppleEventManager.shared().setEventHandler( self, andSelector: #selector(AppDelegate.handleGetURLEvent(event:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL) ) } @objc func handleGetURLEvent(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) { guard let urlString = event?.paramDescriptor(forKeyword: keyDirectObject)?.stringValue, let components = URLComponents(string: urlString) else { return } guard let title = components.queryItems?.first(where: { $0.name == "title" })?.value else { return } ... } 90
  91. ⛏App -> extension via UserDefaults @ Extension extension UserDefaults {

    @objc dynamic var valueFromApp: String? { return string(forKey: "valueFromApp") } } let semaphore = DispatchSemaphore(value: 0) let userDefaults = UserDefaults(suiteName: "YourAppGroup")! userDefaults.synchronize() let observation = userDefaults.observe(\UserDefaults.valueFromApp, options:[.old, .new]) { ud, change in selectedResult = change.newValue?.flatMap { $0 } semaphore.signal() } _ = semaphore.wait() @ App UserDefaults(suiteName: "YourAppGroup")?.set(selectedFileURL?.absoluteString, forKey: "valueFromApp") 91
  92. ⛏App -> extension via DistributedNotificationCenter @ Extension DistributedNotificationCenter.default().addObserver( self, selector:

    #selector(HogeCommand.doSomething(notification:)), name: Notification.Name("XcodeExtensionSample.applicationDidSomething"), object: nil, suspensionBehavior: .deliverImmediately ) @ App DistributedNotificationCenter.default().postNotificationName( Notification.Name("XcodeExtensionSample.applicationDidSomething"), object: nil, userInfo: selectedFileURL.flatMap { ["url": $0] }, deliverImmediately: true ) 92
  93. ͪͳΈʹɺσόοάʹ͍ͭͯͷ౾৘ใ 4 XPC΍App͸extensionͱ͸ผϓϩηε͕ͩɺ SchemeΛઃఆ͓͚ͯ͠͹ϒϨʔΫϙΠϯτΛுͬͨΓίϯιʔϧϩάΛݟΔ͜ͱ΋Մೳ 93

  94. 94

  95. นʹ્·Εͨɺ ڱ͍Xcode Source Editor Extensionͷੈք 95

  96. ͔͠͠ɺͦͷนΛొͬͨઌʹ͸—— 96

  97. 97

  98. 98

  99. 99

  100. 100

  101. 101

  102. 102

  103. ͦͷนΛొͬͨઌʹ͸—— ࣗ༝ͳແݶͷੈք͕޿͕͍ͬͯͨ 103

  104. ͋ͳͨ΋Ұॹʹɺ Xcode Source Editor ExtensionͰ ༡ΜͰΈ·ͤΜ͔ʁ 104

  105. Xcode Source Editor Extensionͷੈք ׬ refer to https://github.com/takasek/XcodeExtensionSample 105

  106. Q&A 106