├── .gitignore ├── Extension ├── Extension.entitlements ├── Info.plist ├── SourceEditorCommand.swift ├── SourceEditorExtension.swift ├── Utilities │ ├── SweetSourceEditorCommand.swift │ ├── UTI.swift │ └── XCTextBuffer+Extension.swift └── XcodeExtensionSample-Bridging-Header.h ├── LICENSE ├── README.md ├── XcodeExtensionSample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ ├── Extension.xcscheme │ │ ├── XcodeExtensionSample.xcscheme │ │ └── XcodeExtensionSampleHelper.xcscheme └── xcuserdata │ └── tksk_mba.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── XcodeExtensionSample ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── ViewController.swift └── XcodeExtensionSample.entitlements └── XcodeExtensionSampleHelper ├── Info.plist ├── XcodeExtensionSampleHelper-Bridging-Header.h ├── XcodeExtensionSampleHelper.entitlements ├── XcodeExtensionSampleHelper.swift ├── XcodeExtensionSampleHelperProtocol.h └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /Extension/Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)io.github.takasek.XcodeExtensionSample 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Extension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Sample 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSExtension 26 | 27 | NSExtensionAttributes 28 | 29 | XCSourceEditorCommandDefinitions 30 | 31 | 32 | XCSourceEditorCommandClassName 33 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 34 | XCSourceEditorCommandIdentifier 35 | $(PRODUCT_BUNDLE_IDENTIFIER).SourceEditorCommand 36 | XCSourceEditorCommandName 37 | Source Editor Command 38 | 39 | 40 | XCSourceEditorExtensionPrincipalClass 41 | $(PRODUCT_MODULE_NAME).SourceEditorExtension 42 | 43 | NSExtensionPointIdentifier 44 | com.apple.dt.Xcode.extension.source-editor 45 | 46 | NSHumanReadableCopyright 47 | Copyright © 2017年 takasek. All rights reserved. 48 | 49 | 50 | -------------------------------------------------------------------------------- /Extension/SourceEditorCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorCommand.swift 3 | // Extension 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/07. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XcodeKit 11 | import Cocoa 12 | 13 | final class PasteboardOutputCommand: SweetSourceEditorCommand { 14 | override class var commandName: String { 15 | return "file UTI -> PasteBoard" 16 | } 17 | 18 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 19 | let pasteboard = NSPasteboard.general 20 | pasteboard.declareTypes([.string], owner: nil) 21 | pasteboard.setString(textBuffer.contentUTI, forType: .string) 22 | 23 | return true 24 | } 25 | } 26 | 27 | final class PasteboardInputCommand: SweetSourceEditorCommand { 28 | override class var commandName: String { 29 | return "PasteBoard -> cursor place" 30 | } 31 | 32 | enum Error: MessagedError { 33 | case hasNoText 34 | var message: String { 35 | switch self { 36 | case .hasNoText: return "Pasteboard has no text." 37 | } 38 | } 39 | } 40 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 41 | let pasteboard = NSPasteboard.general 42 | 43 | guard let text = pasteboard.pasteboardItems?.first? 44 | .string(forType: NSPasteboard.PasteboardType( 45 | rawValue: "public.utf8-plain-text" 46 | )) else { throw Error.hasNoText } 47 | 48 | try textBuffer.replaceSelection(by: text) 49 | 50 | return true 51 | } 52 | } 53 | 54 | final class OpenAppCommand: SweetSourceEditorCommand { 55 | override class var commandName: String { 56 | return "open Calendar" 57 | } 58 | 59 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 60 | 61 | NSWorkspace.shared.launchApplication("Calendar") 62 | 63 | return true 64 | } 65 | } 66 | 67 | final class URLSchemeCommand: SweetSourceEditorCommand { 68 | override class var commandName: String { 69 | return "selected text -> twitter://post" 70 | } 71 | 72 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 73 | let text = textBuffer.selectedText(includesUnselectedStartAndEnd: false, trimsIndents: true) 74 | 75 | var c = URLComponents(string: "twitter://post")! 76 | c.queryItems = [ 77 | URLQueryItem(name: "message", value: text) 78 | ] 79 | NSWorkspace.shared.open(c.url!) 80 | 81 | return true 82 | } 83 | } 84 | 85 | final class LocalCommandCommand: SweetSourceEditorCommand { 86 | override class var commandName: String { 87 | return "completeBuffer -> uppercased by tr -> completeBuffer" 88 | } 89 | 90 | enum Error: MessagedError { 91 | case commandFailed(String) 92 | var message: String { 93 | switch self { 94 | case .commandFailed(let message): return message 95 | } 96 | } 97 | } 98 | 99 | private func runTask(command: String, arguments: [String], standardInput: Pipe? = nil) throws -> Pipe { 100 | let task = Process(), standardOutput = Pipe(), standardError = Pipe() 101 | task.launchPath = "/usr/bin/env" 102 | task.arguments = [command] + arguments 103 | task.currentDirectoryPath = NSTemporaryDirectory() 104 | task.standardInput = standardInput 105 | task.standardOutput = standardOutput 106 | task.standardError = standardError 107 | task.launch() 108 | task.waitUntilExit() 109 | 110 | guard task.terminationStatus == 0 else { 111 | // 異常終了 112 | let errorOutput = String(data: standardError.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" 113 | throw Error.commandFailed(errorOutput) 114 | } 115 | return standardOutput 116 | } 117 | 118 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 119 | let tmpFilePath = NSTemporaryDirectory().appending("inputFile") 120 | 121 | try textBuffer.completeBuffer.write(toFile: tmpFilePath, atomically: true, encoding: .utf8) 122 | 123 | let catOutput = try runTask( 124 | command: "cat", 125 | arguments: [tmpFilePath] 126 | ) 127 | let trOutput = try runTask( 128 | command: "tr", 129 | arguments: ["[:lower:]", "[:upper:]"], 130 | standardInput: catOutput 131 | ) 132 | textBuffer.completeBuffer = String(data: trOutput.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" 133 | 134 | return true 135 | } 136 | } 137 | 138 | final class NetworkCommand: SweetSourceEditorCommand { 139 | override class var commandName: String { 140 | return "URLRequest -> cursor place" 141 | } 142 | 143 | enum MyError: MessagedError { 144 | case timedOut 145 | case connectionFailed(NSError?) 146 | var message: String { 147 | switch self { 148 | case .timedOut: return "connection timed out." 149 | case .connectionFailed(let error): return error?.localizedDescription ?? "unknown error." 150 | } 151 | } 152 | } 153 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 154 | print(textBuffer.contentUTI) 155 | 156 | enum Result { 157 | case success(String) 158 | case fail(MyError) 159 | } 160 | let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/get")!) 161 | 162 | let semaphore = DispatchSemaphore(value: 0) 163 | var result: Result = .fail(.timedOut) 164 | let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in 165 | if let res = data.flatMap({ String(data: $0, encoding: .utf8) }) { 166 | result = .success(res) 167 | } else { 168 | result = .fail(.connectionFailed(error as NSError?)) 169 | } 170 | semaphore.signal() 171 | } 172 | task.resume() 173 | _ = semaphore.wait(timeout: .now() + 10) 174 | 175 | switch result { 176 | case .fail(let e): throw e 177 | case .success(let r): try textBuffer.replaceSelection(by: r) 178 | } 179 | 180 | return true 181 | } 182 | } 183 | 184 | final class ToDesktopCommand1: SweetSourceEditorCommand { 185 | override class var commandName: String { 186 | return "completeBuffer -> desktop (permission denied)" 187 | } 188 | 189 | // this command doesn't work because of permission. You should pass through XPC. 190 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 191 | let dir = NSSearchPathForDirectoriesInDomains( 192 | .desktopDirectory, .userDomainMask, true 193 | ).first! 194 | 195 | try textBuffer.completeBuffer.write( 196 | toFile: dir + "/outputFile", 197 | atomically: true, encoding: .utf8 198 | ) 199 | 200 | return true 201 | } 202 | } 203 | 204 | final class ToDesktopCommand2: SweetSourceEditorCommand { 205 | override class var commandName: String { 206 | return "completeBuffer -> (XPC) -> desktop" 207 | } 208 | 209 | enum Error: MessagedError { 210 | case connectionFailed 211 | case executionFailed 212 | var message: String { 213 | switch self { 214 | case .connectionFailed: return "Failed to make connection." 215 | case .executionFailed: return "Execution failed." 216 | } 217 | } 218 | } 219 | 220 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 221 | let connection = NSXPCConnection(serviceName: "io.github.takasek.XcodeExtensionSampleHelper") 222 | connection.remoteObjectInterface = NSXPCInterface(with: XcodeExtensionSampleHelperProtocol.self) 223 | defer { 224 | connection.invalidate() 225 | } 226 | guard let helper = connection.remoteObjectProxy as? XcodeExtensionSampleHelperProtocol else { 227 | throw Error.connectionFailed 228 | } 229 | connection.resume() 230 | 231 | let semaphore = DispatchSemaphore(value: 0) 232 | var isSuccess = false 233 | 234 | connection.invalidationHandler = { 235 | print("invalid!") 236 | semaphore.signal() 237 | } 238 | 239 | let dir = NSSearchPathForDirectoriesInDomains( 240 | .desktopDirectory, .userDomainMask, true 241 | ).first! 242 | 243 | // this XPC connection doesn't work. I don't know why... 244 | // see fine example at https://github.com/norio-nomura/SwiftLintForXcode 245 | helper.write(text: textBuffer.completeBuffer, to: dir) { _ in 246 | isSuccess = true 247 | semaphore.signal() 248 | } 249 | _ = semaphore.wait(timeout: .now() + 10) 250 | 251 | if !isSuccess { 252 | throw Error.executionFailed 253 | } 254 | 255 | return true 256 | } 257 | } 258 | 259 | extension UserDefaults { 260 | @objc dynamic var valueFromApp: String? { 261 | return string(forKey: "valueFromApp") 262 | } 263 | } 264 | 265 | final class FileSelectionCommand: SweetSourceEditorCommand { 266 | override class var commandName: String { 267 | return "(App by URLScheme) -> select a file -> (UserDefaults) -> cursor place" 268 | } 269 | 270 | private var _applicationWillTerminate: (() -> Void)? 271 | @objc private func applicationWillTerminate(notification: Notification) { 272 | _applicationWillTerminate?() 273 | } 274 | 275 | enum Error: MessagedError { 276 | case noFileSelected 277 | var message: String { 278 | switch self { 279 | case .noFileSelected: return "No file selected." 280 | } 281 | } 282 | } 283 | 284 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 285 | let semaphore = DispatchSemaphore(value: 0) 286 | 287 | var selectedResult: String? 288 | 289 | // The command expects that the app set selected file path to UserDefaults. 290 | let userDefaults = UserDefaults(suiteName: "42U7855PYX.io.github.takasek.XcodeExtensionSample")! 291 | userDefaults.synchronize() 292 | let observation = userDefaults.observe(\UserDefaults.valueFromApp, options:[.old, .new]) { ud, change in 293 | selectedResult = change.newValue?.flatMap { $0 } 294 | semaphore.signal() 295 | } 296 | 297 | // this observation also works. 298 | DistributedNotificationCenter.default().addObserver( 299 | self, 300 | selector: #selector(FileSelectionCommand.applicationWillTerminate(notification:)), 301 | name: Notification.Name("XcodeExtensionSample.applicationWillTerminate"), 302 | object: nil, 303 | suspensionBehavior: .deliverImmediately 304 | ) 305 | _applicationWillTerminate = { 306 | // If the app terminated by user unexpectedly, this observation signals the semaphore. 307 | semaphore.signal() 308 | } 309 | 310 | // Open App via URL Scheme 311 | var c = URLComponents(string: "xcextsample://")! 312 | c.queryItems = [ 313 | URLQueryItem(name: "title", value: "text") 314 | ] 315 | NSWorkspace.shared.open(c.url!) 316 | 317 | _ = semaphore.wait() 318 | 319 | DistributedNotificationCenter.default().removeObserver(self) 320 | observation.invalidate() 321 | 322 | guard let result = selectedResult else { 323 | // cancel the command. 324 | throw Error.noFileSelected 325 | } 326 | 327 | try textBuffer.replaceSelection(by: result) 328 | 329 | return true 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /Extension/SourceEditorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorExtension.swift 3 | // Extension 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/07. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XcodeKit 11 | 12 | class SourceEditorExtension: NSObject, XCSourceEditorExtension { 13 | 14 | func extensionDidFinishLaunching() { 15 | // If your extension needs to do any work at launch, implement this optional method. 16 | print("launched!") 17 | } 18 | 19 | var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] { 20 | // If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter. 21 | 22 | return [ 23 | PasteboardOutputCommand.commandDefinition, 24 | PasteboardInputCommand.commandDefinition, 25 | OpenAppCommand.commandDefinition, 26 | URLSchemeCommand.commandDefinition, 27 | LocalCommandCommand.commandDefinition, 28 | NetworkCommand.commandDefinition, 29 | ToDesktopCommand1.commandDefinition, 30 | ToDesktopCommand2.commandDefinition, 31 | FileSelectionCommand.commandDefinition, 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Extension/Utilities/SweetSourceEditorCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SweetSourceEditorCommand.swift 3 | // ExtensionSample 4 | // 5 | // Created by Yoshitaka Seki on 2017/08/12. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XcodeKit 11 | 12 | /// Error with Message to tell the user. 13 | protocol MessagedError: Error { 14 | var message: String { get } 15 | } 16 | 17 | class SweetSourceEditorCommand: NSObject, XCSourceEditorCommand { 18 | /// can override 19 | var validUTIs: [UTI]? { 20 | return nil 21 | } 22 | 23 | /// for XCSourceEditorCommandDefinitionKey.nameKey. 24 | /// can be overridden. 25 | class var commandName: String { 26 | return className() 27 | } 28 | 29 | /// for XCSourceEditorCommandDefinitionKey.identifierKey. 30 | /// can be overridden. 31 | class var commandIdentifier: String { 32 | let bundleIdentifiler = Bundle.main.bundleIdentifier! 33 | return bundleIdentifiler + "." + className() 34 | } 35 | 36 | /// should be overridden 37 | func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool { 38 | fatalError("should be implemented") 39 | } 40 | 41 | // MARK: - sweet wrappers 42 | 43 | class var commandDefinition: [XCSourceEditorCommandDefinitionKey: String] { 44 | return [ 45 | .nameKey: commandName, 46 | .classNameKey: className(), 47 | .identifierKey: commandIdentifier 48 | ] 49 | } 50 | 51 | enum Error: MessagedError { 52 | case invalidUTI 53 | var message: String { 54 | switch self { 55 | case .invalidUTI: return "invalid UTI" 56 | } 57 | } 58 | } 59 | 60 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Swift.Error?) -> Void ) { 61 | enum Closing { 62 | case complete(Swift.Error?) 63 | case cancel 64 | } 65 | var closing: Closing = .complete(nil) 66 | defer { 67 | switch closing { 68 | case .complete(let error): 69 | let returningError: Swift.Error? 70 | if let error = error { 71 | print(error) 72 | if type(of: error) == NSError.self { 73 | returningError = error 74 | } else if let e = error as? MessagedError { 75 | returningError = NSError(domain: "MessagedError", code: 0, userInfo: [NSLocalizedDescriptionKey: e.message]) 76 | } else { 77 | returningError = NSError(domain: "Swift.Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error as Any)"]) 78 | } 79 | } else { 80 | returningError = nil 81 | } 82 | completionHandler(returningError) 83 | case .cancel: 84 | print("cancelled") 85 | invocation.cancellationHandler() 86 | } 87 | } 88 | 89 | do { 90 | let textBuffer = invocation.buffer 91 | 92 | switch validUTIs?.contains(textBuffer.typedContentUTI) { 93 | case nil: 94 | () // all UTIs can execute the command. 95 | case true?: 96 | () // this UTI can execute the command. 97 | case false?: 98 | throw Error.invalidUTI 99 | } 100 | 101 | guard try performImpl(with: textBuffer) else { 102 | closing = .cancel 103 | return 104 | } 105 | 106 | // complete with no error 107 | 108 | } catch let caughtError { 109 | closing = .complete(caughtError) 110 | return 111 | } 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /Extension/Utilities/UTI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UTI.swift 3 | // ExtensionSample 4 | // 5 | // Created by Yoshitaka Seki on 2017/08/12. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct UTI: Equatable { 12 | let value: String 13 | 14 | static let swiftSource = UTI(value: "public.swift-source") 15 | static let cHeader = UTI(value: "public.c-header") 16 | static let objCSource = UTI(value: "public.objective-c-source") 17 | static let playground = UTI(value: "com.apple.dt.playground") 18 | static let playgroundPage = UTI(value: "com.apple.dt.playgroundpage") 19 | static let storyboard = UTI(value: "com.apple.InterfaceBuilder3.Storyboard.XIB") 20 | static let xib = UTI(value: "com.apple.InterfaceBuilder3.Cocoa.XIB") 21 | static let markdown = UTI(value: "net.daringfireball.markdown") 22 | static let xml = UTI(value: "public.xml") 23 | static let json = UTI(value: "public.json") 24 | static let plist = UTI(value: "com.apple.xml-property-list") 25 | static let entitlement = UTI(value: "com.apple.xcode.entitlements-property-list") 26 | 27 | func conforms(to uti: UTI) -> Bool { 28 | return UTTypeConformsTo(value as CFString, uti.value as CFString) 29 | } 30 | 31 | static func ~= (pattern: UTI, value: UTI) -> Bool { 32 | return value.conforms(to: pattern) 33 | } 34 | 35 | var fileExtension: String? { 36 | return UTTypeCopyPreferredTagWithClass( 37 | value as CFString, 38 | kUTTagClassFilenameExtension 39 | )?.takeRetainedValue() as String? 40 | } 41 | static func == (lhs: UTI, rhs: UTI) -> Bool { 42 | return lhs.value == rhs.value 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Extension/Utilities/XCTextBuffer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTextBuffer+Extension.swift 3 | // ExtensionSample 4 | // 5 | // Created by Yoshitaka Seki on 2017/08/12. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XcodeKit 11 | 12 | extension XCSourceTextBuffer { 13 | var typedContentUTI: UTI { 14 | return UTI(value: contentUTI) 15 | } 16 | func selectedText(includesUnselectedStartAndEnd: Bool, trimsIndents: Bool) -> String { 17 | var texts: [NSString] = [] 18 | var minimumIndentLength = Int.max 19 | 20 | selections 21 | .map { $0 as! XCSourceTextRange } 22 | .forEach { selection in 23 | for line in selection.start.line...selection.end.line { 24 | guard line < lines.count else { 25 | // if you select none and place the cursor on last line, it can be out of bound of lines 26 | return 27 | } 28 | var text = self.lines[line] as! NSString 29 | 30 | if !includesUnselectedStartAndEnd { 31 | if line == selection.end.line { 32 | text = text.substring(to: selection.end.column) as NSString 33 | } 34 | if line == selection.start.line { 35 | text = text.substring(from: selection.start.column) as NSString 36 | } 37 | } 38 | 39 | text = text.trimmingCharacters(in: CharacterSet(charactersIn: "\n")) as NSString 40 | 41 | texts.append(text) 42 | 43 | if text.length != 0 { 44 | // race with latest indent length 45 | minimumIndentLength = min(minimumIndentLength, text.indentLength) 46 | } 47 | } 48 | } 49 | 50 | return texts 51 | .map { 52 | if trimsIndents { 53 | return $0.length >= minimumIndentLength 54 | ? $0.substring(from: minimumIndentLength) 55 | : $0 as String 56 | } else { 57 | return $0 as String 58 | } 59 | } 60 | .joined(separator: "\n") 61 | } 62 | 63 | enum Error: MessagedError { 64 | case noSelection 65 | case invalidLine 66 | 67 | var message: String { 68 | switch self { 69 | case .noSelection: return "no selection found." 70 | case .invalidLine: return "line is invalid." 71 | } 72 | } 73 | } 74 | 75 | func insertConsideringLastLineIndents(_ insertion: String, at lineNum: Int) throws { 76 | guard lineNum <= lines.count else { 77 | throw Error.invalidLine 78 | } 79 | 80 | let lastLine = lines[lineNum-1] as! NSString 81 | let prefix = String.init(repeating: " ", count: lastLine.indentLength) 82 | 83 | let fixedInsertion = insertion.components(separatedBy: "\n").map { 84 | if $0.isEmpty { 85 | return String($0) 86 | } else { 87 | return prefix + String($0) 88 | } 89 | }.joined(separator: "\n") 90 | 91 | lines.insert(fixedInsertion, at: lineNum) 92 | } 93 | 94 | func replaceSelection(by insertion: String) throws { 95 | guard let selection = selections.firstObject as? XCSourceTextRange else { throw Error.noSelection } 96 | 97 | var pre = "" 98 | var post = "" 99 | if selection.end.line < lines.count { 100 | pre = (lines[selection.start.line] as! NSString).substring(to: selection.start.column) 101 | post = (lines[selection.end.line] as! NSString).substring(to: selection.end.column) 102 | lines.removeObjects(at: IndexSet(integersIn: selection.start.line...selection.end.line)) 103 | } else { 104 | // selection.end.line may exceed lines.count at EOF 105 | } 106 | 107 | lines.insert(pre + insertion + post, at: selection.start.line) 108 | } 109 | } 110 | 111 | private extension NSString { 112 | var indentLength: Int { 113 | var result = 0 114 | for i in 0.. 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/Extension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 72 | 74 | 80 | 81 | 82 | 83 | 84 | 85 | 92 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/XcodeExtensionSample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/XcodeExtensionSampleHelper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /XcodeExtensionSample.xcodeproj/xcuserdata/tksk_mba.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Extension.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | XcodeExtensionSample.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | XcodeExtensionSampleHelper.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 2 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | B64AC1071F64209500155E54 26 | 27 | primary 28 | 29 | 30 | 31 | SuppressBuildableAutocreation 32 | 33 | B624E6E31F6155E00081E582 34 | 35 | primary 36 | 37 | 38 | B624E6FA1F61567E0081E582 39 | 40 | primary 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /XcodeExtensionSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // XcodeExtensionSample 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/07. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | func applicationWillFinishLaunching(_ aNotification: Notification) { 14 | NSAppleEventManager.shared().setEventHandler( 15 | self, 16 | andSelector: #selector(AppDelegate.handleGetURLEvent(event:replyEvent:)), 17 | forEventClass: AEEventClass(kInternetEventClass), 18 | andEventID: AEEventID(kAEGetURL) 19 | ) 20 | } 21 | 22 | private var selectedFileURL: URL? 23 | 24 | @objc func handleGetURLEvent(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) { 25 | defer { 26 | UserDefaults(suiteName: "42U7855PYX.io.github.takasek.XcodeExtensionSample")?.set(selectedFileURL?.absoluteString, forKey: "valueFromApp") 27 | 28 | DispatchQueue.main.async { 29 | NSApplication.shared.terminate(nil) 30 | } 31 | } 32 | guard 33 | let urlString = event?.paramDescriptor(forKeyword: keyDirectObject)?.stringValue, 34 | let components = URLComponents(string: urlString) 35 | else { 36 | return 37 | } 38 | 39 | guard 40 | let title = components.queryItems?.first(where: { $0.name == "title" })?.value, 41 | let url = self.selectFile(with: title) 42 | else { 43 | return 44 | } 45 | 46 | self.selectedFileURL = url 47 | } 48 | 49 | private func selectFile(with title: String) -> URL? { 50 | let panel = NSOpenPanel() 51 | 52 | panel.prompt = "Select" 53 | panel.canChooseDirectories = true 54 | panel.canCreateDirectories = true 55 | panel.canChooseFiles = true 56 | panel.title = "title" 57 | 58 | let url: URL? 59 | switch panel.runModal() { 60 | case .OK: 61 | url = panel.url 62 | case _: 63 | url = nil 64 | } 65 | return url 66 | } 67 | 68 | func applicationWillTerminate(_ aNotification: Notification) { 69 | DistributedNotificationCenter.default().postNotificationName( 70 | Notification.Name("XcodeExtensionSample.applicationWillTerminate"), 71 | object: nil, 72 | userInfo: selectedFileURL.flatMap { ["url": $0] }, 73 | deliverImmediately: true 74 | ) 75 | } 76 | 77 | func application(_ application: NSApplication, open urls: [URL]) { 78 | print(urls) 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /XcodeExtensionSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /XcodeExtensionSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | Default 529 | 530 | 531 | 532 | 533 | 534 | 535 | Left to Right 536 | 537 | 538 | 539 | 540 | 541 | 542 | Right to Left 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Default 554 | 555 | 556 | 557 | 558 | 559 | 560 | Left to Right 561 | 562 | 563 | 564 | 565 | 566 | 567 | Right to Left 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | -------------------------------------------------------------------------------- /XcodeExtensionSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLSchemes 27 | 28 | xcextsample 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSMinimumSystemVersion 35 | $(MACOSX_DEPLOYMENT_TARGET) 36 | NSHumanReadableCopyright 37 | Copyright © 2017年 takasek. All rights reserved. 38 | NSMainStoryboardFile 39 | Main 40 | NSPrincipalClass 41 | NSApplication 42 | 43 | 44 | -------------------------------------------------------------------------------- /XcodeExtensionSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // XcodeExtensionSample 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/07. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override var representedObject: Any? { 20 | didSet { 21 | // Update the view, if already loaded. 22 | } 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /XcodeExtensionSample/XcodeExtensionSample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)io.github.takasek.XcodeExtensionSample 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | XcodeExtensionSampleHelper 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSHumanReadableCopyright 24 | Copyright © 2017年 takasek. All rights reserved. 25 | XPCService 26 | 27 | ServiceType 28 | Application 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/XcodeExtensionSampleHelper-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "XcodeExtensionSampleHelperProtocol.h" 6 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/XcodeExtensionSampleHelper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/XcodeExtensionSampleHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeExtensionSampleHelper.swift 3 | // XcodeExtensionSampleHelper 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/09. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | 12 | @objc class XcodeExtensionSampleHelper: NSObject, XcodeExtensionSampleHelperProtocol { 13 | func write(text: String, to directory: String, reply: @escaping HelperResultHandler) { 14 | 15 | try? text.write( 16 | toFile: directory + "/outputFile", 17 | atomically: true, encoding: .utf8 18 | ) 19 | 20 | reply(0) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/XcodeExtensionSampleHelperProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeExtensionSampleHelperProtocol.h 3 | // XcodeExtensionSampleHelper 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/09. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | typedef void (^HelperResultHandler)(NSInteger status); 12 | 13 | @protocol XcodeExtensionSampleHelperProtocol 14 | 15 | - (void)writeText:(NSString * _Nonnull)text 16 | to:(NSString * _Nonnull)directory 17 | reply:(HelperResultHandler _Nonnull)reply 18 | NS_SWIFT_NAME(write(text:to:reply:)); 19 | 20 | @end 21 | 22 | 23 | -------------------------------------------------------------------------------- /XcodeExtensionSampleHelper/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // XcodeExtensionSampleHelper 4 | // 5 | // Created by Yoshitaka Seki on 2017/09/09. 6 | // Copyright © 2017年 takasek. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "XcodeExtensionSampleHelper-Swift.h" 11 | 12 | @interface ServiceDelegate : NSObject 13 | @end 14 | 15 | @implementation ServiceDelegate 16 | 17 | - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { 18 | // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. 19 | 20 | // Configure the connection. 21 | // First, set the interface that the exported object implements. 22 | newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XcodeExtensionSampleHelperProtocol)]; 23 | 24 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. 25 | XcodeExtensionSampleHelper *exportedObject = [XcodeExtensionSampleHelper new]; 26 | newConnection.exportedObject = exportedObject; 27 | 28 | // Resuming the connection allows the system to deliver more incoming messages. 29 | [newConnection resume]; 30 | 31 | // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. 32 | return YES; 33 | } 34 | 35 | @end 36 | 37 | int main(int argc, const char *argv[]) 38 | { 39 | // Create the delegate for the service. 40 | ServiceDelegate *delegate = [ServiceDelegate new]; 41 | 42 | // Set up the one NSXPCListener for this service. It will handle all incoming connections. 43 | NSXPCListener *listener = [NSXPCListener serviceListener]; 44 | listener.delegate = delegate; 45 | 46 | // Resuming the serviceListener starts this service. This method does not return. 47 | [listener resume]; 48 | return 0; 49 | } 50 | --------------------------------------------------------------------------------