2015.01.26

[iOS] App Extensionsを探る(Today Extension導入編)


こんにちは。ゲーム事業の技術支援をしているF.Sです。
もう若くはないのですが、今更ながらiOSデベロッパーになるべく勉強を始めました。
2014年11月にデベロッパー公開されたWatchKitで(まだできることが少なそうですが)なにか作ろうと企んでます。


WatchKitはiOS8から導入されたApp Extensionsの仕組みと関連が深く、この仕組みを知ってるとAppleWatchのコンセプトが理解しやすいということで、勉強がてらApp Extensionsのエントリーを公開します。

[参考] App Extensionsプログラミングガイド
https://developer.apple.com/jp/devcenter/ios/library/documentation/ExtensibilityPG.pdf

[環境]

  • MacOS X v10.9.5(会社都合でまだYosemiteにできない〜)
  • Xcode 6.1.1 / Swift1.1
  • iOS 8.0.2(手持ちの実機)
 

App Extensionsとは

iOS8(およびOS X v10.10)以降で利用できるアプリケーション拡張機能で、
  • アプリケーションを超えて独自の機能とコンテンツを拡張できる
  • ユーザーが他のアプリケーションやシステムを利用している場合でも、それらを利用することができる
  • あるタスクに特化したものとして開発される
と解説されています。

とは言ってもぱっとイメージしにくいと思うので、Extensionsポイントから提供できる機能のイメージを見てみましょう。

 

Extentionsポイント(iOSで利用できるもの)

  • Today・・・純正のカレンダーやリマインダー、株価のように、「今日(Today)」にコンテンツを追加するもの
  • Share・・・Safariや写真のシェア機能でも利用されているソーシャル投稿の機能を追加するもの(下イメージ)
  • Action・・・呼び出し元のアプリのコンテンツを操作・表示する機能を提供するもの
  • Photo Editing・・・「写真(Photos)」アプリケーションで写真や動画を編集する機能を追加するもの
  • Document Provider・・・ドキュメントのインポート/エクスポートやリモート保存などの機能を提供するもの
  • Custom Keyboard・・・「Atok for iOS」のようなサードパーティキーボード
SafariのShare機能拡張イメージ
ios_app_extensions_00

では、これらのExtensionがどのように動作するか見てみましょう。

 

Extensionsの動作

iOSのApp Extensionsは単独では配布できずアプリケーションに含めて配布するため、Extensionの入れ物となる「収容アプリケーション(Containing app)」が必要になります。

Extensionは、その機能を利用するアプリケーション(「ホストアプリケーション(Host app)」)から呼び出しを受けて(システムを介して)起動し、タスクを実行します。ホストアプリケーションから受け取ったオブジェクトを処理して返却することができます。バックグラウンドタスクでHTTP通信することもできるようです。
ios_app_extensions_01
ios_app_extensions_02
出典:Apple Inc. 「App Extensions プログラミングガイド」

収容アプリケーションとExtensionはセキュリティドメイン(sandbox)が独立しており、別々のアプリケーション実行空間を持つので、直接やりとりすることはできません。URLスキームやApp GroupによるNSUserDefautsを使った共有データ領域を使って連携を行います。
ios_app_extensions_03
出典:Apple Inc. 「App Extensions プログラミングガイド」


なお、Extensionは下記のような制約事項があるようです。
  • sharedApplicationオブジェクトへアクセスできません
  • NS_EXTENSION_UNAVAILABLEなど、使用不可マクロが付いたAPI使用できません(HealthKit、EventKit UIなど)
  • カメラ、マイクへのアクセスができません
  • 長時間のバックグラウンドタスクが実行できません
  • AirDropを使ってデータを受信することができません(UIActivityViewControllerでAirDropを使ったデータ送信は可能)

手始めに、Today Extensionを使ってなにか作ってみましょう!!

 

Today Extensionの実装例

おみくじウィジェットを作ってみましょう。
「今日(Today)」に、運勢を表示するだけのウィジェットを作ってみます。

■収容アプリケーションを作成
Xcodeを起動し、File > New > Project… > iOS > Application で「Single View Application」を作成します。
Product Nameを「LuckyDraw」としました。

■Extensionsを作成
本題のExtensionを作成します。
File > New > Target… > iOS > Application Extension で「Today Extension」を選択します。
ios_app_extensions_04
Product NameはTodayとしました。

このようなテンプレートファイルが作成されます。
ios_app_extensions_05

Today ExtensionのコードテンプレートとなるViewControllerはこのようになっています。
ios_app_extensions_10

