Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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/

takasek

September 16, 2017
Tweet

More Decks by takasek

Other Decks in Programming

Transcript

  1. 3

  2. 6

  3. 7

  4. 8

  5. 9

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

    extension ͸ඞͣ .app ͱηοτʹͳ͍ͬͯΔ 4 appΛҰ౓࣮ߦ͢ΔͱɺXcodeͰextension͕ར༻Մೳʹ 16
  7. 20

  8. େৎ෉ʂ 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
  9. େৎ෉ʂ 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
  10. ໺ྑ Extension ͷ ಋೖํ๏ 4 Code Signing Λࣗ෼ͷ΋ͷʹม͑ ͯ Archive

    → Export ͢Δ͚ͩ ɹ˞ Release ϏϧυՄೳͳ ɹɹ ։ൃऀΞΧ΢ϯτ͕ඞཁ 4 Archive ͯ͠ग़དྷ্͕ͬͨ .app Λ ࣮ߦ͢Ε͹Πϯετʔϧ׬ྃ 23
  11. 25

  12. 26

  13. 27

  14. 28

  15. 29

  16. 30

  17. 32

  18. 33

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

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

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

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

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

  24. 44

  25. 45

  26. 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
  27. NSWorkspace Ͱ URL scheme ͱͷ࿈ܞ ྫ: TwitterΞϓϦͰ౤ߘը໘Λ։͘ var c =

    URLComponents(string: "twitter://post")! c.queryItems = [ URLQueryItem(name: "message", value: text!) ] NSWorkspace.shared.open(c.url!) 54
  28. 56

  29. 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
  30. 61

  31. 64

  32. 66

  33. 69

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

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

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

    ΋ XPC ͷҰछ 2 Cocoa Interprocess Communication with XPC - WWDC 2012 https://developer.apple.com/videos/play/wwdc2012/241/ 76
  37. 81

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

    extension 4 UserDefaults ͷߋ৽Λ observe ͢Δ 4 DistributedNotificationCenter 88
  39. ⛏ 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
  40. ⛏ 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
  41. ⛏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
  42. ⛏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
  43. 94

  44. 97

  45. 98

  46. 99

  47. 100

  48. 101

  49. 102