[iOS 10] メッセージ拡張 MSSessionで「まるばつゲーム」を作ってみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

iOS 10から、拡張機能(Extension)を作成することで、メッセージアプリで、ステッカー(Lineのスタンプみたいな感じ)、メディアファイル、及び、対話型のメッセージなどを送信することができるようになりました。

新しいメッセージは、セッション情報を持っており、相互にデータをやり取りすることで簡単なゲームなどを作成することも可能になりました。

今回は、このセッション機能を利用した、まるばつゲームを作成してみました。

相互に、「まる」、「ばつ」を付けて、メッセージを送り合うと、ゲームが勝ち負け判定を行います。

006 007

なお、メッセージ拡張の基本的な作成方法などは下記をご参照下さい。
[iOS 10] メッセージ拡張の作成方法
[iOS 10] メッセージ拡張 MSStickerBrowserViewControllerを使用したビューの作成
[iOS 10] メッセージ拡張 MSMessageTemplateLayoutによるテキストや画像(映像)の配置
[iOS 10] メッセージ拡張 ノンプログラミングによるスタンプ作成
[iOS 10] メッセージ拡張 ノンプログラミングによるアニメーションスタンプ作成

2 プロジェクト作成と画面設計

(1) プロジェクト作成

File > New > Project > iMessage Application で新規にプロジェクトを作成します。プロジェクト名は「MaruBatuApp」としました。

001

(2) フルスクリーンとコンパクト

コンパクトモードで表示するビューは、ゲームを開始するボタンだけを置き、フルスクリーンモードで表示するビューには、中央にボタンを9個スタックで並べました。

002

CompactモードとFullScreenモードは、それぞれ専用に作った2つのビューの表示・非表示を切り替えることで実装しました。

// MARK: - Private

private func presentViewController(style: MSMessagesAppPresentationStyle) {
    compactView.isHidden = style == .expanded
    fullScreenView.isHidden = style == .compact
}

上記のメソッドは、表示モードが切り替わるタイミングで呼ばれています。

// MARK: - Conversation Handling

override func willBecomeActive(with conversation: MSConversation) {
    presentViewController(style: presentationStyle)
}

override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
    presentViewController(style: presentationStyle)
}

3 データモデル

ゲームのデータは、「まる」・「ばつ」・「空白」の3種類のステータスを持つ、9個の配列で表現されています。

enum Kind {
    case Maru
    case Batu
    case None
    case Draw
}

class MaruBatu {

    var kinds: [Kind] = [.None, .None, .None, .None, .None, .None, .None, .None, .None]

・・・省略・・・

勝敗は、縦横斜めに揃っているかどうかで判定され、空白がなくなった時点で勝敗なし(Draw)となります。

func winner() -> Kind {
    if kinds[0] == kinds[1] && kinds[0] == kinds[2] { // 0,1,2
        return kinds[0]
    }
    if kinds[0] == kinds[4] && kinds[0] == kinds[8] { // 0,4,8
        return kinds[0]
    }
    if kinds[0] == kinds[3] && kinds[0] == kinds[6] { // 0,4,6
        return kinds[0]
    }
    if kinds[1] == kinds[4] && kinds[1] == kinds[7] { // 1,4,7
        return kinds[1]
    }
    if kinds[2] == kinds[4] && kinds[2] == kinds[6] { // 2,4,6
        return kinds[2]
    }
    if kinds[2] == kinds[5] && kinds[2] == kinds[8] { // 2,5,8
        return kinds[2]
    }
    if kinds[3] == kinds[4] && kinds[3] == kinds[5] { // 3,4,5
        return kinds[3]
    }
    if kinds[6] == kinds[7] && kinds[6] == kinds[8] { // 6,7,8
        return kinds[6]
    }
    if count() == 9 {
        return .Draw
    }
    return .None
}

4 データの受け渡し

メッセージのやり取りの中でデータのやり取りには、MSMessageurlプロパティを使用しています。

オブジェクトからは、対戦中の現在の9個のステータスを文字列化して取り出し、MSMessageにセットする際にURL型に変化します。

// 現在の対戦状況を文字列で返す
func data() -> String {
    var result = ""
    for var kind in kinds {
        switch kind {
        case .None:
            result = result + "0"
        case .Maru:
            result = result + "1"
        case .Batu:
            result = result + "2"
        default: break
        }
    }
    return result
}

メッセージ受信者は、選択したメッセージのurlプロパティが有効であれば、その文字列(対戦中の状態)を元に、オブジェクトを初期化し、ゲームを継続します。

if let message = conversation.selectedMessage {
    if let url = message.url {
        // 選択中のMSMessageが有効な場合、urlからデータを取得してMaruBatuを初期化する
        maruBatu = MaruBatu(data: url.absoluteString)
    }
}

5 MSMessageTemplateLayout

メッセージは、MSMessageTemplateLayoutで表示しています。 MSMessageTemplateLayoutには、画像を設定するプロパティが有りますが、そこに、現在の対戦状況の画像を貼っています。

003

対戦状況を画像にしているコードは以下のとおりです。

// 現在の対戦状況を画像で返す
func renderSticker() -> UIImage? {
    let baseSize = 100 // ひとマスのサイズは、縦横100
    if let backImage = UIImage(named: "back.png") {
        if let maruImage = UIImage(named: "maru.png") {
            if let batuImage = UIImage(named: "batu.png") {
                UIGraphicsBeginImageContext(CGSize(width:baseSize * 3, height:baseSize * 3))
                if winner() != .None {
                    if let contextRef = UIGraphicsGetCurrentContext() {
                        contextRef.setFillColor(winnerColor().cgColor)
                        contextRef.fill(CGRect(x: 0, y: 0, width: baseSize * 3, height: baseSize * 3));
                    }
                }
                backImage.draw(in: CGRect(x: 0, y: 0, width: baseSize * 3, height: baseSize * 3))
                for i in 0 ..< 9 {
                    let x:Int = i % 3
                    let y:Int = i / 3
                    if kinds[i] == .Maru {
                        maruImage.draw(in: CGRect(x: x * baseSize, y: y * baseSize, width: baseSize, height: baseSize))
                    } else if kinds[i] == .Batu {
                        batuImage.draw(in: CGRect(x: x * baseSize, y: y * baseSize, width: baseSize, height: baseSize))
                    }
                }
                let newImage = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()
                return newImage
            }
        }
    }
    return UIImage()
}

また、その画像をMSMessageに設定しているコードは以下のとおりです。

var message = MSMessage(session: MSSession()) // MSMessageを生成
let layout = MSMessageTemplateLayout() // MSMessageTemplateLayoutを生成
layout.image = maruBatu.renderSticker() // imageプロパティに対戦中の画像を設定
message.layout = layout // MSMessageのlayoutプロパティに、上記layoutを設定
self.activeConversation?.insert(message, completionHandler: nil)

6 最後に

完成度は、かなり低いですが、一応、メッセージのセッション機能でゲーム(らしきもの)が、作成出来ることが確認できました。 すいません、説明不足な部分は、コードをご参照下さい。


github [GitHub] https://github.com/furuya02/MaruBatuApp

7 参考リンク


iMessage + Apps
API Reference Messages Framework
iMessage Apps and Stickers, Part 1
iMessage Apps and Stickers, Part 2
SampleCode Ice Cream Builder: A simple Messages app extension