NCWidgetProvidingプロトコルを実装していますね。
デリゲートメソッドの widgetPerformUpdateWithCompletionHandler はコンテンツを更新するタイミングで呼び出されるようです。

storyboardはこんな感じです。「今日(Today)」に表示されるビューですね。
ios_app_extensions_06

■デバッグ実行
schemeをExtensionに変更すると、Extension機能のデバッグ実行ができます。
ExtensionのTarget作成時に、下のようにschemeを変更するか聞かれます。
ios_app_extensions_07

試しにそのまま実行してみましょう。

なお、Extensionは、その機能を利用するホストアプリケーションから呼び出される形で起動するため、実行するホストアプリケーションを選択するシートが出てきます。
ios_app_extensions_08

Todayを選択して実行すると、下記のようにExtensionが実行されます。
ios_app_extensions_09


■機能実装
それでは、おみくじの実装を入れていきましょう。
新規ファイル LuckyDraw.swift を作成し、おみくじロジックを入れます。

LuckyDraw.swift
import Foundation

struct LuckyDraw {

    static let drawing: Dictionary<String, Int> = [
        "【大吉】ヾ(*ΦωΦ)ノ ヒャッホゥ" :10,
        "【中吉】ヽ(・∀・)ノ ワチョーイ♪" : 30,
        "【小吉】ヽ( ´¬`)ノ" : 40,
        "【吉】(´▽`)" : 40,
        "【末吉】(-_-)" : 40,
        "【凶】Σ( ̄ロ ̄lll)" : 30,
        "【大凶】ドヨォォ─(lll-ω-)─ォォン" : 10
    ]

    static func draw() -> String {

        var result = ""

        // 確率重みの合計を算出
        var rateSum:UInt32 = 0
        for (unuse, rate) in drawing {
            rateSum += rate
        }

        // 確率の母数の範囲から数値を抽選
        let lotteryNo = arc4random_uniform(rateSum)

        // 抽選された数値に応じた結果をセット
        var rateSection: UInt32 = 0;
        for (luck, rate) in drawing {
            rateSection += rate
            if (lotteryNo <= rateSection) {
                result = luck
                break
            }
        }

        return result
    }
}
このおみくじロジックをExtensionsのViewControllerから呼び出します。

コンテンツをアップデートするために、Maininterface.storyboard にあるラベル(デフォルトでHelloWorldとなっているやつ)の参照をViewColtrollerに追加します。widgetが開かれたときにラベルにおみくじの結果が表示されるように、ExtensionのViewControlerに処理を追加してみましょう。

TodayViewController.swift(抜粋)
    @IBOutlet weak var resultLabel: UILabel!

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        resultLabel.text = ""
    }
    
    func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {

        resultLabel.text = LuckyDraw.draw()

        completionHandler(NCUpdateResult.NewData)
    }

実際に動かしてみると、「今日(Today)」を開いてから内容が更新されるまでの1秒間程度、以前のスナップショットが表示されます。
おみくじの場合、前回の結果が見えているとちょっと紛らわしいので、Viewの非表示時に一旦おみくじ結果をクリアするようにしています。

※viewWillDisappearは「今日(Today)」にてスクロールアウトすると呼び出されますが、再度スクロールインしたときにはwidgetPerformUpdateWithCompletionHandlerは呼び出されず、おみくじ結果がクリアされたままになってしまいます。viewWillAppearにてコンテンツ更新することで回避はできます。

また、そのままでは表示結果が格好悪いので、適当にアイコンをつけてinfo.plistのBundle display nameを「今の運勢」に編集しました。


■実行イメージ
ios_app_extensions_11

ウィジェットを上からズルッと引き出す感じが、いかにもおみくじ引くを感じでいいですね。
うちの子供(6歳)は「大吉がでるまで引く!」と、何度も閉じたり開いたりハマってる感じです。多分、今だけですが。。

 

収容アプリは何をするか?

今のままでは真っ白な画面が表示されるだけですが、Extensionの利用解説やユーザー設定などを入れたりするのが一般的ですね。
現状、表示のたびにおみくじを引き直してますが、基本は1日1回のみとし、収容アプリ側のミニゲームをクリアしたら引き直すことができるとかしたいと思います。

ちなみに、収容アプリがユーザーになにかしらの機能を提供しないと、アプリ審査に通らない とガイドに書いてあります!!


次世代システム研究室では、スマートフォンアプリエンジニアを募集しています。スマホアプリ開発経験者の方、スマホアプリ開発に興味がある方、また次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いいたします。

皆様からのご応募、お待ちしてます。

それでは、また。