前言
由于苹果的玄学,我的handoff功能失效了。而我经常有这样的需求:手机打开电脑的当前网页。 Handoff功能失效前的操作步骤是:复制链接(chrome),手机上打开Safari打开链接。失效后我只能通过第三方软件发送链接到手机,复制链接,再粘贴链接到Safari。 幸好发现了这个软件,让我节省不少工作时间,但是算下来并没节省多少,这需要让我打开这个软件,点击软件上的按钮才能用airdrop。 于是就开发了自己的commandline。
食用方法
Usage:AirShare -c Share chrome current tab urlAirShare -s Share safari current tab urlAirShare -h Show usage informationType AirShare without an option to share chrome current tab URL.
Step0 项目过程

编写的Swift程序可以无缝调用AppleScript,通过AppleScript可以获取chrome/safari浏览器当前页面链接,再将链接返回给程序,继续调用苹果的ShareService,可以分享到推特/微博/mail/airdrop。
Step1 初始化项目

注意这里新建的项目是CommandLineTool。
CommandLineTool入口文件是main.swift
,传统macOS/iOS项目都会用@NSApplicationMain/@UIApplicationMain
来简化入口文件,CLT会顺序执行main.swift
中代码,如果不手动添加application,程序将会在main最后一行执行完毕后退出,那么我们如何执行异步操作呢?比如项目里异步调用ShareService
服务?下文再述。
Step2 格式化输出
cmd虽然没有UI花哨的界面,但单调的输出也是比较乏味的。多亏喵神的RainBow,可以对console加点颜色。
enum OutputType { case error case standard}
class ConsoleIO { func writeMessage(_ message: String, to: OutputType = .standard) { switch to { case .standard: print("\(message)") case .error: print("\(message)\n".red.bold) } } func printUsage() { let executableName = (CommandLine.arguments[0] as NSString).lastPathComponent writeMessage("Usage:") // .. }}
这里将程序的标准输出进行了分类:
- 标准输出
- 错误输出
错误输出红色粗体。
其他输出默认颜色。
printUsage
函数是CLI的食用方法打印。
Step3 关键代码
func getUrl(with cmd: String) { var error: NSDictionary? guard let scriptObject = NSAppleScript(source: cmd) else { consoleIO.writeMessage("Cannot attach to browser.", to: .error) exit(1) } let output = scriptObject.executeAndReturnError(&error) if error != nil { consoleIO.writeMessage("\(String(describing: error))", to: .error) exit(1) } guard let urlString = output.stringValue, let url = URL(string: urlString) else { consoleIO.writeMessage("Cannot resolve correct URL.", to: .error) exit(1) } share(url) } func share(_ url: URL) { let service = NSSharingService(named: .sendViaAirDrop)! let items: [URL] = [url] if service.canPerform(withItems: items) { service.delegate = self service.perform(withItems: items) } else { consoleIO.writeMessage("Cannot perform", to: .error) exit(1) } }
getUrl
方法通过执行AppleScript来获取浏览器当前页面链接,具体的script如下:
let CHROME_SCRIPT = "tell application \"Google Chrome\" to get URL of active tab of front window as string"
let SAFARI_SCRIPT = "tell application \"Safari\" to return URL of front document as string"
share
方法将传入的url通过调用系统Service分享到airdrop。
Step4 解决异步
step1提到了如何异步操作时候,程序不退出。有两种方法。
- 使用while循环,获取用户输入
FileHandle.standardInput
- 手动添加NSApplication
本程序采用的是第二种方法,因为使用第一种方法会报fault] 0 is not a valid connection ID.
这个莫名的错误,导致不能成功调出airdrop。
import cocoalet air = AirShare()air.run()
let app = NSApplication.sharedapp.delegate = airapp.run()
这里会生成一个app在后台跑着。

传统的iOS/macOS,只要run程序,就会调出来一个模拟器/有无窗体的window,如何禁止调出来而在后台跑呢? 可以在.plist中添加一个:
Application is agent (UIElement): YES
mattt大神的terminal-share就是这样做的。
Step5 调试与发布


点击了run,其实就是执行了./path/program
这样一个命令,可以通过图一图二添加其他运行参数,这样就是执行了./path/program -c
。


也可以在finder中找到程序本身,cd到目录下运行之。
确保程序完备后,可以添加此CLI到系统:
$ cp AirShare /usr/local/bin
这样在任何目录下都可以识别AirShare
这个命令了。
Step6 @TODO
每次执行程序都会遇到这样的警告,这貌似是系统的bug,网上也没有解决方法,并且每次使用此命令调用系统的airdrop,airdrop窗口总是会在所有窗口最下面。应该是这个系统bug造成的。
2017-12-11 13:37:24.187 AirShare[1106:17796] warning: illegal subclass SHKRemoteView instantiating; client should use only NSRemoteView ( 0 ViewBridge 0x00007fff5f063bff -[NSRemoteView _preSuperInit] + 195 1 ViewBridge 0x00007fff5f063f83 -[NSRemoteView initWithFrame:] + 25 2 ShareKit 0x00007fff5b448aa5 -[SHKRemoteView initWithOptionsDictionary:] + 161 3 ShareKit 0x00007fff5b427fbd __38-[SHKSharingService performWithItems:]_block_invoke_4 + 1347 4 libdispatch.dylib 0x00007fff622e1591 _dispatch_call_block_and_release + 12 5 libdispatch.dylib 0x00007fff622d9d50 _dispatch_client_callout + 8 6 libdispatch.dylib 0x00007fff622e532d _dispatch_main_queue_callback_4CF + 1148 7 CoreFoundation 0x00007fff3aafd7a9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9 8 CoreFoundation 0x00007fff3aabf9ca __CFRunLoopRun + 2586 9 CoreFoundation 0x00007fff3aabed23 CFRunLoopRunSpecific + 483 10 HIToolbox 0x00007fff39dd6e26 RunCurrentEventLoopInMode + 286 11 HIToolbox 0x00007fff39dd6b96 ReceiveNextEventCommon + 613 12 HIToolbox 0x00007fff39dd6914 _BlockUntilNextEventMatchingListInModeWithFilter + 64 13 AppKit 0x00007fff380a1f5f _DPSNextEvent + 2085 14 AppKit 0x00007fff38837b4c -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044 15 AppKit 0x00007fff38096d6d -[NSApplication run] + 764 16 AirShare 0x000000010ae32d76 main + 246 17 libdyld.dylib 0x00007fff62313115 start + 1 18 ??? 0x0000000000000001 0x0 + 1)
将错误输出到错误日志:
$ AirShare -c 2>air_share_error.log