2018年3月25日日曜日

iOSアプリのアクセサビリティ

対応しないとと思いつつも、後回しになってしまう アクセサビリティ対応。
先日、ユーザさんにフィードバックを頂いて、「う、ごめんなさい」と思いつつ具体的に調べてみた。

iOSアプリで対応が必要なのは、VoiceOver という読み上げ機能。
ある程度デフォルトで組み込まれているので、意識せずアプリを作成していても、VoiceOverが動作する。
良い意味でも悪い意味でも、iOSアプリはアクセサビリティ対応が標準という位置づけ。
そのため、知らずに恥ずかしい状態でアプリをリリースしている可能性がある。

iOSアクセシビリティプログラミングガイド に日本語のドキュメントがある。
プログラミングガイドとなっているが、どんなラベル、ヒントなどを入れると良いのか?という記述もあり、プログラマー以外でもとても参考になると思う。

(ただドキュメントは少し古いようで、Accessibility Inspector の表示方法が違っていて、XCode のメニュー  Open Developer Tool  - Accessibility Inspector で可能。)

基本的なことは、そのガイドを理解できるので、ここでは、実際に対応を試してみて、気がついたところを書いてみる。


VoiceOver対応のコスト

VoiceOver対応にかかる工数・コストは、一概にこれぐらいというのはない。
UIデザインに依存していて、なにも対応しなくてOKな場合あるし、そもそもVoiceOverでは操作が無理という場合もある。

つまり、アプリのUIを考える前に、VoiceOverについて理解しておくとGood。


画像Onlyボタン

ラベル、ボタンはデフォルトでアクセサビリティがONになっていて、読み上げ対象になる。
特に指定が無い場合は、表示されている文字が読み上げられるのだが、画像のみのボタンの場合は「画像のアセット名」が読み上げられてしまうようだ。
これはなんとも恥ずかしい。適当に書いたコードを見られてしまったような恥ずかしさ。

画像のみのボタンは、Accessibility Label に文字を入れておくようにしよう。
StoryboardのIdentity Inspector で設定できる。



正しいTraitsを


TraitsにButtonが設定されていたら「○○ Button」、Headerが設定されていたら「○○ Heading」と読み上げてくれる。
特にボタンかどうか?は操作する側にとっては大事な情報なはず。
Traitsの設定はここをポチッと変えるだの簡単なものなので普段から意識して設定するのがいいと思う。


段組みレイアウト

読み上げの順番は、左上から右下になる。(コードでカスタマイズ可能)
あ、右から読む言語だと逆かもですが。
普通であれば、問題ないのですが、段組みレイアウトの場合は困ったことになる。


この場合、
「ワークアウト」→「平均」→「ベスト」→「100」→「8」→ ....
と読み上げてしまうことに。
この解決には、段組ごとのViewをAccessibilityをONにして、
「ワークアウト, 100件」→「平均, 8分」→「ベスト, 53分」
という文字をViewのAccessibilityのLabelに設定する必要がある。

Appleのカレンダーアプリでも、AccessibilityのLabelがカスタマイズされている様子。
「9」をタップすると、「2018年3月9日 金曜日, 1件のイベント ボタン」と読み上げする。
このようなレイアウトは、多くの場合カスタマイズが必要になりそう。


Table Cellは自動的に

Table Cellの場合、Cell の単位で自動的にAccessibleになる。便利!

上記の場合、セルのAccessibility Labelに以下の文字が自動的に設定される。

1行目: 「水泳」
2行目: 「場所, プール」
3行目: 「プールの長さ, 25m」

カスタムCellの場合もOK。
さらに、Disclosure Indicator ( 右の > ) が設定されていれば、Traitsが自動的にButtonになる。


単位の表記

「10 min」は「Ten minutes」、「1 min」は「One minute」(単数形) で読み上げてくれる。
ただ、「10, min」は、「Ten, min」になる。
カンマがあると、読み上げ時に区切って発音してくれる効果があるが、このような違いもでてくる。

値と単位とで、別のLabelにすることをよくしていた。
「単位は少し小さいフォントにしたい」 というデザインの要望があった時でも、Storyboardだけで対応しやすいから。

しかし、その対応はイマイチだったようだ。
強い要望がない限りは、値と単位は同じフォント・サイズで、1つのラベルで表示するのがよさそう。


イマイチだったUI


タップ中は、その箇所の情報が表示され、指を離すと、もとに戻るというUI。
キャンセル・戻る ようなボタンを用意する必要がないので好きだったのだが、アクセサビリティはイマイチだった。


