2015年4月17日金曜日

仕事で使うiOSアプリ開発

はじめに

趣味の開発だと見過ごしがちな注意点が、仕事の開発だと多く存在します。
特に、ネットで検索した情報だとその場しのぎのプログラムが書かれていることが多いのも原因の一つだと思われます。

今回は、私がiOSのアプリを開発して気づいた点・ノウハウ化されるべき点をまとめてみました。ここで書かれている以外にもまだまだ書くことは多いです。少しずつ蓄積していこうと思います。

User Interactionの制御

UIに関して、Webとスマホアプリとの一番大きな違いは
・同時に複数のオブジェクトを操作できる点
です。

もう少し細かく説明しましょう。
Webであればカーソルは1つしかないので、同時に同じボタンをクリックすることはあり得ません。
しかしスマホの場合、複数の指を用いた操作を提供しているため、ある指でボタンを押しながら、他の指で別のボタンを押したりドラッグする操作を許容してしまいます。

スマホ開発において、ボタンに限らずUIオブジェクトを作成する時は、他のオブジェクトとの同時操作可能性を常に意識しなければなりません。
この可能性を無視してアプリを作ると、アプリがクラッシュ・操作不能になるような致命的なバグが大量に埋め込まれてしまいます。以下、代表的な事例とその回避策をあげてみます。

UIButton
let button = UIButton()
button.exclusiveTouch = true

exclusiveTouchをtrueにすることで、他のボタンとの同時操作をブロックします。
デフォルトではfalseになっています。もちろん、複数の指で操作するUIを組み立てるときはこの限りではありません。ただ、指一本で操作するボタンの方が多いと思われるため、UIButtonを使用する時はexclusiveTouch属性をtrueにするのを癖にするぐらいでちょうど良いと思います。

Gesture Recognizer
let view = UIView()
let gesture = UIGestureRecognizer()
view.addGestureRecognizer(gesture)
view.exclusiveTouch = true

UIButtonと同様です。UIViewにGestureを付与する時は同時操作の可能性が生まれます。
exclusiveTouch属性を忘れないようにしましょう。

Animation

Animation中はuserInteractionEnabledをfalseにしUI操作をブロックしましょう。

データの保存

アプリのデータベースの持ち方は大きく2つに分かれます。
1. サーバーへの保存(リモートのサーバー)
2. ローカルへの保存(iPhone端末内)

1. サーバへの保存

サーバーにデータを保存する際は、個人情報の取り扱いについて明記し・ユーザに許可を求める必要があります。
当たり前のように思われるかもしれませんが、iOSアプリ開発ではこのようなプライバシーポリシーに特に留意しなければなりません。その理由はiOSアプリ審査という手続きに起因します。iPhone端末にインストールされるアプリケーションがユーザに害を及ぼさないことを保証するために、Appleにより厳しい審査が施されます。
ユーザへの許可なく個人情報をサーバーに送るアプリなどは悪質なアプリとみなされApp Storeへの公開を差し押さえられるばかりか、最悪、開発アカウントを凍結される恐れがあります。

ローカルへの保存

とくにスマホゲームアプリの場合、ローカルにユーザデータを保存することがチートの可能性を生み出すことがあります。最近のゲームはソーシャル機能を持ったものが非常に多く、個人のチートがユーザ全体・サービス全体に影響を与えるため致命傷になりかねません。

オフラインの可能性

ネットワークを利用するアプリの場合は、オフラインの可能性を考えましょう。
オフライン時使用不可と割り切るのも一つの手法ですが、ネットワーク接続時にキャッシュとしてローカルにデータを保存し、オフラインコンテンツを提供することでユーザの満足度が上がります。

サーバー連携

サーバーへのデータ送信に失敗した場合の処理、再送信処理などを考慮しましょう。

画面規格対応

見積もりを出す際に、どの画面規格に対応するかを必ず確認しましょう。
iPadなどは勿論ですが、iPhone5 ~ iPhone6Plusでもアスペクト比が異なるため、
特定の端末依存で画面の開発をしていると、違う端末でスタイルが崩れることがあります。
コードベースで画面の組み立てを行っている場合は特に注意です。

確認方法

Xcode 6.0からPreview機能が追加されました。Xcode上で全ての画面規格でのレイアウトを確認することができます。ただし、コードにより動的にaddSubviewした部品は確認できません。

メモリリーク

知らない人にメモリリークとは何かを簡単に説明すると、
・オブジェクト(例:UIView、画面を追加する)を作成するとメモリを確保する必要がある
・不要になったオブジェクトは解放されなければならない

A. 解放したメモリに誤ってアクセスするとエラーによりアプリが強制終了する(クラッシュ)
B. 不要になったメモリを解放せずに放っておくと、確保しているメモリの容量が増大していき、一定容量を超えた時にOSによりアプリが強制終了される

詳しく勉強したい人は、ARCやガベージコレクションについて検索すると良いと思います。

iOS開発でよくあるメモリリークと、その防ぐ方法について説明します。

循環参照

オブジェクトAがオブジェクトBを持つ場合、オブジェクトAからオブジェクトBへの強い参照が生まれます。自分自身を指す強い参照がなくなった時に、オブジェクトは自動的に解放されます。つまり、画面遷移などでオブジェクトAが不要になり解放された時にその参照先であるオブジェクトBも自動的に参照されます。

循環参照とは、2つのオブジェクトが相互に強い参照を持った時に、どちらのオブジェクトも解放されなくなることを言います。

例1. オブジェクトBがオブジェクトAを持つ場合

// 強い参照
class ObjectB {
var objA: ObjectA?
// 弱い参照
class ObjectB {
weak var objA: ObjectA?

のように、weak参照子をつけることにより、強い参照を回避できます。(ObjectAから見た時にObjectBからの参照は無視される)

例2. クロージャの場合
複数のclass(オブジェクト)間で情報をやり取りする時にクロージャを使用することがあります。クロージャは強い参照を生み出します。

// 強い参照
closure = {
    ...
}
// 弱い参照
closure = {
    [weak self] in
    ...
}

のように修飾子をつけることで強い参照を回避できます。
修飾子はweak, unownedの2種類存在します。
使い分けは、
・selfがnilの可能性がある時はweak
・selfが常にnilではない時はunowned
となります。
unownedを使用してselfがnilだった場合はアプリがクラッシュするため、
迷う場合はweakを使っていればOKです。

クロージャの循環参照については、この記事が大変参考になります。(日本語)
http://qiita.com/susieyy/items/f57f9362b8e0a89023cb

メモリリークの発見

Add exception break pointにより、例外発生時にブレークできるためデバッグしやすくなります。

画面遷移の作り方

pushした画面をpopする、不要になった画面を解放するなどの処理を怠ると、メモリリークすることがあります。注意しましょう。

アプリ起動処理の実行時間制限

以下の条件を満たした時に、アプリが強制終了します。
・AppDelegateのアプリ起動 〜 最初のUIViewControllerの画面が表示されるまでに一定時間経過する(20秒〜30秒)
・Releaseモードでビルドした場合

つまり、アプリ起動時の初期化処理などをAppDelegateの中に書いてはいけません。
初期化処理用のUIViewControllerを作成し、
viewDidAppear -> 初期化処理 -> アプリのコンテンツ画面に遷移
とする必要があります。

厄介なことに、Macから実機に直接接続しDebugモードで実験している時には確認することができません。要注意です。

こちらの記事も参考になります。(日本語)
http://cocoadays.blogspot.jp/2011/02/ios.html

容量

アプリの容量が100MBを超える場合、Wifi接続時のみしかApp Storeからのアプリインストールができません。

アップデート

DBマイグレーション

かならずDBのバージョン管理を行い、カラムが追加・削除される可能性を考慮しましょう。

ログ

NSLog, printlnの違い

swiftの場合、NSLog, printlnの2種類のログ出力方法があります。
printlnの方が高速かつ手軽であり、ネットで検索した際もprintlnを使用しているコードが多い印象がありますが、製品開発の場合はNSLogを使用するほうをおすすめします。
理由は

  1. printlnはスレッドセーフでない
    printlnの場合、非同期処理の際に、出力が混ざることがあります。

  2. printlnはDevicesからのログに出力されない
    XcodeのコンソールではNSLog, printlnどちらともログ出力されますが、Devicesから確認できるコンソールではNSLogでしかログ出力されません。

例えば、TestFlightで配布したアプリで動作試験をしていたらクラッシュした場合、Deviceからログを確認することになりますが、printlnで出力したログは確認できません。

アーキテクチャ

32bit, 64bit

iPhone5cまでは32bitですが、iphone5s以降は64bitになっています。
バイナリをいじる処理が含まれる場合は双方で十分に試験を行いましょう。
Simulatorで試験をする場合はSimulatorのarchitectureを確認したうえで試験しましょう。

スレッド

サーバー通信やDB操作などはメインスレッドとは別のスレッドで行いましょう。
サーバー通信をメインスレッドで行ってしまうと、処理が重い時に画面がフリーズしてしまいます。

iOSバージョン別対応

iOSバージョンで使用するメソッドが異なることがあるので注意しましょう。

検証端末の取得

iOSのバージョンをダウングレードすることはできません。
最新バージョン以外のiOSに対応する場合は、検証端末をバージョンごとに調達する必要があります。開発会社であれば古いバージョンのiPhone端末をアップグレードしないまま保持しておくことが多いです。
Simulatorを使用することもできますが、カメラなどSimulatorでは再現できない機能もあり、様々な面で完全な試験のためには実機を用意する他ありません。

UILocalNotificationの上限

アラート機能のために使用するUILocalNotificationですが、実は最大64個までしか同時に登録することができません。古いアラートが無効化された時に備えて再登録処理を考慮しましょう。
iOS8でのバグにより64個を超えて登録できることもあるようですが、公式サイトには確かに64個と明記されています。
https://developer.apple.com/library/ios/documentation/iPhone/Reference/UILocalNotification_Class/

WatchKitで画面遷移する

はじめに

WatchKitで画面遷移を作成する方法を説明します。

対象

iOSアプリを少しでも作成したことのある人
言語はSwiftを使用しますが、objective-Cでもやり方に大きな違いはありません

つくるもの

・2枚の画面からなる画面遷移
・ボタンを押して、新しい画面に遷移する
・遷移後、戻るボタンにより元の画面に戻る

・遷移方法がmodal, pushの2種類あるのでそれぞれを試します
iOSアプリを開発したことがある方はご存知の通り、modal遷移は詳細表示のための一時的な画面遷移、push遷移は階層構造を持った画面を行き来するための画面遷移です。

作成手順

1. iPhoneアプリを作成する

File -> new -> ProjectからiOSアプリを作成します。
TemplateはiOS -> Application -> Single View Applicationとします。

Product NameはWatchKitTransitionとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. iPhoneアプリと連携するWatchKitアプリを作成する

File -> New -> TargetからApple Watch Targetを追加します。
TemplateはiOS -> Apple Watch -> WatchKit Appとします。
言語はSwiftです。
Include Notification Sceneにチェックが入った状態で、Finishします。
SchemeはActiveさせましょう。

3. 遷移のトリガーとなるボタンを配置する

interface.storyboard上でInterfaceController上にボタンを2つ配置します。
1つにはmodal遷移を、もう1つにはpush遷移を設定していきます。

ボタンを配置したら、ソースコード上へ参照を引っ張りましょう。
storyboardファイルとInterfaceControllerファイルを同時に開き、ctrl + ドラッグでSent Actionsからの参照を張ります。これで、ボタンを押した時の処理を記述できるようになりました。modal遷移用のボタンに対してはpresentButtonTapped、push遷移用のボタンに対してはpushButtonTappedという名前をつけます。

4. 遷移先画面を作成する

遷移した先の画面を作成しましょう。
modal遷移、push遷移それぞれで遷移先の画面を作成するので、
interface.storyboard上に新しいInterface Controllerを2つ配置します。
modal遷移用のInterface Controllerに対してはIdentifierを”PresentNewView”として設定します。
push遷移用のInterface Controllerに対してはIdentifierを”PushNewView”として設定します。

5. 画面遷移を作成する

InterfaceControllerのソースコードを編集していきます

presentButtonTappedメソッドを以下のように編集します

@IBAction func presentButtonTapped() {
    presentControllerWithName("PresentNewView", context: nil)
}

pushButtonTappedメソッドを以下のように編集します

@IBAction func pushButtonTapped() {
    pushControllerWithName("PushNewView", context: nil)
}

これで完成です。

起動

Xcodeシミュレータで実際に動かしてみましょう。

TargetをWatchKitTransition WatchKit Appに変更します。
ちなみにCommand + control + [でTargetの切り替えが出来ます。
同様にCommand + Rで起動しましょう。

iPhoneのシミュレータとApple Watchのシミュレータが同時に起動します。
もしApple Watchのシミュレータが起動しなければ、
メニューのHardware -> External Displays -> Apple Watchを選択するとシミュレータ画面が追加されます。

デモ

ボタンを押すことで画面遷移します。
遷移先の画面には自動で戻るボタンが設置されているため、自由に行き来できます。

ソースコード

こちらからどうぞ
https://github.com/takanori-matsumoto-mulodo/WatchKitTransition

2015年4月3日金曜日

iOS Extensionでデータを共有する

はじめに

これまで、Apple Watch、Action ExtensionとiOS Extension機能を実装する方法を説明してきました。今回もiOS Extensionに関する話です。

iOSアプリでデータを保存する際、基本的には1つのアプリの中だけでデータを共有することが出来ます。複数のアプリ間でデータを共有するためにはApp Groupsなどを利用しなければなりません。

Extensionはベースのアプリの拡張機能として提供されるにもかかわらず、ベースのアプリから独立した領域にストレージが割り当てられます。
ベースアプリとExtension間でデータを共有するためには複数のアプリのときと同様にApp Groupsを利用する必要があります。

今回は、NSUserDefautlsとplistファイルでデータを保存する場合を例にして、ベースのアプリとExtension間でデータを共有する方法を確認していきます。

対象

前回の記事(iOS Extensionの作り方講座(Action Extension編))を読んだ方向け
iOS開発アカウントを持っている人(App Groupsを利用するために必要です)
Xcode 6.2

作業内容

Extensionの例として、Action Extensionを利用します。
実際にExtensionの機能の実装はしません。
ベースのアプリで保存したデータが、Extensionからアクセスできることを確認します。

手順

作成手順を示します。
1. iPhoneアプリを作成する
2. Action Extensionを追加する
3. App Groupsを設定する
4. App Groupsを通じて、ベースアプリとExtension間でデータを共有できることを確認する

1. iPhoneアプリを作成する

File -> new -> ProjectからiOSアプリを作成します。
TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはSDSampleとします。
言語はSwiftを選択します。

2. Action Extensionを追加する

File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Application Extension -> Action Extensionとします。
名前はSDActionExtensionとします。
言語はSwiftです。
Action Typeはデフォルト値(Presents User Interface)のままです。
SchemeもActivateします。

3. App Groupsを設定する

Apple developer member centerのidentifiers設定にApp Groupsの項目があります。
ここからApp GroupsのIDを設定します。ID名はgroupから始まる必要があります。
名前を”group.com.hoge”にしたとして以下説明します。

SDSampleのTargetを選択してCapabilitiesタブからApp GroupsをONにします。
このとき、iOSの開発アカウントを尋ねられます。App Groupsを設定した開発アカウントを選択します。もしここでつまづくようであれば、Xcode -> Preferences -> Accounts項目に正しくアカウントが設定されているかどうかを確認しましょう。

作成したApp GroupsのIDが一覧に表示される筈なのでチェックを入れます。

次にSDActionExtensionのTargetについても同様にApp GroupsをONにしgroupのIDのチェックを入れます。

4. App Groupsを通じて、ベースアプリとExtension間でデータを共有できることを確認する
NSUserDefaultsのデータを共有する

SDSample/ViewController.swiftのviewDidLoad内に以下を追記します。

let groupName = "group.com.hoge"
let commonUserDefaults = NSUserDefaults(suiteName: groupName)!
// save
commonUserDefaults.setValue(1, forKey: "Sample")
// load
println(commonUserDefaults.valueForKey("Sample"))
println(commonUserDefaults.valueForKey("Extension"))

次に、SDActionExtension/ActionViewController.swiftのviewDidLoad内に以下を追記します。
既にviewDidLoad内にテンプレートコードが書かれていますが、Extensionとしての機能は今回利用しないので削除してしまって問題ありません。

let groupName = "group.com.hoge"
let commonUserDefaults = NSUserDefaults(suiteName: groupName)!
// save
commonUserDefaults.setValue(1, forKey: "Extension")
// load
println(commonUserDefaults.valueForKey("Sample"))
println(commonUserDefaults.valueForKey("Extension"))
}

SDSampleとSDActionExtensionは異なるストレージを保持するので、NSUserDefaults.standardUserDefaults()を使用した際には、互いに保存したデータにアクセスすることは出来ませんが、App Groupsを経由することで、データの共有が可能になります。

まずはSDSampleをTargetに設定しビルド&実行します。
次にSDActionExtensionをTargetに設定しビルド&実行します。
SDSampleを実行した際に”Sample”という名前のKeyに値を保存し、
SDActionExtensionの実行時に”Sample”のKeyが取得できることを確認しています。
ビルドする順番を逆にした場合も同様です。

plistファイルのデータを共有する

SDSample/ViewController.swiftのviewDidLoad内に以下を追記します。
(NSUserDefaultsに関するコードは消してしまいましょう)

let groupName = "group.com.hoge"
let fileManager = NSFileManager.defaultManager()
let samplePlistData = ["id": 1, "name": "Sample"] as NSDictionary

let groupPath = fileManager.containerURLForSecurityApplicationGroupIdentifier(groupName)!.path!
let samplePlistPath = groupPath.stringByAppendingPathComponent("sample.plist")
let extensionPlistPath = groupPath.stringByAppendingPathComponent("extension.plist")
// save
let saveData = NSKeyedArchiver.archivedDataWithRootObject(samplePlistData)
fileManager.createFileAtPath(samplePlistPath, contents: saveData, attributes: nil)

// load
if fileManager.fileExistsAtPath(samplePlistPath) {
    println("Exist: \(samplePlistPath)")
    let loadData = NSData(contentsOfFile: samplePlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}
if fileManager.fileExistsAtPath(extensionPlistPath) {
    println("Exist: \(extensionPlistPath)")
    let loadData = NSData(contentsOfFile: extensionPlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}

次に、SDActionExtension/ActionViewController.swiftのviewDidLoad内に以下を追記します。

let groupName = "group.com.hoge"
let fileManager = NSFileManager.defaultManager()
let extensionPlistData = ["id": 1, "name": "Extension"] as NSDictionary

let groupPath = fileManager.containerURLForSecurityApplicationGroupIdentifier(groupName)!.path!
let samplePlistPath = groupPath.stringByAppendingPathComponent("sample.plist")
let extensionPlistPath = groupPath.stringByAppendingPathComponent("extension.plist")
// save
let saveData = NSKeyedArchiver.archivedDataWithRootObject(extensionPlistData)
fileManager.createFileAtPath(extensionPlistPath, contents: saveData, attributes: nil)

// load
if fileManager.fileExistsAtPath(samplePlistPath) {
    println("Exist: \(samplePlistPath)")
    let loadData = NSData(contentsOfFile: samplePlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}
if fileManager.fileExistsAtPath(extensionPlistPath) {
    println("Exist: \(extensionPlistPath)")
    let loadData = NSData(contentsOfFile: extensionPlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}

NSUserDefaultsの場合と同様にビルドして確認してみましょう。
お互いのplistファイルにアクセスできる筈です。

2015年4月2日木曜日

iOS Extensionの作り方講座(Action Extension編)

はじめに

Extensionというのは、1つのアプリの範囲を超えた機能を実装するための仕組みです。iOS8以降から利用できるようになった機能の一つです。

例えば、Extensionを使って以下のような機能が実装できるようになります。
・Safariで見つけた画像を、独自の画像コレクションアプリに取り込む
・カスタムキーボードアプリを作成して、他のアプリの入力UIとして使用する
・iPhone通知センターのTodayビューに独自のウィジェットを追加する

以上のように、Extensionは他のアプリと連携する機能の実装や、iPhoneの標準機能の拡張を実現するための仕組みです。Extensionを利用することでアプリ同士の密な連携が取れるようになるので、iPhoneそのもののカスタマイズ性が上がりアプリユーザにとっても嬉しい仕組みです。
Extensionを開発するためには、Extensionのベースとなるアプリが必ず必要になります。ベースとなるiOSアプリをApp StoreからインストールしたときにExtensionも一緒にインストールされます。

前回の記事を読んだ方は気づいたかもしれませんが、
以前紹介したApple Watch向けアプリも実はExtensionの一種です。

これからExtensionを利用した開発が増えていくことが予想されます。
今回は、Extensonに慣れるための簡単なサンプルアプリの作成チュートリアルを行います。

対象

iOSアプリを少しでも作成したことのある人
Extensionを全く利用したことがない人
開発言語はSwift
Xcode 6.2

作成するもの

今回は以下のExtensionを作成します。
・Safariブラウザを閲覧しているときに、閲覧しているページと同じページを独自アプリで開くExtension

使用するExtension

今回はAction Extensionを利用します。
Extensionは特定の拡張機能を実装するのに特化した仕組みなため、あらかじめ利用できるExtensionの種類が定められています。

Action Extensionというのはホストアプリ(今回の場合はSafari)のコンテンツを操作・表示するためのExtensionです。Extensionの中でも特に汎用性の高い機能です。

今回のExtensionではSafariブラウザで開いているページのURLをExtension経由でゲストアプリ(つまり独自アプリ)に教えてあげることで、同じページを開かせます。

作成手順

概要

作成手順を示します。
1. iPhoneアプリを作成する(Extensionのベースとなるアプリです。)
2. Action Extensionを追加する
3. Web Viewを追加する
4. SafariブラウザからURLを受け取りWeb Viewに表示する

1. iPhoneアプリを作成する

iOSアプリを作成する通常の方法と何も変わりません。

File -> new -> ProjectからiOSアプリを作成します。

TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはAETSampleとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. Action Extensionを追加する

File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Application Extension -> Action Extensionとします。
名前はAETExtensionとします。
言語はSwiftです。
Action Typeはデフォルト値(Presents User Interface)のままです。

Activate Scheme?に対してはActivateと答えましょう。
ExtensionをビルドするためのSchemeが追加されます。

3. WebViewを追加する

AETExtension/MainInterface.storyboardを開きます。
初期状態でUIImageViewのUIが用意されていますが、今回は使用しないためUIImageViewは削除します。
“Done”ボタンを含むNavigation Barは再利用できるのでそのままでOKです。

UIImageViewと同じ場所に置き換えるようにして、UIWebViewを追加しましょう。

さらに、ActionViewControllerファイルにIBOutlet参照を張ります。

4. SafariブラウザからURLを受け取りWeb Viewに表示する

次はActionViewControllerを編集していきます

viewDidLoad内に既にコードが書かれていると思います。
このコードはstoryboardにデフォルトで配置されていたUIImageViewに対するコードです。ホストアプリ(Safariブラウザ)から値を受け取る処理は共通部分が多いので、既存のコードを編集する形で実装していきます。

viewDidLoadを以下のように編集してください。

// ActionViewController
    override func viewDidLoad() {
        super.viewDidLoad()

        // Get the item[s] we're handling from the extension context.

        // For example, look for an image and place it into an image view.
        // Replace this with something appropriate for the type[s] your extension supports.
        for item: AnyObject in self.extensionContext!.inputItems {
            let inputItem = item as NSExtensionItem
            for provider: AnyObject in inputItem.attachments! {
                let itemProvider = provider as NSItemProvider
                if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as NSString) {
                    itemProvider.loadItemForTypeIdentifier(kUTTypeURL as NSString, options: nil, completionHandler: {
                        [weak self] (data, error) in
                        if error != nil {
                            println(error)
                        }
                        if let url = data as NSURL? {
                            println("url: \(url)")
                            self?.webView.loadRequest(NSURLRequest(URL: url))
                        }
                    })
                }
            }
        }
    }
ポイント1
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as NSString) {
    itemProvider.loadItemForTypeIdentifier(kUTTypeURL as NSString, options: nil, completionHandler: {...})
}

Action Extensionによりホストアプリから受け取る値は様々です。
更にホストアプリから受け取る値は複数存在することもあり、どのような値が来るかは分からないため、受け取った値の種類を判別する必要があります。
既存のコードではUIImageを受け取るためにkUTTypeImageが指定されていました。
今回はURLを受け取るのでkUTTypeURLを指定します。

ポイント2
self?.webView.loadRequest(NSURLRequest(URL: url))

URLタイプの値を受け取ったときにwebViewからURLリクエストを行いwebページを読み込みます。

実装はこれで終了です。

デモ

Xcodeシミュレータで実際に動かしてみましょう。

TargetをAETExtensionに変更します。
Command + control + [でTargetの切り替えが出来ます。
Command + Rで起動しましょう。

起動する際にExtentionを起動するホストアプリを選択する必要があります。
Safariを選択しましょう。

Safariの画面下部のツールバーのアクティビティボタン(真ん中)を選択します。
表示されるバーの中にAETExtensionというボタンがある筈です。
無い場合は下のバーのその他を選択し、AETExtension項目にチェックを入れましょう。

AETExtensionを選択すると、AETSampleアプリが起動し、Safariと同じWebページが表示されます。

補足

できあがったアプリはこちらからダウンロードできます。
https://github.com/takanori-matsumoto-mulodo/AETSample

2015年3月24日火曜日

Apple Watch向けWatchKitアプリの作り方講座

はじめに

Xcode6.2よりWatchKitのAPIが一般向けに公開され、シミュレータ上で誰でもApple Watch向けアプリが開発できるようになりました。

開発するための情報が未だ少ないですが、既にアプリ開発に取りかかっている開発者も多いようです。
今回は、WatchKitアプリを全く開発したことが無いiOS開発者向けに、WatchKitアプリ作成のチュートリアルを行います。

対象

iOSアプリを少しでも作成したことのある人
言語はSwiftを使用しますが、objective-Cでもやり方に大きな違いはありません

つくるもの

GitHub上に、WatchKitのサンプルアプリが公開されています。
今回はこちらのアプリを参考に、WatchKitアプリの作成方法を順番に説明していきたいと思います。
https://github.com/kostiakoval/WatchKit-Apps

リポジトリ内には、シチュエーションに合わせた機能ごとにサンプルが用意されています。今回作成していくサンプルアプリは3. AppsCommunicationです。

ペアリングされたApple WatchとiPhone間で、AppleWatch上で押したボタンに応じてiPhoneにアクションが送れる、というアプリです。

作成手順

概要

作成手順を示します。
1. iPhoneアプリを作成する
2. iPhoneアプリと連携するWatchKitアプリをTargetとして追加する
3. [iPhone側実装] Apple Watchでボタンを押したときの処理を設定する
4. [iPhone側実装] Apple Watchからのアクションを受け取る処理を設定する
5. [WatchKit側実装] ボタンのUIを設定する
6. [WatchKit側実装] ボタンを押したときのiPhoneへのアクションの送信処理を設定する

1. iPhoneアプリを作成する

WatchKitアプリはApple Watch上で単独で起動するアプリではありません。
iOSアプリの拡張機能として提供されます。開発方法も、Xcode上でiOSアプリにTargetを追加する形で実装していきます。
作成したWatchKitアプリは、App StoreからiOSアプリをインストールした際に、ペアリングしたApple Watchにインストールされます。

File -> new -> ProjectからiOSアプリを作成します。

TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはMyAppsCommunicationとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. iPhoneアプリと連携するWatchKitアプリを作成する

WatchKitのTargetを追加することで、既存のiOSアプリをApple Watch向けに拡張することが出来ます。
File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Apple Watch -> WatchKit Appとします。
言語はSwiftです。
Include Notification Sceneにチェックが入った状態で、Finishします。
Include Notification Sceneについては、Apple Watch Programming Guideに書かれている通り、Notification機能を使用するつもりが無くてもチェックを入れることが推奨されています。

引用:

If you plan to implement a glance or custom notification interface, select the appropriate checkboxes.
For notification interfaces, it is recommended that you select the Include Notification Scene checkbox, even if you do not plan on implementing that interface right away. Selecting that checkbox adds an additional file to your project for debugging your notification interfaces. If you do not select that option, later you must create the file manually.

Activate “MyAppsCommunication WatchKit App” scheme?のダイアログに対しては、Activateします。

3. [iPhone側] アクションを受け取ったときのViewの更新を設定する

Apple Watch上でボタンを押したときに、iPhone画面上のラベルの文字が変わる、という機能を想定します。

Templateとして作成されているViewControllerに、
・ラベル
・ラベルの文字を変えるメソッド
をそれぞれ実装しましょう。

Main.storyboardとViewController.swiftを2つ開きます。
ファイル名をダブルクリックするなどして、同時に2つのウィンドウが見えるようにしましょう。

Main.storyboard上でViewControllerの画面中央にLabelを配置します。
右下のウィンドウからLabelをドラッグすることで配置できます。Label領域は広めに取りましょう。

storbyboard上で配置したLabelを選択し、ViewControllerのソースコードにOutletの参照を作成します。参照の名前はlabelとします。

ViewController上にlabelの文字を変更するメソッドを追加します。

// ViewController
    @IBOutlet weak var label: UILabel!
    func changeLabel(text: String) {
        label.text = text
    }
4. [iPhone側] Apple Watchからのアクションレシーバーを設定する

AppDelegate.swiftを編集します。
以下の処理を追加します。

// AppDelegate.swift
    func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {

        if let info = userInfo as? [String: String] {
            let VC = window?.rootViewController as ViewController
            VC.changeLabel(info["label"]!)
            reply(["response" : "success"])
        } else {
            reply(["response" : "failed"])
        }

    }

WatchKitから受け取った処理を設定するためのデリゲートメソッドはhandleWatchKitExtensionRequestで設定できます。

ポイント1
if let info = userInfo as? [String: String] {
} else {
}

この書き方はswiftでよく使う定型句です。
optional型の値をlet定数に入れてif分で評価することで、nil値の評価を行っています。userInfoの値がnilで無かったときはif ~ elseでinfo変数として使用できます。

ポイント2

userInfo、つまりWatchKitから受け取った値がnilでなかった時は、ViewControllerのchangeLabelメソッドを呼び出し、画面上の文字を変えます。画面上に表示する文字は、WatchKitから受け取ります。

ポイント3

replyはWatchKitからのcallback関数です。
reply関数が呼ばれなかった場合、WatchKit側でwarningが出るため、呼ぶようにしましょう。
引数を空データにしたければ、

reply([NSObject : AnyObject]())

とすればOKです。
今回は、処理の成功/失敗を引数に与えています。

5. [WatchKit側] ボタンのUIを設定する

ソースコードの構成を見てみましょう。
WatchKitのTargetを追加することで、以下の2つのグループが追加されました。
・MyAppsCommunication WatchKit Extension
・MyAppsCommunication WatchKit App

WatchKitのアプリの構造は大きく2つに分かれます。

WatchKit Extension
WatchKitの動的な処理を行います。iOSアプリと同じようにコントローラを実装します。
余談ですが、一見Apple Watch上で全ての処理を行っているように思われがちですが、WatchKit ExtensionはiPhone側にインストールされます。
WatchKit App
Apple WatchにインストールされるUIです。ボタンの配置・画面遷移などを構築していきます。
UIの構築はstoryboard上で行っていきます。
iOSアプリではinterface builder(GUI)でもaddSubviewメソッド(CUI)でもどちらでもUIの構築を行えました。
しかしWatchKitではaddSubviewのようなCUIベースでのUI構築は行えません(2015年3月現在)。

さて、UIを構築していきます。
まずInterface.storyboardを開きます。
ボタンを3つ追加します。やりかたはiOSアプリのときと同じです。

Mainという矢印のついたビューに対して、Buttonを3つ追加します。

6. [WatchKit側] ボタンを押したときのiPhoneへの送信処理を設定する

3つのボタンについて、Interface.storyboardからInterfaceController.swiftに対しアクションの参照を追加します。
アクションの名前はbutton1Tapped, button2Tapped, button3Tappedとしました。

さらに、WKInterfaceController.openParentApplicationメソッドでiOSアプリにアクションの通知を送ります。

Main.storyboardとViewController.swiftのときと同じ具合です。

// InterfaceController
    @IBAction func butotn1Tapped() {
        buttonTapped("button1")
    }

    @IBAction func button2Tapped() {
        buttonTapped("button2")
    }

    @IBAction func button3Tapped() {
        buttonTapped("button3")
    }

    func buttonTapped(label: String) {
        var userInfo = ["label": label]
        WKInterfaceController.openParentApplication(userInfo, reply: {
            (data, error) in
            if let error = error {
                println(error)
            }
            if let data = data {
                println(data)
            }
        })
    }

実装はこれで終了です。

起動

準備

Xcodeシミュレータで実際に動かしてみましょう。

TargetをMyAppsCommunication WatchKit Appに変更します。
ちなみにCommand + control + [でTargetの切り替えが出来ます。
同様にCommand + Rで起動しましょう。

iPhoneのシミュレータとApple Watchのシミュレータが同時に起動します。
もしApple Watchのシミュレータが起動しなければ、
メニューのHardware -> External Displays -> Apple Watchを選択するとシミュレータ画面が追加されます。

起動直後は
iPhoneは、ホーム画面にMyAppsCommunicationアプリがインストールされた状態
Apple Watchはボタンが3つ表示された状態
になるはずです。

iPhone側で、MyAppsCommunicationを選択してアプリを起動します。

デモ

Apple Watch上のボタンを好きに押してみましょう。
iPhone側でのLabelの文字が切り替わる筈です。

補足:デバッグについて

TargetをiOSアプリにすると、iOSアプリのみのデバッグ、
TargetをWatch Appにすると、WatchKitアプリのみのデバッグになります。

iOS, WatchKitの2つを同時にデバッグする方法が以下に紹介されています。
https://mkswap.net/m/blog/How+to+debug+an+iOS+app+while+the+WatchKit+app+is+currently+running+in+the+simulator
やり方は次の通りです。

  1. TargetをWatch Appにして、インストールする
  2. iPhone側でアプリを起動する
  3. アクティブウィンドウをXcodeに戻す
  4. Debug -> Attach to ProcessからMyAppsCommunicationアプリのプロセスを選択する

これで同時にデバッグすることができます。どちらのソースコードについても、ブレークポイントを貼ることができるようになります。

できあがったアプリはこちら
https://github.com/takanori-matsumoto-mulodo/MyAppsCommunication

2015年3月16日月曜日

知らない人向け DI(Dependency Injection)の説明

DIについて調べたので簡単に説明します

DIとは

  • クラス同士の依存関係を設定する仕組みを作ること
具体的に説明すると
  • クラス内のメンバ変数(インスタンス)をセットするための注入関数を用意して、注入関数をクラス外部から呼び出すこと
です。

DIを利用するメリット

  1. 柔軟性が生まれる
  2. テストしやすくなる(テストコードが書きやすい)

実例を見せましょう。
Javaのソースコードで具体例を示します。説明のために細かいところは省きます。

DI が無いコード

public class Factory {
  public void run() {
    System.out.println("Factory run");
  }
}

public class Manager {
  private Factory factory;

  public Manager() {
    factory = new Factory();
  }
  public void runFactory() {
    factory.run();
  }
}

public class DITest {
  public static void main(String[] args) {
    Manager manager = new Manager();
    manager.runFactory();
  }
}
ManagerクラスがFactoryクラスのインスタンス変数をメンバとして保持しているので、ManagerとFactoryは依存関係にあることが分かります。
Factoryクラスのインスタンス変数はManagerクラス内部で生成されています。

DI があるコード

public interface Factory {
  void run();
}

public class PencilFactory implements Factory {
  public void run() {
    System.out.println("Pencil Factory run");
  }
}

public class Manager {
  private Factory factory;
  public void setFactory(Factory f) {
    factory = f;
  }
  public Manager() {
  }
  public void runFactory() {
    factory.run();
  }
}

public class DITest {
  public static void main(String[] args) {
    Manager manager = new Manager();
    manager.setFactory(new PencilFactory());
    manager.runFactory();
  }
}
インターフェースが利用され、変数をセットするためのsetFactory関数が実装されています。
Factoryのインスタンス変数は、Managerクラスの外部(DITest)からsetFactory関数を通じて設定されています。
これが依存性の注入(DI、Dependency Injection)です。


ここで、DIのメリットについてもう一度確認してみましょう。

DIを利用するメリット

  1. 柔軟性が生まれる
  2. テストしやすくなる(テストコードが書きやすい)

1については、インターフェースを利用することにより抽象的な実装が出来るようになりました。
クラス内変数を定義する際にインターフェースで定義することで、複数のクラスを受け入れる余裕が生まれました。

2については、インスタンスのモックアップを外部から設定することが出来るようになりました。
様々な条件に対するテストケースを作成するときに、インスタンス、つまり条件を外部から設定する機構が生まれました。

DIはデザインパターンにおける重要な概念です。
エンジニア初心者の方は、ぜひ覚えておきましょう。

2015年3月5日木曜日

Gitのcommitを美しくするtips

  • git commit --amend
    直前のcommitに混ぜて一緒にcommitすることができます
    このときに書いたコミットメッセージで直前のコミットメッセージを上書きすることが出来ます
    以下のようなシチュエーションで利用できます
    • コミットメッセージを打ち間違えたから書き換えたい
    • コミットした後、ソースコードにデバッグコードが混じっていることに気づいた。直前のコミットにデバッグコードの削除分も含めたい
  • git rebase -i head~{n}
    git commit --amendの拡張版です。直前のcommitだけでなく、過去全てのcommit logに対して
    • コミットメッセージを書き換える
    • 複数のコミットを1つのコミットに結合する
    ことができます。
    nにはheadから遡るcommitの数を指定します。
    例えば、
    git rebase -i head~4
    のようになります。 以下のようなシチュエーションで利用できます
    • ローカルでの作業のために細かくcommitはしたいけど、リモートブランチにpushするcommitはまとめたい、といったシチュエーションで利用できます。
  • git add -p
    unstageなソースコードをcommitするためにaddコマンドを実行しますが、git addではファイル単位でしか追加することが出来ません。
    git add -pを使用することでファイル内のセクションごとに追加するかどうかを分けることが出来ます。
    以下のようなシチュエーションで利用できます
    • 試行錯誤して大量の修正を施した後まとめてaddした。同じファイル内に複数の機能の修正が混ざっているので、別々にaddしてcommitしたい。

gitを使い倒して、上手なソースコード管理を目指しましょう!