本文介紹了 iOS 10 的一個重要更新:Messages 應用支持第三方插件了。做者用一個小遊戲做爲例子,說明了插件開發從建工程開始,到繪製界面、收發消息的全過程。ios
《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者須要瞭解的 iOS 10 新特性,每週更新。本系列翻譯(文集地址)已取得官方受權。倉薯翻譯,歡迎指正:)git
Shinobicontrols 爲 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤爲是圖表方面的控件。 官網 : shinobicontrols.com twitter : @shinobicontrolsgithub
蘋果官方的 Messages 在 iOS 10 推出了很是重大的更新,可能主要是想從其餘 IM 巨頭手裏搶點市場份額回來,包括 Facebook Messenger, Wechat 和 Snapchat。web
一個重要的新功能是,用戶能夠直接在 Messages 裏使用第三方開發者開發的擴展插件了。這個功能是在 iOS 8 引入的 Extension 技術基礎上實現的,能夠參考咱們往年系列裏 Sam Davies 寫的文章。Messages 插件的一大好處是,它是能夠獨立於 app 存在的,不用跟父 app 打包在一塊兒。今年晚些時候 iOS 10 將會發佈一個小巧的 Messages App Store,裏面會有一堆插件供用戶挑選。瀏覽器
爲了演示一下這個使人興奮的插件功能,咱們看一個簡單的例子吧,這個插件可讓兩個用戶玩一個簡化版的流行遊戲 Battleships。爲了讓約束佈局方面簡單一些,咱們只考慮豎屏的狀況。爲方便你們下載這個 demo,我把它放到Github上了。session
遊戲規則是這樣的:app
玩家 A 發起遊戲,在棋盤上佈置兩個『戰艦』,而後隱藏起來ide
另外一個玩家 B 要猜想戰艦的位置函數
若是猜中了兩艘隱藏戰艦的位置,玩家 B 就贏了;可是若是猜錯 3 次,玩家 B 就輸了。佈局
用 Xcode 新建一個插件工程很是簡單。只需點擊 File -> New Project,而後在窗口中選擇 iMessage Application。
給工程起個名字,而後語言選擇 Swift(本系列均使用 Swift 語言示例),這就完事了。由於有一個自動生成的MessagesExtension
target ,而後默認的Info.plist
裏帶有必需的配置(插件界面的 storyboard 以及插件的類型等),因此只要運行工程,Messages 就能自動識別出咱們的插件了。
若是在模擬器裏運行MessagesExtension
這個 target,它會讓你選擇在哪一個 app 裏運行這個插件。咱們選擇Messages
。
Messages 打開的時候,應該能在輸入框下方看到咱們的插件。若是看不到,可能須要點擊 "Applications" icon,而後再點 4 個橢圓的 icon,從裏面選擇咱們的插件。
如今裏面啥也沒有,不過咱們將很快改變這一點。眼下最迫切的是要把咱們插件的 display name 改改:如今顯示的是 "MessagesExtension"(其實是 "MessagesEx..." 後面被截掉了)。下面咱們點擊 target,而後把Display Name
輸入框裏的名字改一改。
咱們須要展現的是 3x3 的棋盤。有不少實現方法,我用的是 UICollectionView。在本教程裏,畫界面這一塊並不重要,所以實現細節再也不詳述了。
爲了記錄一局遊戲自己以及遊戲的狀態,咱們定義如下兩個結構體:
struct GameConstants { /// 一共須要佈置的戰艦數 static let totalShipCount = 2 /// 容許玩家 B 失敗的次數 static let incorrectAttemptsAllowed = 3 } struct GameModel { /// 戰艦的位置 let shipLocations: [Int] /// 遊戲是否已經結束 var isComplete: Bool }
MessagesViewController
是咱們插件的入口點。它是MSMessagesAppViewController
的子類,至關因而 Messages 插件的 root View Controller。自動生成的模板裏面包含了一些供咱們重寫的方法,好比插件啓動狀態下用戶收到消息的回調函數。待會咱們就要用到其中的一部分方法。
第一點要注意的是,咱們的插件啓動以後有兩種可能的 presentation style:
compact
expanded
compact
是用戶從應用托盤裏打開插件的模式,插件顯示在鍵盤區域裏。expanded
則多給了一些喘息的空間,插件佔據大部分的屏幕。
爲了讓代碼整潔一些,咱們會用不一樣的 view controller 來分別實現兩種模式,而且把這些 view Controller 都加爲MessagesViewController
的子 view controller。
本文不會花太長篇幅來描述這些 controller 的實現細節,只會重點關注在收發信息的過程,遊戲狀態和數據是怎麼變化的。關於具體實現,請自行閱讀 Github 上的源碼。
咱們的插件剛啓動的時候處於compact
狀態。這點空間並不夠展現遊戲的棋盤,在 iPhone 上尤爲不夠。咱們能夠簡單粗暴地當即切換成expanded
狀態,可是蘋果官方警告不要這麼作,畢竟仍是應該把控制權交給用戶。
因而,咱們來顯示一個簡單的歡迎界面,裏面有一個 label 和一個 button。按下 button 的時候,再切換到遊戲的主界面,用戶就能夠開始放置『戰艦』了。
這個 view controller 是玩家 A 佈置戰艦的界面。
咱們實現gameBoard
的onCellSelection
方法來控制 cell 的樣式:上面有戰艦的 cell 顯示爲綠色,空白的顯示爲藍色。
shipsLeftToPosition
返回 0 時,結束按鈕會變得可點。這個按鈕的點擊事件是一個叫completedShipLocationSelection:
的IBAction
方法,它會新建一個遊戲 model,而後使用 UIImage 的 extension 來建立一張遊戲棋盤的截圖(咱們會先reset()
棋盤,因此截圖的時候戰艦的位置是隱藏的——如今可不是揭曉謎底的時候!)。這張截圖在待會發消息的時候會用到。
當玩家 B 點擊對話中的消息時,咱們但願他能看到一個略微不一樣的 view controller —— 一個能讓他尋找隱藏戰艦的界面。
咱們仍是實現棋盤的onCellSelection
方法。這一次咱們把選擇的 cell 位置與玩家 A 佈置的位置匹配的(『擊中戰艦』)標爲綠色,若是沒有擊中就標爲紅色。
遊戲結束後,不論是由於 3 條命用完了,仍是由於兩條戰艦都找出來了,咱們都會相應地記錄在數據模型中,而後調起遊戲結束的回調。
回到咱們的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。
private func configureChildViewController(for presentationStyle: MSMessagesAppPresentationStyle, with conversation: MSConversation) { // 清空全部以前的子 view controller for child in childViewControllers { child.willMove(toParentViewController: nil) child.view.removeFromSuperview() child.removeFromParentViewController() } // 好,如今建一個新的吧 let childViewController: UIViewController switch presentationStyle { case .compact: childViewController = createGameStartViewController() case .expanded: if let message = conversation.selectedMessage, let url = message.url { // 若是 conversation.selectedMessage 不爲空,說明玩家 A 已經把戰艦佈置好了,當前是玩家 B // 因此咱們須要顯示能讓玩家 B 選擇位置來擊沉戰艦的界面 let model = GameModel(from: url) childViewController = createShipDestroyViewController(with: conversation, model: model) } else { // 不然,咱們就須要佈置戰艦了 childViewController = createShipLocationViewController(with: conversation) } } // 添加子 view controller addChildViewController(childViewController) childViewController.view.frame = view.bounds childViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(childViewController.view) childViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true childViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true childViewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true childViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true childViewController.didMove(toParentViewController: self) }
上面這個方法決定了咱們該向當前的用戶展現哪一個子 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) }
以前在 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 衝突之類。
若是你還想繼續往下探索,我推薦你看這場 WWDC 視頻,也能夠看看蘋果官方的例子工程:裏面能夠學到不少有趣的小 tips,例如如何優雅地解析 URL。
若是有任何問題和評論,咱們都很歡迎你的反饋。能夠發我 tweet @sam_burnstone,也能夠關注 @shinobicontrols 關注最新動態以及 iOS 10 Day by Day 系列的更新。感謝閱讀!
原文地址:iOS 10 Day by Day :: Day 1 :: Messages
原做者:Sam Burnstone @sam_burnstone
ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉薯