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

Finder Sync Extension で Mac 向け便利ツールを作ろう / iOSDC...

Finder Sync Extension で Mac 向け便利ツールを作ろう / iOSDC Japan 2021

iOSDC Japan 2021 Day1 15:50~ Track B レギュラートーク(20分)

プロポーザル:https://fortee.jp/iosdc-japan-2021/proposal/891d13b7-9e95-498f-8adc-0cd099f5c15d
デモアプリ1:https://github.com/Kyome22/ScaleHelper
デモアプリ2:https://github.com/Kyome22/RenameHelper
デモアプリ3:https://github.com/Kyome22/NewCanvas

Kyome (Takuto Nakamura)

September 18, 2021
Tweet

More Decks by Kyome (Takuto Nakamura)

Other Decks in Technology

Transcript

  1. "'JOEFSػೳ֦ு͕༗ޮʹͳ͍ͬͯΔ͔Ͳ͏͔Λ֬ೝ͢Δํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ if FIFinderSyncController.isExtensionEnabled { // Finderػೳ֦ு͸ڐՄࡁΈ } else {

    // Finderػೳ֦ு͸ڐՄ͞Ε͍ͯͳ͍ } FIFinderSyncController.showExtensionManagementInterface() "γεςϜ؀ڥઃఆͷ'JOEFSػೳ֦ுͷϖʔδΛ։͘ํ๏
  2. "'JOEFSػೳ֦ு͕؂ࢹ͢ΔϑΥϧμΛࢦఆ͢Δํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ class FinderSync: FIFinderSync { override init() { super.init()

    let targetPath = "/Users/UserName/Desktop" let targetURL = URL(fileURLWithPath: targetPath, isDirectory: true) FIFinderSyncController.default().directoryURLs = [targetURL] } }
  3. "؂ࢹԼͷϑΥϧμΛϢʔβ͕ૢ࡞։࢝ʗऴྃͨ͜͠ͱΛݕग़͢Δํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ class FinderSync: FIFinderSync { // ؂ࢹԼͷϑΥϧμΛϢʔβ͕ૢ࡞։࢝ͨ͠ࡍʹݺͼग़͞ΕΔ override func

    beginObservingDirectory(at url: URL) {} // ؂ࢹԼͷϑΥϧμΛϢʔβ͕ૢ࡞ऴྃͨ͠ࡍʹݺͼग़͞ΕΔ override func endObservingDirectory(at url: URL) {} } url ͸Ϣʔβͷૢ࡞ର৅ͷϑΥϧμύεͰ͋Δ
  4. "؂ࢹԼͷΞΠςϜʹόοδΛ͚ͭΔํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ  'JOEFS4ZODʹόοδͷొ࿥Λ͢Δ class FinderSync: FIFinderSync { override init()

    { super.init() FFIFinderSyncController.default() .setBadgeImage(NSImage, label: String?, forBadgeIdentifier: String) } }
  5. "؂ࢹԼͷΞΠςϜʹόοδΛ͚ͭΔํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ  όοδͱΞΠςϜΛඥ͚ͮΔ class FinderSync: FIFinderSync { override func

    requestBadgeIdentifier(for url: URL) { FIFinderSyncController.default() .setBadgeIdentifier(String, for: url) } } ຊདྷ͸ url ͔ΒϑΝΠϧͷ৘ใΛऔಘͯ͠ɺόοδ෇༩ͷ൑அ͢Δ ྫ͑͹ɺurl.hasDirectoryPath ͰϑΥϧμ͔Ͳ͏͔ url.pathExtension Ͱ֦ுࢠͷ֬ೝ͕Ͱ͖Δ
  6. "ΧελϜίϯςΩετϝχϡʔͷ௥Ճํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ class FinderSync: FIFinderSync { override func menu(for menu:

    FIMenuKind) -> NSMenu? {} } Swift Ͱ͸ύϥϝʔλ໊Λม͑ͯ΋ಉؔ͡਺ͱͯ͠ίϯύΠϧ͞ΕΔͷͰɺ menu Λ menuKind ʹมߋ͢Δͱѻ͍΍͍͢ override func menu(for menuKind: FIMenuKind) -> NSMenu? {}  ௥Ճ͢ΔͨΊͷ"1*ʹ͍ͭͯ
  7. "ΧελϜίϯςΩετϝχϡʔͷ௥Ճํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ override func menu(for menuKind: FIMenuKind) -> NSMenu? {

    let menu = NSMenu(title: "") // ϑΥϧμͷഎܠ্ͰӈΫϦοΫΛͨ͠ͱ͖ if menuKind == .contextualMenuForContainer { menu.addItem(withTitle: String, action: Selector?, keyEquivalent: String) } // ϑΝΠϧΛӈΫϦοΫͨ͠ͱ͖ if menuKind == .contextualMenuForItems { menu.addItem(withTitle: String, action: Selector?, keyEquivalent: String) } return menu }
  8. "ΧελϜπʔϧόʔϘλϯͷ௥Ճํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ  πʔϧόʔϘλϯͷ໊લɺπʔϧνοϓɺΞΠίϯը૾Λઃఆ͢Δ class FinderSync: FIFinderSync { // πʔϧόʔϘλϯͷ໊લ

    override var toolbarItemName: String {} // πʔϧόʔϘλϯͷπʔϧνοϓ override var toolbarItemToolTip: String {} // πʔϧόʔϘλϯͷΞΠίϯը૾ override var toolbarItemImage: NSImage {} }
  9. "ΧελϜπʔϧόʔϘλϯͷ௥Ճํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ  ϘλϯΛԡͨ͠ͱ͖ʹදࣔ͢ΔίϯςΩετϝχϡʔͷࢦఆΛ͢Δ override func menu(for menuKind: FIMenuKind) ->

    NSMenu? { let menu = NSMenu(title: "") // ΧελϜπʔϧόʔϘλϯΛΫϦοΫͨ͠ͱ͖ if menuKind == .toolbarItemMenu { menu.addItem(withTitle: String, action: Selector?, keyEquivalent: String) } return menu }
  10. "ίϯςΩετϝχϡʔʹऩೲΞϓϦͷΞΠίϯΛදࣔͤ͞Δํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ let menuItem = NSMenuItem(title: String, action: Selector?, keyEquivalent:

    String) let id = "ऩೲΞϓϦͷBundle Identifier" if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: id) { menuItem.image = NSWorkspace.shared.icon(forFile: url.path) }
  11. "Ϣʔβ͕ૢ࡞͍ͯ͠ΔϑΥϧμͷ63-Λऔಘ͢Δํ๏ 'JOEFS4ZOD&YUFOTJPO࣮૷ͷجຊʢػೳผฤʣ let targetedURL = FIFinderSyncController.default().targetedURL() let itemURLs = FIFinderSyncController.default().selectedItemURLs()

    "Ϣʔβ͕બ୒ͨ͠ϑΝΠϧͷ63-Λऔಘ͢Δํ๏ Ϣʔβ͕ϑΝΠϧΛબ୒ͤͣɺϑΥϧμ্ͰӈΫϦοΫͨ͠৔߹͸ɺ .selectedItemURLs() ͸ .targetedURL() ͱಉ͡URLʹͳΔ
  12. "9DPEFͰϒϨʔΫϙΠϯτΛ࢖͏ํ๏ 'JOEFS4ZOD&YUFOTJPOͷσόοάํ๏ 1.Clean Build Folder Λ࣮ߦ͢Δ 2.Terminal ͳͲͰ killall Finder

    Λୟ͘ 3.ऩ༰ΞϓϦΛϏϧυˍ࣮ߦͯ͠ɺγεςϜ؀ڥઃఆͰػೳ֦ுΛ༗ޮʹ͓ͯ͘͠ 4.ίʔυͷ೚ҙͷՕॴʹϒϨʔΫϙΠϯτΛ഑ஔ͢Δ 5.Finder Sync Extension ͷλʔήοτΛબΜͰϏϧυ͚ͩ͢Δʢ࣮ߦ͸͠ͳ͍ʣ 6.Finder Ͱ؂ࢹର৅ͷϑΥϧμΛ։͘ 7.Debug -> Attach to process Ͱ࠷΋൪߸ͷେ͖͍ process Λ Attach ͢Δ 8.௨ৗͷϒϨʔΫϙΠϯτΛ༻͍ͨσόοάΛߦ͏ 9.σόοά͕ࡁΜͩΒ Debug -> Detach from ʓʓ Λͯ͠ Detach ͢Δ
  13. "(6*Λ࢖͏ํ๏/4"MFSU 'JOEFS4ZOD&YUFOTJPOͷσόοάํ๏ DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle =

    .informational alert.messageText = "check point" alert.runModal() } ಈ࡞֬ೝ͍ͨ͠ՕॴʹҎԼͷΑ͏ͳ/4"MFSUදࣔͷίʔυΛهड़͢Δ
  14. "(6*Λ࢖͏ํ๏ϩʔΧϧ௨஌ʢ6TFS/PUJpDBUJPOTʣ 'JOEFS4ZOD&YUFOTJPOͷσόοάํ๏  'JOEFSػೳ֦ுଆͰϩʔΧϧ௨஌Λදࣔͤ͞ΔίʔυΛهड़͢Δ private func postUserNotification(subtitle: String, body: String)

    { let content = UNMutableNotificationContent() content.title = "SampleExtension" content.subtitle = subtitle content.body = body let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current() .add(request, withCompletionHandler: nil) } // ಈ࡞֬ೝ͍ͨ͠Օॴʹهड़ postUserNotification(subtitle: "check url", body: url.path)
  15. ศརπʔϧ࡞੒࣌ͷ5JQT "63-͔ΒϑΝΠϧ໊ʗϑΥϧμ໊Λऔಘ͢Δ extension URL { var fileName: String { return

    self.deletingPathExtension().lastPathComponent } } URL.deletingPathExtension() Λ࢖͏ͱ֦ுࢠΛআ͍ͨ URL ͕औಘͰ͖Δ
  16. ศརπʔϧ࡞੒࣌ͷ5JQT "ϑΝΠϧͷ৘ใΛऔಘ͢Δ let url = URL(fileURLWithPath: "/Sample.txt") let attributes =

    try? FileManager.default.attributesOfItem(atPath: url.path) // ࡞੒೔ let creationDate = attributes?[FileAttributeKey.creationDate] as? Date // มߋ೔ let modDate = attributes?[FileAttributeKey.modificationDate] as? Data // ϑΝΠϧΦʔφʔ໊ let ownerName = attributes?[FileAttributeKey.ownerAccountName] as? String // ϑΝΠϧαΠζʢByteʣ let fileSize = attributes?[FileAttributeKey.size] as? NSNumber // ࢀর͞Εͨճ਺ let refCount = attributes?[FileAttributeKey.referenceCount] as? NSNumber
  17. ศརπʔϧ࡞੒࣌ͷ5JQT "/4"MFSUɾ/40QFO1BOFMɾ/44BWF1BOFMΛ׆༻͢Δ ‣ BDDFTTPSZ7JFX಺ͷςΩετϑΟʔϧυͰ͸Χοτɾίϐʔɾϖʔετ ͳͲͷγϣʔτΧοτ͕ޮ͔ͳ͍ͷͰɺ͓·͡ͳ͍͕ඞཁ extension NSTextField { open override

    func performKeyEquivalent(with event: NSEvent) -> Bool { let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if flags == [.command] { let selector: Selector switch event.charactersIgnoringModifiers?.lowercased() { case "x": selector = #selector(NSText.cut(_:)) case "c": selector = #selector(NSText.copy(_:)) case "v": selector = #selector(NSText.paste(_:)) case "a": selector = #selector(NSText.selectAll(_:)) case "z": selector = Selector(("undo:")) default: return super.performKeyEquivalent(with: event) } return NSApp.sendAction(selector, to: nil, from: self) } else if flags == [.shift, .command] { if event.charactersIgnoringModifiers?.lowercased() == "z" { return NSApp.sendAction(Selector(("redo:")), to: nil, from: self) } self.undoManager?.undo() } return super.performKeyEquivalent(with: event) } }
  18. ศརπʔϧ࡞੒࣌ͷ5JQT "/4"MFSUɾ/40QFO1BOFMɾ/44BWF1BOFMΛ׆༻͢Δ ‣ BDDFTTPSZ7JFX಺ͷςΩετϑΟʔϧυͰ͸Χοτɾίϐʔɾϖʔετ ͳͲͷγϣʔτΧοτ͕ޮ͔ͳ͍ͷͰɺ͓·͡ͳ͍͕ඞཁ extension NSTextField { open override

    func performKeyEquivalent(with event: NSEvent) -> Bool { let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if flags == [.command] { let selector: Selector switch event.charactersIgnoringModifiers?.lowercased() { case "x": selector = #selector(NSText.cut(_:)) case "c": selector = #selector(NSText.copy(_:)) case "v": selector = #selector(NSText.paste(_:)) case "a": selector = #selector(NSText.selectAll(_:)) case "z": selector = Selector(("undo:")) default: return super.performKeyEquivalent(with: event) } return NSApp.sendAction(selector, to: nil, from: self) } else if flags == [.shift, .command] { if event.charactersIgnoringModifiers?.lowercased() == "z" { return NSApp.sendAction(Selector(("redo:")), to: nil, from: self) } self.undoManager?.undo() } return super.performKeyEquivalent(with: event) } } ϝχϡʔόʔ͕ͳ͘ʮฤूʯܥͷ FirstResponder ͱͷܨ͕ΓΛ ࣋ͨͳ͍ͨΊɺNSTextField ͷ performKeyEquivalent Ͱ γϣʔτΧοτ͝ͱʹॲཧΛׂΓ౰ͯΔඞཁ͕͋Δ
  19. 'JOEFS4ZOD&YUFOTJPOͷ஫ҙ఺  'JOEFSػೳ֦ு͸Ϣʔβ͕બ୒ͨ͠ϑΝΠϧͰ͋Δʹ΋͔͔ΘΒͣɺ ಡΈॻ͖ݖݶʢ&OUJUMFNFOUTʣ͕௨༻͠ͳ͍৔߹͕͋Δ 㾎 /40QFO1BOFM΍/44BWF1BOFMΛ࢖ͬͯϑΝΠϧύεΛऔಘ͢Ε͹ ಡΈॻ͖ݖݶ͕௨༻͢Δ 㾎 "QQ4BOECPY5FNQPSBSZ&YDFQUJPOͰ͋Δ DPNBQQMFTFDVSJUZUFNQPSBSZFYDFQUJPOpMFTBCTPMVUFQBUISFBEXSJUF

    ·ͨ͸ DPNBQQMFTFDVSJUZUFNQPSBSZFYDFQUJPOpMFTIPNFSFMBUJWFQBUISFBEXSJUF Λ༻͍ͯ؂ࢹ͢ΔϑΥϧμͷύεΛࢦఆ͢Ε͹ಡΈॻ͖͕ՄೳʹͳΔ App Store Ͱ഑৴Ͱ͖Δ͔͸৹ࠪһͱͷަব࣍ୈ Temporary Exception Λ෇༩͢Δ৔߹͸ɺͳͥͦΕ͕ඞཁͳͷ͔ ৹ࠪһʹઆ໌͢Δඞཁ͕͋Δ
  20. 'JOEFS4ZOD&YUFOTJPOͷ஫ҙ఺  "QQ4BOECPYԼͰ؂ࢹ͢ΔϑΥϧμΛࢦఆ͢Δͷ͸໽հ ‣ "QQ4BOECPY༗ޮ࣌͸ૉ௚ͳύεΛऔಘͰ͖ͳ͍ let homeDirectoryPath = NSHomeDirectory() let

    desktopPath = FileManager.default .urls(for: .desktopDirectory, in: .userDomainMask) .first!.path // App Sandbox ແޮ࣌ /Users/UserName /Users/UserName/Desktop // App Sandbox ༗ޮ࣌ /Users/UserName/Library/Containers/AppBundleIdentifier/Data /Users/UserName/Library/Containers/AppBundleIdentifier/Data/Desktop
  21. 'JOEFS4ZOD&YUFOTJPOͷ஫ҙ఺  "QQ4BOECPYԼͰ؂ࢹ͢ΔϑΥϧμΛࢦఆ͢Δͷ͸໽հ ‎ %BSXJOͷ"1*Ͱڧ੍తʹϢʔβͷϗʔϜϑΥϧμύεΛऔಘՄೳ if let pw = getpwuid(getuid()),

    let home = pw.pointee.pw_dir { let homePath = FileManager.default .string(withFileSystemRepresentation: home, length: strlen(home)) // homePath => /Users/UserName let targetURL = URL(fileURLWithPath: homePath) .appendingPathComponent("Desktop") FIFinderSyncController.default().directoryURLs = [targetURL] }
  22. ·ͱΊ  'JOEFS4ZOD&YUFOTJPO͸ৗறܕϢʔςΟϦςΟπʔϧͷ࡞੒ʹͽͬͨΓʂ ‣ ࢦఆͨ͠ϑΥϧμ಺ͷঢ়ଶ؂ࢹ ‣ ϑΝΠϧ΍ϑΥϧμ΁ͷόοδͷ෇༩ ‣ ΧελϜίϯςΩετϝχϡʔʹΑΔλεΫ࣮ߦ ‣

    ΧελϜπʔϧόʔϘλϯʹΑΔλεΫ࣮ߦ  σόοάํ๏͕ಛघ  "QQ,JUͷμΠΞϩάܥ6*Λ࢖ָ࣮ͬͯͯ͠૷  ϑΝΠϧͷಡΈॻ͖ݖݶʹ͸஫ҙʂ