マイナスとプラスを1つのボタンになっていて、タップした位置によって判断しているUI。
指を離さなくても、増減を変えられたり、増加量を変えたり出来ると思い作成したが、アクセサビリティはイマイチだった。






• • •

2018年3月6日火曜日

CloudKit(3): Subscription

CloudKitシリーズ第三弾、Subscription。
Subscriptionとはデータが変更されたタイミングをPushで受け取ることが出来る仕組み。


実はCloudKitは、Record Zoneの種類によって利用出来るSubscriptionが違う。
Subscriptionを使わない場合でも、ここを理解すること、Record Zoneの設計方法が少し見えてくるはず。


Subscriptionの種類

Subscriptionには以下の3種類がある。

Subscription対応する差分取得Operation
CKDatabaseSubscriptionCKFetchDatabaseChangesOperation
CKRecordZoneSubscriptionCKFetchRecordZoneChangesOperation
CKQuerySubscriptionCKFetchNotificationChangesOperation
第一回: CloudKit(1): DatabaseとRecord で説明したように、Record Zone には Default と Custom, Shared の3つの種類がある。それらのRecord ZoneごとのSubscription利用可・不可をマトリックスにすると以下になる。

Public DBPrivate DBShared DB
Default ZoneDefault ZoneCustom ZoneShared Zone
CKDatabaseSubscription××
CKRecordZoneSubscription×××
CKQuerySubscription×

前回の差分の取得のところで、さらっと 「Default Zone以外」と書いた。
じゃぁDefault Zone では差分取得ができないのか?
Default Zoneで差分を取得するには、CKQuerySubscription を利用できる....のだが....
CKFetchNotificationChangesOperation が iOS11 で deprecated になってしまった。
代わりにCKDatabaseSubscription, CKFetchDatabaseChangesOperation, CKFetchRecordZoneChangesOperation を使えとなっていますが、これらはDefault Zoneでは使えない。
詳しい調査はしていませんが、もし方法がないのであれば、Default Zoneでの差分取得はできなくなった(?)
関連: Apple Developer Forum: Notifications in iOS 11

ほかにも、CKFetchRecordChangesOperation がiOS10で deprecated になっているのをみると、Tokenを使ったRecord差分取得は、 CKFetchRecordZoneChangesOperation のみに限定...言い換えると、「Default Zone 以外で全件対象のみ可能」とする方向なのかもしれない?
(あくまで個人の感想)

以上の情報から、
Private DBでデータ量が多いものは特に、Custom Zone にしておくのが無難と思った。Customは選択肢が多いので状況に合わせていろいろ選択しやすい。
2014年のWWDC: Advanced CloudKit で、Custom ZoneはAtomic Commits ができるよ とも言っているし、なんか、ほかにも色々ありそうだ。
そして、可能な限り、Custom Zone は複数に分けておくとよさそう。
(Zoneが違うRecordを一気に更新することはできないので注意)

Default Zoneを使う場合は、一回のQueryが適切な件数になるようUIやIndexをうまく設計する というところだろうか。
さらに、Public DBの場合は、競合がなるべく起こらないような設計が大事そうだ。

ちなみに、Record Zoneの変更があると、DatabaseSubscriptionにも通知が届く。
そのため、Record Zoneの変更トリガーはCKDatabaseSubscription か CKRecordZoneSubscription のどちらかを登録していればOK。
アプリ利用中にRecord Zoneが増減するのであれば、CKDatabaseSubscription を使い、CKRecordZoneSubscriptionは使わない という選択もアリ。


Subscriptionの登録

3つのSubscription、ともに、CK***Subscriptionを作成し、CKModifySubscriptionsOperation で登録していく。

DatabaseSubscriptionの登録例
CKRecordZoneSubscriptionはZoneIDを、CKQuerySubscriptionは対象のRecord Type, NSPredicate(条件),タイミング(作成、変更、削除時)などを指定できる。


通知を受ける

通常のPush通知と同様、registerForRemoteNotifications しておいて、UIApplicationのdidReceiveRemoteNotification にて取得する。

通知設定の登録 通知の受信
Database, RecordZone のSubscriptionの受けには、前回の記事で書いた、Tokenを使った差分の取得をすれば良い。(※1)
CKQueryNotificationは、対象のRecordIDが取得できるが、通知が必ず届くとは保証されないので、そのRecordを最新にすれば全て最新の状態になるとは言えない。
だが、前述したように、通知の差分を取得するためのCKFetchNotificationChangesOperationがdeprecatedになったので、差分の取得をせずCKQueryOperationで検索かけて最新になるようするのが良さそうだ。


終わりに

SubscriptionはCloudKitを理解するための大きな山場だと思う。
当初、私はCustom Zoneを、「Defaultではない自由に作れるZone」 というイメージしか持っていなかったので混乱した。こんなに機能が違うのかと。
この記事の内容をざっくり理解してから、ドキュメントやサンプルコードをみると理解し易いのではないかと思う。


次回はShareについて書く予定!


※1
以下は、CKDatabaseSubscriptionで通知を受けた時、Record Zoneも一緒に反映する例としてもう少し詳しく書いたもの。長いので文中からは削除したがせっかくなのでペタリ。





• • •

2018年3月4日日曜日

CloudKit(2): Operation

前回、CloudKit(1): DatabaseとRecord に続き、今回はCloudKitのOperationについて。

Databaseの取得

let privateDB = CKContainer.default().privateCloudDatabase

CKContainerからPublic, Private, Shared のDatabaseが取得可能。
CKContainer.default() は、アプリデフォルトのコンテナで、その設定はCapabilities, entitlementsにある。
アプリ間で同じコンテナを参照したい場合は、カスタムコンテナの作成を行う。
CloudKit Quick Start - アプリケーション間でコンテナを共有する


Databaseへデータ登録や取得

RecordやZoneなどをDatabaseに登録・取得する方法は2種類ある。

1. Databaseにあるメソッドを使う方法

saveRecord, fetchRecordWithID など、Databaseのメソッドを使う。
クラスメソッドさんの記事 にある方法。

2. CKOperation を使う方法

CKOperation のサブクラスが各操作ごとに用意されている。



たとえば、Recordを保存する場合は、

CKOperation は、CKDatabase の add メソッドで追加ができますが、databaseプロパティにDatabaseを設定していれば自分で作成したOperationQueueでもOK。

各CKOperationには ***CompletionBlock があるので、その処理が完了した時にcallbackすることも可能。
使い方は基本同じなので、該当するCKOperationのクラスを探して実装していけばOKと思う。

複数の処理をまとめて行いたい場合は、これらのOperationを使うと良さげ というのは理解し易いと思う。
CKOperationGroup を使えば、まとめてRollbackもしてくれる(はず)。
でも、それ以外にもメリットがある。
  • キャンセルがしやすい
  • 複数のRecordをまとめて指定できる(network request quotaの最適化)
  • desiredKeys の指定がある (複数回ダウンロードしたくないAssetとかに使えるっぽい)
  • Timeoutの制御
  • Long-Lived Operations
WWDCのセッションでも推奨しているような口ぶりでしたし、特に理由がない限りはCKOperationを使う方向でよさそうに思う。

ということで、今私は、単発処理も含め全てのDBアクセスをCKOperationで記述している。
ちなみに、DatabaseやZoneの変更系と、アプリで利用しているRecordの更新系の2つのQueueを用意して実装している。


CKOperationのTimeout

OperationのqualityOfService の設定により、タイムアウトの時間が変わる。



デフォルトは、7 days。
オフラインの時は、オンラインになるまで待ってから処理してくれる。

ちなみに userInteractive は 60sec となっているが、オフライン時はすぐにNetwork Unavailableのエラーが返ってくる。


Long-Lived Operations

アプリが終了した後も引き続き処理を行ってくれる仕組み。
詳しくは こちらのApple Documentを。

AirPlaneモードにして試してみたが、fetchAllLongLivedOperationIDs のIDが0件になってしまう。謎。
LongLivedのOperationIDを保存しておいて、明示的に指定して取得するとOperationを取得できる。謎。

Operationを実行
アプリ再起動時にLongLivedOperationを取得
関連: Apple Developer Forum - How to fetch all long-lived operations on iOS

fetchLongLivedOperationで取得したOperationには、callbackが設定されていないようなので改めて設定してから実行し直す必要がある。
「Long-Lived Operations」って名前を見た第一印象は、"裏で勝手に動き続けてくれ、処理完了したらアプリが自動的に裏で起動して動く?" というイメージを持っていたのだが、そうではなく、アプリ起動時に、完了していないOperationの情報を取得できるので再実行できる という機能のよう。



クライアントでRecordをキャッシュ

クライアントでRecordをキャッシュしたり、変更差分を更新したりするコードはアプリ側で全て用意する必要がある。CloudKitに便利な機能はない。

ちなみに、CoreData + iCloudの統合機能はiOS10でdeprecatedになったようだ。

試していないが、RealmとCloudKitの連携フレームワークなどもある様子。

