skip to content
FaiChou's blog

airshare with swift

/ 7 min read

前言

由于苹果的玄学,我的handoff功能失效了。而我经常有这样的需求:手机打开电脑的当前网页。 Handoff功能失效前的操作步骤是:复制链接(chrome),手机上打开Safari打开链接。失效后我只能通过第三方软件发送链接到手机,复制链接,再粘贴链接到Safari。 幸好发现了这个软件,让我节省不少工作时间,但是算下来并没节省多少,这需要让我打开这个软件,点击软件上的按钮才能用airdrop。 于是就开发了自己的commandline

食用方法

Usage:
AirShare -c
Share chrome current tab url
AirShare -s
Share safari current tab url
AirShare -h
Show usage information
Type 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加点颜色。

ConsoleIO.swift
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:")
// ..
}
}

这里将程序的标准输出进行了分类:

  1. 标准输出
  2. 错误输出

错误输出红色粗体。 其他输出默认颜色。 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提到了如何异步操作时候,程序不退出。有两种方法。

  1. 使用while循环,获取用户输入FileHandle.standardInput
  2. 手动添加NSApplication

本程序采用的是第二种方法,因为使用第一种方法会报fault] 0 is not a valid connection ID.这个莫名的错误,导致不能成功调出airdrop。

main.swift
import cocoa
let air = AirShare()
air.run()
let app = NSApplication.shared
app.delegate = air
app.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

参考

  1. Swift Command Line Tutorial
  2. Mattt大神的terminal-share
  3. 开源的WebDrop
  4. ApplicationMain
  5. ApplicationMain2
  6. Application is agent
  7. ApplicationMain3