iOS 10 的一个重要更新-开发 iMessage 的第三方插件
蘋果官方的 Messages 在 iOS 10 推出了非常重大的更新,可能主要是想從其他 IM 巨頭手里搶點市場份額回來,包括 Facebook Messenger, Wechat 和 Snapchat。
?
一個重要的新功能是,用戶可以直接在 Messages 里使用第三方開發者開發的擴展插件了。這個功能是在 iOS 8 引入的 Extension 技術基礎上實現的,可以參考我們往年系列里 Sam Davies 寫的文章。Messages 插件的一大好處是,它是可以獨立于 app 存在的,不用跟父 app 打包在一起。今年晚些時候 iOS 10 將會發布一個小巧的 Messages App Store,里面會有一堆插件供用戶挑選。
?
為了演示一下這個令人興奮的插件功能,我們看一個簡單的例子吧,這個插件可以讓兩個用戶玩一個簡化版的流行游戲 Battleships。為了讓約束布局方面簡單一些,我們只考慮豎屏的情況。為方便大家下載這個 demo,我把它放到Github上了。
http://t.cn/RcLmZ0e
?
游戲規則是這樣的:
?
-
玩家 A 發起游戲,在棋盤上布置兩個『戰艦』,然后隱藏起來
-
另一個玩家 B 要猜測戰艦的位置
-
如果猜中了兩艘隱藏戰艦的位置,玩家 B 就贏了;但是如果猜錯 3 次,玩家 B 就輸了。
?
建工程
?
用 Xcode 新建一個插件工程非常簡單。只需點擊 File -> New Project,然后在窗口中選擇 iMessage Application。
給工程起個名字,然后語言選擇 Swift(本系列均使用 Swift 語言示例),這就完事了。因為有一個自動生成的MessagesExtensiontarget ,然后默認的Info.plist里帶有必需的配置(插件界面的 storyboard 以及插件的類型等),所以只要運行工程,Messages 就能自動識別出我們的插件了。
?
改 Display Name
?
如果在模擬器里運行MessagesExtension這個 target,它會讓你選擇在哪個 app 里運行這個插件。我們選擇Messages。
?
在 Messages 里運行
?
Messages 打開的時候,應該能在輸入框下方看到我們的插件。如果看不到,可能需要點擊 “Applications” icon,然后再點 4 個橢圓的 icon,從里面選擇我們的插件。
?
現在里面啥也沒有,不過我們將很快改變這一點。眼下最迫切的是要把我們插件的 display name 改改:現在顯示的是 “MessagesExtension”(實際上是 “MessagesEx…” 后面被截掉了)。下面我們點擊 target,然后把Display Name輸入框里的名字改一改。
?
改 display name
?
棋盤
?
我們需要展示的是 3×3 的棋盤。有很多實現方法,我用的是 UICollectionView。在本教程里,畫界面這一塊并不重要,因此實現細節不再詳述了。
?
數據模型
?
為了記錄一局游戲本身以及游戲的狀態,我們定義以下兩個結構體:
?
struct GameConstants {
????/// 一共需要布置的戰艦數
????static let totalShipCount = 2
????/// 允許玩家 B 失敗的次數
????static let incorrectAttemptsAllowed = 3
}
?
struct GameModel {
????/// 戰艦的位置
????let shipLocations: [Int]
????/// 游戲是否已經結束
????var isComplete: Bool
}
?
MessagesViewController
?
MessagesViewController 是我們插件的入口點。它是MSMessagesAppViewController的子類,相當于是 Messages 插件的 root View Controller。自動生成的模板里面包含了一些供我們重寫的方法,比如插件啟動狀態下用戶收到消息的回調函數。待會我們就要用到其中的一部分方法。
?
第一點要注意的是,我們的插件啟動之后有兩種可能的 presentation style:
?
-
compact
-
expanded
?
compact是用戶從應用托盤里打開插件的模式,插件顯示在鍵盤區域里。expanded則多給了一些喘息的空間,插件占據大部分的屏幕。
?
為了讓代碼整潔一些,我們會用不同的 view controller 來分別實現兩種模式,并且把這些 view Controller 都加為MessagesViewController的子 view controller。
?
幾個子 View Controller
?
本文不會花太長篇幅來描述這些 controller 的實現細節,只會重點關注在收發信息的過程,游戲狀態和數據是怎么變化的。關于具體實現,請自行閱讀 Github 上的源碼。
?
GameStartViewController
?
我們的插件剛啟動的時候處于compact狀態。這點空間并不夠展示游戲的棋盤,在 iPhone 上尤其不夠。我們可以簡單粗暴地立即切換成expanded狀態,但是蘋果官方警告不要這么做,畢竟還是應該把控制權交給用戶。
?
于是,我們來顯示一個簡單的歡迎界面,里面有一個 label 和一個 button。按下 button 的時候,再切換到游戲的主界面,用戶就可以開始放置『戰艦』了。
?
Ship Location View Controller
?
這個 view controller 是玩家 A 布置戰艦的界面。
?
我們實現gameBoard的onCellSelection方法來控制 cell 的樣式:上面有戰艦的 cell 顯示為綠色,空白的顯示為藍色。
?
shipsLeftToPosition返回 0 時,結束按鈕會變得可點。這個按鈕的點擊事件是一個叫completedShipLocationSelection:的IBAction方法,它會新建一個游戲 model,然后使用 UIImage 的 extension 來創建一張游戲棋盤的截圖(我們會先reset()棋盤,所以截圖的時候戰艦的位置是隱藏的——現在可不是揭曉謎底的時候!)。這張截圖在待會發消息的時候會用到。
?
Ship Destroy View Controller
?
當玩家 B 點擊對話中的消息時,我們希望他能看到一個略微不同的 view controller —— 一個能讓他尋找隱藏戰艦的界面。
?
我們還是實現棋盤的onCellSelection方法。這一次我們把選擇的 cell 位置與玩家 A 布置的位置匹配的(『擊中戰艦』)標為綠色,如果沒有擊中就標為紅色。
?
游戲結束后,不管是因為 3 條命用完了,還是因為兩條戰艦都找出來了,我們都會相應地記錄在數據模型中,然后調起游戲結束的回調。
?
添加子 Controller
?
回到我們的MessagesViewController,我們現在可以把子 controller 們加進去了。
?
class MessagesViewController: MSMessagesAppViewController {
????override func willBecomeActive(with conversation: MSConversation) {
????????configureChildViewController(for: presentationStyle, with: conversation)
????}
?
????override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
????????guard let conversation = self.activeConversation else { return }
????????configureChildViewController(for: presentationStyle, with: conversation)
????}
}
?
這兩個方法是繼承自MSMessagesAppViewController的,分別提醒我們插件啟動了(比如被用戶打開了)以及要變換到另一種 presentation style 了。我們利用這兩個方法來配置子 view controller。
?
上面這個方法決定了我們該向當前的用戶展示哪個子 view controller。如果處于compact 模式,那么應該顯示 “start game” 界面。
?
如果處于expanded模式,我們需要判斷是 A 玩家還是 B 玩家。如果是 B 玩家在對話界面中點擊消息,此時conversation.selectedMessage就不會是 nil,這說明游戲已經開始了,所以我們要展示ShipDestroyViewController。否則就展示ShipLocationViewController。
?
切換界面模式
?
在GameStartViewController點擊 “start game” 按鈕,我們希望插件能切換到expanded模式,好讓我們展示棋盤。
?
// 在 'createGameStartViewController' 里
controller.onButtonTap = {
????[unowned self] in
????self.requestPresentationStyle(.expanded)
}
?
?
切換到 expanded 模式
?
創建『可以更新』的消息
?
之前在 Messages 里面,任何新的內容——不管是新的短信還是表情——都會以一條新消息的形式出現在對話的底部,跟之前的所有消息都不相干。
?
然而,這一點可能帶來很多麻煩:比如,一個下國際象棋的游戲插件會造成每走一步棋都要發一條新消息。而我們理想中的情況應該是更新后的消息能代替之前的消息。
?
謝天謝地,蘋果也想到了這一點,給我們提供了一個類MSSession——這個類沒有屬性也沒有方法,只是用來更新消息的。
?
我們發一條消息的時候,就用這個 session 來告訴 Messages,要覆蓋此前 session 相同的信息。前一條信息會被從聊天記錄中移除,然后新的信息插入到底部。
?
使用聯系人姓名
?
最近幾年,蘋果一直說要把保護用戶隱私當做頭等大事。對 Messages framework 來說確實如此:你并不能得到用戶的身份,只能得到一個每個設備不同的UUID。也就是說,你不能在消息里加入發消息的用戶的身份 ID,然后指望收消息的用戶能通過這個 ID 識別出發消息的是誰。
?
另外,你只能訪問到用戶點擊的那條消息的內容,不能訪問到對話中任何其他消息的內容(而且點擊的這條消息還必須是從你的插件發出來的)。
?
MSConversation 這個類有兩個屬性localParticipantIdentifier和remoteParticipantIdentfiers,可以用來顯示對話雙方的名字。要加一個前綴$。
?
let player = "$\(conversation.localParticipantIdentifier)"
?
把它放在消息里發出去,Messages 會解析這個 UUID,然后顯示出對應的聯系人姓名。
?
顯示聯系人姓名
?
收發應用數據
?
游戲狀態的數據是以 URL 的形式傳遞的。你的插件裝在任意一臺手機上,都應該有能力解析這個 URL,展示相關的內容。
?
使用 URL 的另一個好處是,它還能為 MacOS 用戶提供一個備用方案。不幸的是,MacOS 上的 Messages 應用并不支持插件功能。文檔里是這樣說的:
?
如果在 macOS 上點擊這條信息,系統會轉到 web 瀏覽器打開這個 URL。所以這個 URL 應該定向到你自己的 web service,基于 URL 里 encode 的數據為用戶呈現合理的結果。
?
要構建這個 URL,我們可以使用URLComponents,組合一個 base url 和一群URLQueryItems(都是有效的鍵值對)。
?
extension GameModel {
????func encode() -> URL {
????????let baseURL = "www.shinobicontrols.com/battleship"
?
????????guard var components = URLComponents(string: baseURL) else {
????????????fatalError("Invalid base url")
????????}
?
????????var items = [URLQueryItem]()
?
????????// 戰艦的位置
????????let locationItems = shipLocations.map {
????????????location in
????????????URLQueryItem(name: "Ship_Location", value: String(location))
????????}
?
????????items.append(contentsOf: locationItems)
?
????????// 游戲結束
????????let complete = isComplete ? "1" : "0"
?
????????let completeItem = URLQueryItem(name: "Is_Complete", value: complete)
????????items.append(completeItem)
?
????????components.queryItems = items
?
????????guard let url = components.url else {
????????????fatalError("Invalid URL components")
????????}
?
????????return url
????}
}
?
最后得出的 url 結果形如:www.shinobicontrols.com/battleship?Ship_Location=0&Ship_Location=1&Is_Complete=0
?
而解碼基本與此過程相反:先得到 url,取出每個鍵值對,由每個對應的值來構建游戲的數據模型。
?
在聊天對話中插入信息
?
經過前面的艱苦努力,我們終于創建出了這條消息,準備好讓玩家在對話中發給其他玩家了。
?
/// 構建一條消息,然后插入到對話中
func insertMessageWith(caption: String,
?????????????????? _ model: GameModel,
?????????????????? _ session: MSSession,
?????????????????? _ image: UIImage,
?????????????????? in conversation: MSConversation) {
????let message = MSMessage(session: session)
????let template = MSMessageTemplateLayout()
????template.image = image
????template.caption = caption
????message.layout = template
????message.url = model.encode()
?
????// 我們構建好這條消息之后,把它插入對話中
????conversation.insert(message)
}
?
就像前面說過的那樣,這條消息是用一個 session 創建的,這樣我們就可以覆蓋對話中同一個 session 的信息了。
?
為了修改消息的外觀,我們要用到MSMessageTemplateLayout。它能讓我們修改消息的一系列屬性,在這個例子里主要用到caption(文字)和image(圖片)。
?
修改完消息的外觀,配置好 session 和 URL 屬性,我們終于可以把消息插進對話中了。最后這行代碼會把消息放進 Messages 的輸入框里。注意:我們沒有權限直接把這條消息發出去——只能放進輸入框里。
?
結束啦
?
插入完這條消息之后,我們的插件也沒有必要再在這閑待著了。用戶可以手動把它關掉,不過為了讓他們體驗好一點,所以我們調用這行代碼,自己結束掉MessagesViewController的生命:
?
self.dismiss()
?
?
擴展閱讀
?
謝謝你看完這么長一篇文章,希望能讓你對于 iOS 10 Message 應用的強大功能略窺一二。
目前的 beta 版肯定少不了一些小問題:iOS 模擬器啟動 Messages 應用速度很慢,而且有時就是加載不出來插件——我經常需要從 Messages 的應用托盤里手動重啟我的插件。而且 Messages framework 非常『絮叨』:打出來的 log 簡直多到極點。當然,在 iOS 10 結束 beta 之后這些問題都會得到解決,不過目前這種狀態下你還是需要一雙火眼金睛,從大量 debug 信息里尋找跟你插件有關的內容,比如 AutoLayout constraint 沖突之類。
?
轉載于:https://www.cnblogs.com/fengmin/p/6006622.html
總結
以上是生活随笔為你收集整理的iOS 10 的一个重要更新-开发 iMessage 的第三方插件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python-快速排序算法
- 下一篇: Jmeter—控件