だが私は、現時点でフレームワークを使うのは避けた方がいいのではと思っている。
CloudKit のAPIはまだまだ変更が多いのと、データ通信量の増加・フレームワークの学習コストなどをカバーできるぐらい楽になるのか? というと疑問なので。


差分の取得

Database, Record Zoneは serverChangeToken を使って差分のみの取得が可能。
(Default Zone 以外)
Fetch完了時に取得できるtokenをアプリでキャッシュしておき、次回Fetch時に指定する。

Databaseの場合は、CKFetchDatabaseChangesOperationのインスタンス時に前回取得していたServerChangeTokenを渡す。

Record Zoneの場合は、CKFetchRecordZoneChangesOptionsに設定する。

Tokenの期限切れエラーの場合は、Tokenをクリアし全取得しなおしが必要。




次回はSubscriptionを!


• • •

2018年3月3日土曜日

CloudKit(1): DatabaseとRecord

CloudKit でどこまで何ができるのか? を理解するため、実際に試してみた。
私が知りたかったことを中心に何回かに分けて書いてみます。
今回はDatabaseとRecordについて。

Database



Database は、Public, Private, Shared の3つある。
Public は一般公開用、Private は個人用、Shared は個人同士で共有できるもの。
開発者であっても、各個人のPrivate , Shared DBの中身は見ることはできない。

Appleのアプリで利用している例を見るのがわかりやすい。
いや、逆に、これらのアプリで利用することを目的に設計されていると考えてもいいと思う。

設計次第かもしれないが、ソーシャルゲームのようなものは向いていないと思う。

CloudKitのメリットは、アカウント管理しなくても各デバイスに同期させることができたり、個人情報や健康情報などのセキュアな状態にすべき情報を自社サーバーにもたなくてもいい ということ。
なので、ツール系アプリが合っているのかもしれない。

ただ、ユーザがCloudKitを使わない設定にすることが可能なので、Private DBをメイン機能として使うのは難しい。(iCloudを設定していないアカウントでもPublicはいつでも利用可能らしい)



Price

Privateは利用ユーザのiCloudに保存され、PublicはアプリのiCloud Storageに保存される。



このFreeのStorage容量はPublicのものと思うが、Data transfer, Requests per sec はPublicだけなのか、Privateなども含むのか、いまいちはっきり分からない。

CloudKit Dashboard にて Data transfer,  Requests per sec のグラフが見えるのはPublic だけなのと、Apple Developer Forum でのAppleの人が

Private database storage goes against the user’s account quota and they pay for that. Data transfer from public to private will cost the user.  Data transfer from private to public will cost the developer. 
...といっているので、Public だけ という認識でOKな気も。多分。
だとすると、Publicを使わなければ開発者側は完全無料ということか。


Record

データはCKRecordで保存する。
最初にRecordを保存した際、そのレコードの情報が型としてiCloudに保存される。
上記の場合、"MyType" という Record Type、その属性としてtitle, price が登録される。
確認、変更は iCloud Dashboard から行える。

保存出来るデータ型は以下が利用可能。
  • NSString
  • NSNumber
  • NSData
  • NSDate
  • NSArray
  • CLLocation
  • CKAsset : 画像など
  • CKReference : Recordの親子関係を設定
Recordで保存するデータ量は1MBを超えてはいけない。

CKRecordにはsystemFieldがあり、iCloudからfetchしたRecordにはsystemFieldが設定されている。
systemFieldが含まれているRecordを登録すると更新(Update)になり、systemFieldが空だと新規追加(Insert)になる。
systemFieldを encodeSystemFields(with:) でDataにしてローカルに保存が可能。


Record Zone



DatabaseにはRecord Zoneがあり、その中にRecordが保存される。
Zone指定でSubscription(変更通知)を登録したりする。

Default Zone は名前通り最初からあるデフォルトのZone。Custom Zone は自由に作成できるZone。



注目すべきところは、Public にはDefault Zone のみ、Shared には Shared Zone のみ存在できるということ。

Shareするためには、Custom Zone である必要がある。(Default Zone は Share できない)
Aさんが 「Private DB : Custom Zone」 を BさんにShareすると、Bさんには 「Share DB : Shared Zone」 が自動的に作成される。
ShareしたAさんの 「Private DB : Custom Zone」 は変わらずPrivateのまま。Sharedになるわけではない。
※厳密には、Shareは(Root)Recordを指定する

どんな単位で Zoneを使うとよさそうか? を考える際には、Share を使うかどうか というのも重要な要素になるということ。




次回は、Operation について書いてみます。
その後は、Subscription、Share などについて書いていく予定。


• • •