使用“apple watch complication”显示当前比特币价格

有时候我们想要扫一眼手表就可以看到当前的比特币价格。这个时候,我们可以考虑把价格显示到apple watch的complication这个widget中。

complication 是什么

所谓的complication其实就是显示在自定义表盘上的这些模块了。其中有大有小。比如下面的例子,除了时间以外,
有表示日期,温度,运动量的小模块,也有表示日程安排的大模块。

那么为了表示我们需要的比特币价格的模块,有哪些方法呢?

方法1:通过WatchConnectivity

WatchConnectivity 这个Framework用来实现ios和watchos的通信。

这个时候很显然我们要自己建一个ios的application,然后通过这个application把当前的比特币价格传送给apple watch,然后表示在表盘的complications上面。 用xcode新建watch application然后勾选上complications的选项这个就不说了。

ios获取数据

我们看到bitflyer提供了realtime的PubNub的subscribe key。利用这个Key我们可以连续获得ticker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//AppDelegate.swift

import PubNub

// 需要 delegate PNObjectEventListener

// ...省略

let configuration = PNConfiguration(publishKey: "demo", subscribeKey: "sub-c-52a9ab50-291b-11e5-baaa-0619f8945a4f")
self.client = PubNub.clientWithConfiguration(configuration)
self.client.addListener(self)
self.client.subscribeToChannels(["lightning_ticker_BTC_JPY"], withPresence: false)

// 添加实现delegate的fucntion

func client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {

let bicoinInfo = message.data.message as! NSDictionary

// bitcoinInfo是一个词典类型,保存了诸如`best_ask`, `best_bid`, `ltp` 之类的情报。
// bitcoinInfo["ltp"] => 299900
// 我们需要把这个东西传给apple watch。
}

ios传送数据

导入 WatchConnectivity, 实现WCSessionDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 导入
import WatchConnectivity

// 初始化
if WCSession.isSupported() {
self.session.delegate = self
self.session.activate()
}

// 实现delegate
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
// 将当前的tick保存在lastestTick中。
// 获取 lastTick
self.session.transferUserInfo(["tick": lastestTick])
}

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}

watchos extension接受数据

watchos 需要接受ios传来的数据并刷新当前的complications来表示最新的价格。

这里注意一点是complications的更新,在watchos里面会有一个计数,一天更新超过一定的次数就无法更新了。
在上面的ios的实现中,我们通过didReceiveMessage 来触发传输给watchos的信息,所以我们在这里要主动发送一个空的信息,来让ios把信息发过来。

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
// 同样需要导入WatchConnectivity

import WatchConnectivity

// 初始化

func applicationDidFinishLaunching() {
if WCSession.isSupported() {
self.session.delegate = self
self.session.activate()
}
// Perform any final initialization of your application.
}

// 实现delegate

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {

}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
let lastTick = userInfo["tick"] as! NSDictionary
// 更新持有的tick数据
TickData.setTick(tick: lastTick)
let complicationServer = CLKComplicationServer.sharedInstance()
guard let activeComplications = complicationServer.activeComplications else {
return
}

for complication in activeComplications {
complicationServer.reloadTimeline(for: complication)
}

}

注意我们这里主要实现didReceiveUserInfo,这个函数实现对通过transferUserInfo传过来的数据进行处理。

在具体的处理实现中,我们获取传来的lastTick, 然后通过complicationServer.reloadTimeline来刷新complication。

watchos complication 模版和渲染

ComplicationController.swift,我们可以看到xcode已经帮我们delegate了CLKComplicationDataSource

首先我们得决定当我们没有任何数据的时候,我们的complication表示什么内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MARK: - Placeholder Templates

func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
// This method will be called once per supported complication, and the results will be cached
let template = CLKComplicationTemplateModularLargeStandardBody()
let image = UIImage(named: "bitcoin")

template.headerImageProvider =
CLKImageProvider(onePieceImage: image!)

template.headerTextProvider =
CLKSimpleTextProvider(text: "BTC/JPY")
template.body1TextProvider =
CLKSimpleTextProvider(text: "0")

handler(template)
}

CLKComplicationTemplateModularLargeStandardBody()用来新建一个ModularLarge尺寸的模版,这个模版的就是我们上图看到的日程安排那块的大小。

接着我们实现getCurrentTimelineEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MARK: - Timeline Population

func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {

// Call the handler with the current timeline entry
let lastTick = TickData.getTick()
let ltp = String(describing: lastTick["ltp"] as! Int)
let template = CLKComplicationTemplateModularLargeStandardBody()
let image = UIImage(named: "bitcoin")

template.headerImageProvider =
CLKImageProvider(onePieceImage: image!)

template.headerTextProvider =
CLKSimpleTextProvider(text: "BTC/JPY")
template.body1TextProvider =
CLKSimpleTextProvider(text: ltp)

let entry = CLKComplicationTimelineEntry.init(date: Date(), complicationTemplate: template)

handler(entry)

}

在这里做的就是通过lastTick这个Datasource把数据获取数据,然后建立一个模版,和时间一起封装成一个CLKComplicationTimelineEntry的实例然后交给handler。

到这里,我们已经可以在complication上面显示比特币的价格了。

手表截图

方法2 使用PushOver的Glances API

上面的实装要是了解原理也不是那么复杂,但是等我们真正build这个application,上传,审核,发布,那可是很长一段岁月。
更何况往往我们并没有那么多耐心去折腾。

很多时候,稍微花一点钱,我们的问题很快就可以迎刃而解。比如我们可以使用PushOver的glances api来实现在complication上表示自定义信息的需求。

文档已经很详细就不多说了,唯一需要注意的,还是complication不接受太频繁的更新这一点。

手表截图2

总结

技术往往是达成目的的手段。当我们一味追求技术的时候,往往会忽视一些最根本的问题。
有些东西,如果是花钱就能解决的问题,那么不妨考虑一下花这个钱好了。