你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS 10 Day by Day 1:開發 Message 的第三方插件

iOS 10 Day by Day 1:開發 Message 的第三方插件

編輯:IOS開發基礎

本文介紹了 iOS 10 的一個重要更新:Messages 應用支持第三方插件了。作者用一個小游戲作為例子,說明了插件開發從建工程開始,到繪制界面、收發消息的全過程。

《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者需要了解的 iOS 10 新特性,每周更新。本系列翻譯(文集地址)已取得官方授權。倉薯翻譯,歡迎指正:)

Shinobicontrols 為 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤其是圖表方面的控件。 官網 : shinobicontrols.com  twitter : @shinobicontrols

蘋果官方的 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上了。

227290-0cf6c6067a0a0b01.gif

demo 動圖

游戲規則是這樣的:

  • 玩家 A 發起游戲,在棋盤上布置兩個『戰艦』,然後隱藏起來

  • 另一個玩家 B 要猜測戰艦的位置

  • 如果猜中了兩艘隱藏戰艦的位置,玩家 B 就贏了;但是如果猜錯 3 次,玩家 B 就輸了。

建工程

用 Xcode 新建一個插件工程非常簡單。只需點擊 File -> New Project,然後在窗口中選擇 iMessage Application。

227290-0b262803fe0723e5.png

建工程

給工程起個名字,然後語言選擇 Swift(本系列均使用 Swift 語言示例),這就完事了。因為有一個自動生成的MessagesExtensiontarget ,然後默認的Info.plist裡帶有必需的配置(插件界面的 storyboard 以及插件的類型等),所以只要運行工程,Messages 就能自動識別出我們的插件了。

改 Display Name

如果在模擬器裡運行MessagesExtension這個 target,它會讓你選擇在哪個 app 裡運行這個插件。我們選擇Messages

227290-730e4b432d54109f.png

在 Messages 裡運行

Messages 打開的時候,應該能在輸入框下方看到我們的插件。如果看不到,可能需要點擊 "Applications" icon,然後再點 4 個橢圓的 icon,從裡面選擇我們的插件。

現在裡面啥也沒有,不過我們將很快改變這一點。眼下最迫切的是要把我們插件的 display name 改改:現在顯示的是 "MessagesExtension"(實際上是 "MessagesEx..." 後面被截掉了)。下面我們點擊 target,然後把Display Name輸入框裡的名字改一改。

227290-a66bf7d9b826cdb8.png

改 display name

棋盤

我們需要展示的是 3x3 的棋盤。有很多實現方法,我用的是 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 布置戰艦的界面。

我們實現gameBoardonCellSelection方法來控制 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。

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)
}

227290-fb737c9c56c6c7ca.gif

切換到 expanded 模式

創建『可以更新』的消息

之前在 Messages 裡面,任何新的內容——不管是新的短信還是表情——都會以一條新消息的形式出現在對話的底部,跟之前的所有消息都不相干。

然而,這一點可能帶來很多麻煩:比如,一個下國際象棋的游戲插件會造成每走一步棋都要發一條新消息。而我們理想中的情況應該是更新後的消息能代替之前的消息。

謝天謝地,蘋果也想到了這一點,給我們提供了一個類MSSession——這個類沒有屬性也沒有方法,只是用來更新消息的。

我們發一條消息的時候,就用這個 session 來告訴 Messages,要覆蓋此前 session 相同的信息。前一條信息會被從聊天記錄中移除,然後新的信息插入到底部。

使用聯系人姓名

最近幾年,蘋果一直說要把保護用戶隱私當做頭等大事。對 Messages framework 來說確實如此:你並不能得到用戶的身份,只能得到一個每個設備不同的UUID。也就是說,你不能在消息裡加入發消息的用戶的身份 ID,然後指望收消息的用戶能通過這個 ID 識別出發消息的是誰。

另外,你只能訪問到用戶點擊的那條消息的內容,不能訪問到對話中任何其他消息的內容(而且點擊的這條消息還必須是從你的插件發出來的)。

MSConversation 這個類有兩個屬性localParticipantIdentifierremoteParticipantIdentfiers,可以用來顯示對話雙方的名字。要加一個前綴$

let player = "$\(conversation.localParticipantIdentifier)"

把它放在消息裡發出去,Messages 會解析這個 UUID,然後顯示出對應的聯系人姓名。

227290-9a983b45bf5c6519.png

顯示聯系人姓名

收發應用數據

游戲狀態的數據是以 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

文集地址:iOS 10 day by day 倉薯翻譯

本文地址:http://www.jianshu.com/p/8728d405b310

譯者:戴倉薯



文章轉自 戴倉薯的簡書
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved