【Apple Watchでできること】まとめ

WordPressブログをiPhoneネイティブアプリ化(AppleWatch対応版)のチュートリアル 第6回「AppleWatch対応」

AWJ tutorial

WordPressを使っているサイトのネイティブアプリ化(AppleWatch対応版)のチュートリアル第6回目。ようやくAppleWatch対応です!
(前回はこちら→第5回「お気に入りの記事を保存する(後編)」

作るのはこんなアプリ

アプリ版AppleWatchJournalをそのまま作成します。
詳しい動作は実際のアプリを参照ください!

AWJ

無料
(2015.08.30時点)
posted with ポチレバ

※そして、もしよかったらAppleWatchJournalも定期的にチェックしていただけますと超幸いです。よろしくです。

アプリに「WatchKit」を追加する

WatchKitのアーキテクチャーはどうなっているの?

アプリをAppleWatch対応させるにはWatchKitの追加が必要です。
Appleの開発者向けサイトによると、AppleWatch向けアプリのアーキテクチャは以下のようになっています。
App communication 2x
[ cf. Apple Watchプログラミングガイド: WatchKitアプリケーションのアーキテクチャ ]

画面(storyboard)を担当するWatchKitアプリケーションはAppleWatch側に、実際の処理を担当するWatchKitエクステンションはiPhone側に存在し、WatchKitを通して情報のやり取りを行います。
今回作成するアプリは、そこまで複雑な処理や情報のやり取りをするわけではないので、このざっくりとしたアーキテクチャーを理解していれば、問題なく開発できると思います。

WatchkitアプリケーションとWatchkitエクステンションを追加する

メニューからFile > New > Target…を選択。
iOS > Apple Watchから「watchkit App」を選択します。
20150830010720

Product Name等の設定はデフォルトのままでOK。
今回作るアプリは「グランス」も「通知」も使わないので、「Include Notification Scene」「Include Glance Scene」のチェックは外しておきます。
20150830010901

プロジェクト名に合わせた「WatchKit Extension」と「watchKit App」が作られていることを確認できると思います。
20150830011150

ここで一度アプリを実行してみましょう。
実行するときは、ターゲットが「WatchKit App」になっていることを確認してください。
20150830012010

実行すると、ターゲットのiPhoneとペアリングしているAppleWatchに自動的にアプリがインストールされます。
IMG 7829

インストールが終わると自動的にアプリが起動…するはずですが、うまくいかないときも多いので、アプリアイコンをタップしてみてください。
ただ、インストール直後にタップするとアプリがクラッシュしてしまうことも多いです。
そういうときは、適当にインストール済みのアプリを起動してみると、思い出したように実行したアプリが起動します。
この辺りの挙動は、AppleWatchのバッテリー節約のために、アプリの自発的な起動を制限しているために起こっているんじゃないかと思います。
ちょっと気持ち悪い動きではありますが、ここはAppleWatchの挙動に慣れていただくしかないようです…。

という感じで、アプリを起動してみると、まずはiPhoneとの通信が行われ、真っ黒画面の何もないアプリが表示されました!

IMG 7830

これでWatchKitの追加は完了。
あとは、画面と処理をそれぞれ実装していくだけです。

画面を作る

UIを構成する。

iPhoneアプリと同様に、storyboardを使ってアプリの画面を作っていきます。
AppleWatch用のstoryboardは、WatchKit Appフォルダの中にInterface.storyboardとして用意されています。

20150830013046

WatchKitの部品はiPhoneのUIKitではなく、WatchKit独自のUIが用意されています。
UIKitほどの拡張性がないためUIの自由度はかなり低めですが、その分シンプルに作ることができると思います。

ObjectLibraryから画面内に「Label」をドラッグ&ドロップ。
20150830013509

同じように、Labelをあと2つ、Image、Button、さらにLabelをもう一つ追加します。
20150830013912

それぞれ、Attributes Inspectorで以下のように設定していきます。
■Label①
Label
– Text: AWJ Reader (起動→記事データを読み込む間に表示するので、サイトタイトルやアプリ名を入れておくと良いと思います。)
– Text Color: Default
– Font: Headline
– Lines: 3
Size
– Width: Relative to Container
– Height: Size to Fit Content

■Label②
Label
– Text: (空白)
– Text Color: #E0275F
– Font: Caption1
– Lines: 1
Size
– Width: Relative to Container
– Height: Size to Fit Content

■Label③
Label
– Text: (空白)
– Text Color: #C4C4B8
– Font: Caption2
– Lines: 1
Size
– Width: Relative to Container
– Height: Size to Fit Content

■Image
Image
– Image: (空白)※あとで設定します。
– Mode: Aspect Fill
Size
– Width: Relative to Container
– Height: Fixed(80)

■Button
Button
– Content: Text
– Title: あとで読む
– Background: No Image
– Color: #E0275F
Size
– Width: Relative to Container
– Height: Size to Fit Content

■Label④
Label
– Text: (空白)
– Text Color: Default
– Font: Body
– Lines: 200
Size
– Width: Size to Fit Content
– Height: Size to Fit Content

設定が完了すると、以下のように表示されていると思います。
20150830022055

ダミーのサムネイル画像を設定する

作成するアプリでは、実際の記事データが読み込まれるまでタイトル+ダミー画像+ボタンのみが表示されるという仕様になっています。
タイトルとボタンの表示はできているので、最後にダミー画像を設定します。

まずは42mmのAppleWatchの横幅312pxに合わせて、312px四方のダミー画像を作成します。
DummyThumbnail 2x

AppleWatchの解像度は312pxですが、論理幅は半分の156px。
ということで、同じ画像の156px四方版も作成しておきます。

Xcodeに戻り、WatchKit AppフォルダのImages.xcassetsを選択。
左下の「+」ボタンをクリックし、「New ImageSet」を選択します。
20150830024512

先ほど作成したダミー画像を、156pxの画像を「1x」へ、312pxの画像を「2x」へそれぞれドラッグ&ドロップ。
「3x」は空欄のままでOKです。
20150830024739

再びstoryboardに戻り、Imageの項目Imageに、先ほど作成したImage Setを指定します。
20150830025029

これで、画面の作成は完了です。
ふたたびアプリを実行してみると、タイトル+ダミー画像+ボタンの画面が表示されることを確認できると思います。
IMG 7832

UI部品をOutlet接続する。

4つのLabel、Image, Buttonをすべて、WatchKit Extensionの「InterfaceController.swift」にOutlet接続します。
接続の方法はiPhoneの場合と同じで、Controlキーを押しながらUI部品をソース上にドラッグ&ドロップするだけです。

class InterfaceController: WKInterfaceController {
  
  @IBOutlet weak var wkTitle: WKInterfaceLabel!
  @IBOutlet weak var wkCategory: WKInterfaceLabel!
  @IBOutlet weak var wkDate: WKInterfaceLabel!
  @IBOutlet weak var wkTumbnail: WKInterfaceImage!
  @IBOutlet weak var favButton: WKInterfaceButton!
  @IBOutlet weak var wkBody: WKInterfaceLabel!

これで画面の作成は完了です。

記事を表示する

記事を読み込んで表示する処理を記述していきます。
基本的にiPhone版と同じような処理をしているので、サクサクっと実装していきましょう!

まずは、Alamofireをインポートします。

import WatchKit
import Foundation
import Alamofire // Alamofireをインポート

つづいて、iPhone版と同じように記事のデータを格納する変数を用意します。

class InterfaceController: WKInterfaceController {

  @IBOutlet weak var wkTitle: WKInterfaceLabel!
  @IBOutlet weak var wkCategory: WKInterfaceLabel!
  @IBOutlet weak var wkDate: WKInterfaceLabel!
  @IBOutlet weak var wkTumbnail: WKInterfaceImage!
  @IBOutlet weak var favButton: WKInterfaceButton!
  @IBOutlet weak var wkBody: WKInterfaceLabel!
  
  var entryDataArray = NSArray() // entriesデータを格納する配列
  var entryUrl = "" // 記事のURLを格納するString変数
  var entryTitle = "" // 記事のタイトルを格納するString変数
  var entryNo: Int = 0 // 表示する記事の番号を指定するInt変数

一点iPhone版と違うのは、AppleWatch版では記事一覧がなく、最新の記事を表示する設計になっている点。
記事を切り替えるメニューも用意するので、Feedの何番目の記事を表示するのかを決める変数「entryNo」を用意しています。
アプリ起動時は最新記事を読み込むため、0で初期化しています。

つづいて、Alamofireの読み込み処理を実装します。
ここもほとんどiPhone版と同じ処理です。

override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  
  self.favButton.setEnabled(false) // 読み込み直後はボタンを非アクティブ化
  
  // iPhone版と同じようにFeedを読み込み。Feedの読み込み件数は8件。
  let now = NSDate()
  let formatter = NSDateFormatter()
  formatter.dateFormat = "yyyy/MM/ddHHmm"
  let addeddate = formatter.stringFromDate(now)
  let requestUrl = "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://applewatchjournal.net/feed/?" + addeddate + "&num=8"
  
  Alamofire.request(.GET, requestUrl).responseJSON{
    (request, response, json, error) in
    
    let jsonDic = json as! NSDictionary
    let responseData = jsonDic["responseData"] as! NSDictionary
    let feed = responseData["feed"] as! NSDictionary
    self.entryDataArray = feed["entries"] as! NSArray
    
    // Set Content
    self.setContent(self.entryNo)
  }
}

iPhone版に比べてiPhone→AppleWatchの転送時間が結構遅いので、少しでも転送量を軽くできるように、iPhone版より少なめの8件のみにしました。
記事の表示は、setContent()というメソッドで切り出しています。

というわけで、次はそのsetContent()を実装しましょう。
これもiPhone版を作っていれば特に迷うことはないと思います。

func setContent(index: Int){
  //Index番目のデータをentryDataArrayから取得
  var entryDic = (self.entryDataArray.objectAtIndex(index)) as! NSDictionary
  
  // 記事タイトル・カテゴリー名・更新日を、それぞれ対応するLabelに設定
  self.entryTitle = (entryDic["title"] as? String)!
  self.wkTitle.setText(self.entryTitle)
  let category = entryDic["categories"] as! NSArray
  self.wkCategory.setText(category[0] as? String)
  self.wkDate.setText(entryDic["publishedDate"] as? String)
  self.entryUrl = entryDic["link"] as! String
  
  // 本文中のHTMLタグを削除→本文用のLabelに設定
  let pattern = "<.*?>"
  var content = (entryDic["content"] as? String)!.stringByReplacingOccurrencesOfString(pattern, withString: "", options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
  self.wkBody.setText(content)
  
  // Thumbnail画像を取得し設定
  var mediaGroups:NSArray = (entryDic["mediaGroups"] as? NSArray)!
  var mediacontents:NSDictionary = (mediaGroups[0] as? NSDictionary)!
  mediaGroups = (mediacontents["contents"] as? NSArray)!
  mediacontents = (mediaGroups[0] as? NSDictionary)!
  
  let urlString = mediacontents["url"] as! String
  let targetURL = NSURL(string: urlString)
  
  NSURLSession.sharedSession().dataTaskWithURL(targetURL!, completionHandler: { (data, response, error:NSError?) -> Void in
  
    if error != nil{
    }else if data != nil && data.length > 0{
      self.wkThumbnail.setImageData(data)
    }
  }).resume()
  
  self.favButton.setEnabled(true) // 画面要素表示後に、お気に位入りボタンをアクティブ化
}

iPhone版と違うのは、AppleWatch版には本文の表示を行う点。(このために、第1回目のwordpress設定でフィードの表示を「全文を表示」にしていました。)
本文はentryDataArrayに“content”として格納されていますが、そのまま表示するとHTMLタグが含まれています。
文字列を置換するために、NSStringクラスのstringByReplacingOccurrencesOfStringメソッドを使って、HTMLタグ(<.*?>)を空文字と置換しています。

ここでまたアプリを実行してみると、最新の記事が表示されることを確認できると思います。
IMG 7833

「あとで読む」ボタンの実装

次に、「あとで読む」ボタンの機能を実装していきます。
まずは、storyboardで「あとで読む」ボタンをInterfaceControllerにAction接続します。
action名は「save」としました。
20150830181047

つづいて、ボタンの処理の実装です。
AppleWatchから直接MagicalRecordを使っての保存がうまくできないようなので、iPhone側にデータを渡し、iPhone側で保存処理を行ってもらうようにしましょう。
(AppleWatchはAppDelegateを持たないのでうまく実装できませんが、このあたりはおそらく僕の経験値不足が問題かもしれません…。もっとスマートに処理できるよ!という方、ぜひご意見お待ちしております!!)

AppleWatchからiPhoneへデータを渡して処理をしてもらうためには、WKInterfaceControllerのopenParentApplication()メソッドを利用します。
渡すデータは、CoreDataのエンティティ作成に必要な記事のタイトルと記事のURLになります。

@IBAction func save() {
  WKInterfaceController.openParentApplication(["entryUrl":"\(self.entryUrl)", "entryTitle":"\(self.entryTitle)"],
  reply: {replyInfo, error in
    self.favButton.setEnabled(false)
  })
}

データ転送後、iPhone側からの反応を受けて、「お気に入り」ボタンを非アクティブにしています。
連打して同じ記事が何件も登録されないための処理です。

続いて、データを受け取るiPhone側の処理を記述します。
処理は「AppDelegate.swift」に記述します。

処理の内容は、iPhone版の「あとで読む」ボタンと同じで、AppleWatchから受け取った「タイトル」「URL」に現在時間を加えて、MagicalRecordのMR_createEntity()、MR_saveToPersistentStoreAndWait()メソッドを使い保存を行います。

func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!){
  
  var entryUrl: String = userInfo["entryUrl"] as! String!
  var entryTitle: String = userInfo["entryTitle"] as! String!
  
  let now = NSDate()
  let formatter = NSDateFormatter()
  formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
  let adddate = formatter.stringFromDate(now)
  
  let newItem: Entity = Entity.MR_createEntity() as Entity
  newItem.title = entryTitle
  newItem.url = entryUrl.componentsSeparatedByString("?")[0]
  newItem.adddate = adddate
  newItem.managedObjectContext!.MR_saveToPersistentStoreAndWait()
}

アプリを実行してみると、AppleWatchの「あとで読む」ボタンを押すと何か処理が行われたあとにボタンが非アクティブ化すること、
IMG 7838

さらに、iPhoneのお気に入り記事一覧画面に、記事が追加されることを確認できると思います。
IMG 7840
※一覧画面をリフレッシュする処理は入れてないので、一覧画面を表示していた場合は一度メイン画面に戻って開きなおしてください。

フォースタッチのメニューを実装

Menu・Menu Itemを画面に追加する

AppleWatchから追加された“画面を強く押す”という操作(フォースタッチ)。
AppleWatchでのメニューは、このフォースタッチを使ったメニューが推奨されているので、今回のアプリでも利用したいと思います。

ObjectLibraryから画面内に「Menu」をドラッグ&ドロップ。
「Menu Item」を一つ含む「Menu」が作られるので、そこにObjectLibraryから「Menu Item」をドラッグ&ドロップ。
20150830175858

Attributes Inspectorで、それぞれ以下のように設定
Menu Item①
Title: 一つ前の記事
Image: Repeat

Menu Item②
Title: 最新の記事
Image: Resume

アプリを実行してみると、フォースタッチでメニューが開くことを確認できると思います。
IMG 7837

あとは、それぞれの処理を実装するだけです。

「一つ前の記事」の処理を実装する

storyboardで、「一つ前の記事」のMenu ItemをInterfaceControllerにAction接続。
Action名は「next」としました。
20150830180700

処理の内容は非常にシンプルで、entryNoを+1して、setContent()メソッドを呼ぶだけです。
entryDataArraryの最後の記事まで行った場合は、再び最新記事へループするような処理も忘れずに入れておきましょう。

@IBAction func next() {
  self.entryNo += 1
  if (self.entryNo >= self.entryDataArray.count) {
    entryNo = 0
  }
  self.setContent(self.entryNo)
}

「最新の記事」の処理を実装する

「最新の記事」の処理はさらに簡単です。
storyboardでInterfaceControllerにAction接続(Anction名はreset)。
entryNoを0にして、setContent()メソッドを呼ぶだけです。

@IBAction func reset() {
  self.entryNo = 0
  self.setContent(self.entryNo)
}

実行してみると、メニュー操作で記事を切り替えてることが確認できると思います。
IMG 7841

以上で、すべての処理が完了しました!!
なんだかんだで全6回の連載記事になってしまいましたが、ここまでおつきあいいただきありがとうございました!

第1回でも書いたように、私はAppleWatch購入をきっかけにiOS開発を勉強し始めた超初心者です。
間違っている記述や適切じゃない処理を行っている場所もあると思います。
FacebookTwitterでご指摘いただけますと幸いです!

というわけで、WordPressブログをiPhoneネイティブアプリ化(AppleWatch対応版)のチュートリアル、これにて完了です!!

連載記事

第1回「HTTP通信でブログのデータを取得する」
第2回「テーブルビューに情報を表示する」
第3回「WebViewでページを表示する」
第4回「お気に入りの記事を保存する(前編)」
第5回「お気に入りの記事を保存する(後編)」
第6回「AppleWatch対応」

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です