2018年11月7日水曜日

サマータイム関連のバグ ふたたび

サマータイムに伴うバグを作っていたのを発見!
うう、何回目だろう。気にしているつもりなんだけどなぁー。
参考になる人がいるかもなので、共有しますっ

1. ○分後 を取得する


たとえば、30分後を取得したいとき。

        let date = dateGMT(year: 2018, month: 11, day: 4, hour: 8, minute: 30)
        print("日付:", date)
        let cal = Calendar(identifier: .gregorian)
        var comps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: date)
        comps.minute! += 30
        print("30分後", cal.date(from: comps)!)

とかくと、

        日付: 2018-11-04 08:30:00 +0000
        30分後 2018-11-04 09:00:00 +0000

...となりますが、iPhoneのTime Zoneをサンフランシスコにしていると、

        日付: 2018-11-04 08:30:00 +0000
        30分後 2018-11-04 10:00:00 +0000

になります。
2018-11-04 2:00:00 PDT = 2018-11-04 09:00:00 GMT がサマータイム終了タイミングで、1時間進むためです。
○分後 という絶対秒数を追加するには、素直にdate.addingTimeInterval(1800) と書けっつーことですな。

※ PDT = Pacific Daylight Time = 太平洋 サマータイム時間 UTC-7
    PST = Pacific Standard Time = 太平洋 標準時間 UTC-8


2. カレンダーイベントの登録

カレンダー(EventKit)で、イベント(EKEvent)を登録する際、開始日時と終了日時でサマータイムの有無が変わる場合、注意が必要です。

たとえば、以下の日付のイベントを登録した場合。

開始日時: 2018-11-04 1:30:00 PDT = 2018-11-04 08:30:00 +0000
終了日時: 2018-11-04 1:00:00 PST = 2018-11-04 09:00:00 +0000

        let event = EKEvent(eventStore: eventStore)
        event.calendar = eventCalendar
        event.title = "Test"
        event.startDate = dateGMT(year: 2018, month: 11, day: 4, hour: 8, minute: 30)
        event.endDate = dateGMT(year: 2018, month: 11, day: 4, hour: 9, minute: 00)

        do {
            try eventStore.save(event, span: .thisEvent)
        } catch {
            print(error)
        }

iPhone端末のTime Zoneがサンフランシスコなど(US/Pacific)に設定されていると、以下の日付になってしまいます。

開始日時: 2018-11-04 1:30:00 PDT =  2018-11-04 08:30:00 +0000
終了日時: 2018-11-04 1:00:00 PDT = 2018-11-04 08:00:00 +0000

つまり開始が終了よりも遅い時間に。
ちゃんとDate型で指定しているのだから反応してよーーとは思うんですけどねぇw

ちなみに、iCloudのカレンダーで上記の状態になります。
同期しているGoogleカレンダーに登録すると、終了時刻が勝手に2時間後に置き換わるようです。
開始 > 終了 というのが不正な状態なので、その場合は置き換わるようになっていそう。

開始と終了日付のTime Zoneが別々に指定できたらよいのですが、iPhoneカレンダーは1つのTime Zone (サマータイム有無は現在の日時のものが有効?)の設定のみ。
Googleカレンダーは別々にTime Zoneが設定できるものの、サマータイム有無について設定ができるわけではない。

EKEventのtimeZoneプロパティに TimeZone(secondsFromGMT: 0) を設定すると正しい日時が登録ができます。
が、Googleカレンダーの場合、詳細を見るとGMTでの登録なのがわかる。

どうするのが一番良い方法なのかは分からず。
iPhoneの設定 > カレンダーにある、「Time Zone Override」の設定によっても変わる予感も。
ふむー。
• • •

2018年7月4日水曜日

サーキットトレーニング

先週からサーキットトレーニングを始めました。
サーキットトレーニングとは、筋力系と有酸素系の運動を組み合わせたインターバルトレーニングのこと。

私はmilon のマシンが置いてあるジムに通っている。
筋力系6種類を1分間づつ、有酸素系2種類を4分間づつ、それらの間は30秒のインターバルになっていて、1周17分30秒でトレーニング。
行う順番は決まってきて、並んでいるマシンを順々に移動していく感じ。

milon のマシンは、シートの位置や、負荷が記録されたカードをマシンに差し込むと、自動で準備してくれるのがすごく便利。
ちなみに、心拍計はついていなく、Polarの心拍計とつなげることが出来るらしい。
でも、そんなにバタバタ動かないし、時間も30分くらいなので Apple Watch で十分と思う。
トレーニング記録は、milon ME というアプリでも見ることができる。
まぁ私は Zones で記録するけれどもねw


やることが決まっているので考えることが少ない

これが、なにげにいい。
通常のジムにいくと、
空いてるところどこかな?...負荷はどれくらいだったっけ...何回やるかな...1回、2回...次はどれにしようか...
と考えながら動くと思いますが、これが全くイラナイ。
英語のPodcastも聞きながらもできる。


全部できてる安心感

筋力も持久力もまったくない私としては、全体的な体力ベースを上げていきたい。
そういう人にとっては、一通り回れば全体的に鍛えられているはず!という安心感がある。


体力ないのがバレにくい

人によって負荷設定が違うが、それがまわりからは見えない。
なので、体力がないために、滝汗をかいて息を切らしていても、「あー、高負荷でやってるんだろうな」って思ってもらえそうなw


心拍数・強度

筋力系マシンを早く動かして、ガシャコガシャコしてるお兄さんを昨日見た。
HICT(高強度サーキットトレーニング)をしたかったのかな?と思うが、種類を選べないジムでのHICTは難しそうに思った。
レッグカールで心拍数を80%以上にとか想像できない...。




上記は私が3周回ってみた時の結果。
心拍数が上がっているのは有酸素系をやっている箇所で、筋肉系では全然心拍数をあげられず、緑のゾーンまで落ちちゃっているという図。

1周回れなかったらどうしよう?というビビリもあり、負荷は軽めでやっているので、これからゆっくりあげていって、全体が黄色のゾーン以上が保てるといいなぁーなんて考え中。

心拍数がだんだん落ちにくくなっているのは、回復心拍数が良くない = 体力がない ということと思う。
体力がついてきたら、きれいな連続した山になっていく...ハズ!


プロテイン

ちょうど、一日の食事で得られるたんぱく質、脂質、炭水化物をメモしてバランスを見ていたので、たんぱく質不足になりやすく、いざ摂取しようと思っても難しい のは感じていた。
脂質や炭水化物を取るのは楽なんですけど、たんぱく質ってホント難しい。
そして、運動をすると更に多くのたんぱく質が必要と言われたら、もう、効率よく取れるなにかに頼るしかないなーと。

たんぱく質の1日の推奨量、
運動していない人は 体重1kgあたり 0.8g だけど、
運動し始めた人は 体重1kgあたり 1.7g  って... 倍じゃん!?


ドンキに行ってプロテインコーナーをぼんやり見ていたが、マッスルな感じのものは手がが出せず、Dear Natura の ソイプロテイン を購入してみた。
プロテインの含有量は少なめとは思うけど、腎臓が弱く、普段の食事のバランスも取れてない初心者の私としては良いかなと。

今まで意識して食べていた納豆のたんぱく質は、6.3g / 1食
Dear Naturaでは一気に15gとれる。
PFCバランスでみると、納豆 の27%から、Dear Naturaは74%にグーンとアップ。

味も飲めるレベルだし泡立ち少なくて飲みやすい。最近のプロテイン、やるね。
気になったのは、甘かったこと。甘い粉なので、ダニが発生しやすそう... 大きな袋でまとめて購入はちょっと怖い。
あと、続けていくには値段が気になるトコロ。手軽さを考えると価値はアリか。


• • •

2018年6月23日土曜日

Flaskが2人であること

6月は人と話す機会が多かった。
Flaskという名前で2人で開発してます」 と自己紹介をするのですが、その時の自分の気持ちとしては、たった2人で頑張っているんです! というのを伝えたいがためなんですが、

「2人は仲がいいのですね」
「喧嘩はしないのですか?」
「なぜ2人なのですか?」

と聞かれることが多い。
そういや、ベンチャー解散の理由はFounder同士の仲違い ってよく聞いたりするなぁと思い出した。仲違いせずにどうして続けていけるのだろう?という疑問が根底にある質問なのですね、多分。

ぶっちゃけ、そういうことはあまり意識していなかった。
1人よりも2人のほうが心強くてやっぱいいよね! と単純な思いで始めて、5年続いた私はラッキーなのだと思う。


仲違いしない ということ

いろんな所で言われていることと思うけど、複数のFounderって結婚に近くて、根底にある価値観が一致していないと難しいのかも。
他人だから意見の相違があるのは当然で、話し合いで解決していくわけだけど、根底の価値観が違うと大変そうだ。

お金に対する価値観 が一致しているのか? は大事と思う。
お金って泥臭い所なので、あまり話しづらいけど、かなり大切。
「一気にboostしてexitしたい」って人と、「大金持ちにならなくていいから、こだわったものをじっくり作りたい」って人はどう考えてもプロセスもゴールも違う。

仲間を見つけようとしている人には、Passionだけでなく、お金とその時間軸、ライフスタイルバランス などの価値観があっているかどうか?も相談しあうと良さげ。

ちなみにFlaskの場合は、最初にそういう話はしなかった。なんとなく、一致していそうだ...とは感じてはいたけど。
後に、「こう思っているけど、一致してる?」ってドキドキしながら聞いた記憶があるw


1人ではない ということ

良いことも悪いことも意見してくれて、親身になってい一緒に考えてくれる という存在は、やっぱりとっても有り難い。
1人だと、決めるのも作業も早いけど、孤独デス。
友達に意見を聞くことはできるけど、「なるほど...」(でも、ココはアレだからやりたくないな..)って反論を心の中で殺しがち。
意見を聞かせてもらっているってお願いしている立場だと、否定するようなことは言えないな...と、気を使っちゃうけど、仲間なら全部吐き出して、その場で議論ができる。そして、その議論の延長上に新しい発想が結構転がっている。

リスクもメリットも同等に共有している仲間だからこそできる会話ってあると思う。
それが心地いいと感じるのは、昔、孫請け開発していた時に、なにかを変えようとしても各関連会社のリスク配分が違うために話がまとまらない というイライラを見てきた経験があるからなんだろうな...。


2人である ということ

人数を増やさないのか? もよく聞かれるけれども、Flaskにとっての成功は、大きな会社にすることじゃない。それよりも、納得感をもって仕事できる方が幸せ。
というか、3人、4人...と人数が増えてもうまくやっていける人が逆にうらやましい。

私は、言語化するのに時間がかかるタイプ。
意見やアイデアを聞いて、なにか「モヤッ」があった時、その「モヤッ」が何なのか? は、1人でゆっくり考えないとうまく表せない。
こういうタイプの私が、多数決ができる人数の中にいると、「みんなが一致しているのだから大丈夫なのだろう」と流されてしまう。
「自分のせいで、みんなの時間を浪費したくない」と思ってしまうケチな要素もある。
...だが、頑固でもあって、流されるのも嫌 という、あーー、めんどくさいヤツw

悩んで時間をかけた箇所って、思った以上に良いアイデアが生まれたりするので、これがまた楽しい。
逆に、ほとんど議論せず作られたものって、当たり障りのない面白くないもの だったりしがちかも。

ということで、私は「ゆっくり考える時間をオクレ!」と気軽に言える人数感がBEST。
BESTな人数や方法って、メンバーの性格によって千差万別なんだろうな。



2人であること を言語化したいと思って、このブログを書いてみたのだけども、自分がめんどくさいヤツということを再認識して少しオチたw


• • •

2018年6月16日土曜日

WWDC 2018に行ってきた



WWDC 2018 に行ってきました。
今年も、空が青い!


街ナカには、Bird と Lime-S (電動スクーター)が、あらゆる所に乗り捨てられていていた。
スピード結構でるし、手軽だし、かなり便利。
サンノゼは歩道が結構広いので、乗り捨てられていても邪魔にならずに受け入れられているっぽい。

今年は、WWDCの公式なSpecial Eventsとして、朝にみんなでRunとかBootcampがあったりなど。
私は、Kayla Itsines のBootcamp に参戦してみた。


参加できるのはWWDCの参加者なので、体力がないオタク な人も多いハズ!
と思っていたが、なんか動ける人ばかりで場違い感ハンパなかった。
スタイル良しのスマートなDeveloper。すげー憧れるー!


さて、本題のWWDC。

技術的には、ARKit2Siri Shortcuts の発表があった。
それらも楽しみではあるのだが、今回注目すべきことは、Apple が Developer に宣言した、以下の一言に集約される方針と思う。



広告収入が目的のアプリの場合、どれだけアプリを起動してもらえるか、長く滞在してもらえるか が重要かと思う。
だが、今回発表された、Screen Timeではアプリ滞在時間を確認して、無駄に時間を浪費するのを防いだり、Siri Shortcuts でアプリを起動せずにやりたいことだけ指示する など、ある意味、反対方向の仕組みを投入してきた。

以前、AppleはiAdという広告プラットフォームを提供していたが、2016年6月に終了し、それからは、サブスクリプション(定期購読)の押しが強くなった。
iAdが終了したときには衝撃を受けたのだが、今思うと、こういう方針でいくのであれば当然な決断だったのだろう。

Developerとしても、マネタイズを意識するあまりにユーザにとって「悪」なことはしてなかったか? を意識するよいきっかけなのかもしれない。

Siri Shortcuts を勝手に深読みすると、VR/MRを見据えていると捉えることもできそうな。
音声で指示するのって、とても大事な要素な気がするから。
Natural LanguageCreate MLで、いろんな言語の情報を蓄積しておきたいって気持ちもありそうな感じ。
サードパーティのDeveloperに、Siri Shortcutsで、そういうアプリの設計に慣れておいて欲しい という期待も含まれているかもしれない。


あと、iOS12 で動作が軽くなるのがとても嬉しい!
Twitterなどの反応をみるに、実感できるほど軽くなっているらしい。
昨年末に旧機種の動作を故意に遅くしている疑惑が持ち上がったが、それに対する回答の一つだろうか。
Developerとしては正直、ユーザーはどんどん新機種に移行して欲しい のだが、今後も切られるデバイスは少なくなっていくのかもしれない。

ともあれ、WWDC楽しかったです!
Appleさま、現地で仲良くしてくださったみなさま、ありがとうございました (_ _)


• • •

2018年5月31日木曜日

WWDC 2018の期待

WWDC 2018 の開催が来週に迫ってきました!
今年も現地に行って体感してきます。

今年はどんな発表があるのでしょうか?
新型iPad, Macbookなどのハード系の話が多く、ソフト系は安定・セキュリテイ強化が中心であまり進化がないのでは? という話がちらほら。
でも、ソフト系のワクワクもあって欲しいな!だってWWDCなのですから!


期待は昨年と同様...

昨年、WWDC 2017の期待 という記事を書いたのですが、今年もあまり変わっていないカモ。
・HomePodは、次はカナダ、フランス、ドイツの発売らしく日本はしばらく無さげ。
・AirPowerは、待っているよ。早く発売して欲しい!(お土産にしてくれてもいいよw)
・Apple Watchの血糖値測定、睡眠トラッカーは今年も期待してる!
・Watch Faceのサードパーティへの開放はもう無いのかもな...。


ヘルスケア系

今回は、WWDCのスペシャルイベントとして、WatchのActivityの3つのリングを完成させる「Close Your Rings Challenge」や、「Kayla Itsines Bootcamp powered by SWEAT」「WWDC Run with Nike Run Club」というヘルス系のものが多くある。
ヘルスケアに力入れている感が伝わってくるので、なんだか期待しちゃう!


OCR

iOSで使えるOCR、あまり進化がないもんなんだなと最近思ったので。
MRと騒がれているのだから、もっと進化した何かがポンポン出てきてもいいんじゃないかしらーと素人目に思ってしまう今日このごろ。
(難しいところは賢い人にまかせて、結果を使いたい派です...)

Google Cloud Vision API だと賢くなっているのかもですが、Tesseract OCR iOS のようなローカルなものか、Apple提供の何か... でてくると嬉しい。


NFCのサードパーティ開放

これがきたら忙しくなる日本の開発者も多そう。



ワクワクさせてほしい気持ちもあるけど、ちょっと落ち着いて溜まったバグを早く消化して欲しい気持ちもある。
後者であれば、いろんなBest Practiceをお勉強する場になりそう。

ともあれ、楽しんできます!



• • •

2018年4月21日土曜日

電子タバコをお試し中

今年もWWDCでサンノゼに行くことになった!

心配は、英語はもちろんなのだが、タバコが吸えないこともある。
ホテルやバーも含め、室内では一切吸えないし、夜中にフラフラ外にでてタバコを吸いに行くのも怖いので....吸えない時間はかなり長く、ツライ。

なので、タバコの量を減らすことも目的に、電子タバコを試し中です。

EMILI


2年前ぐらいに EMILI を購入していた。

サイズも小さくて、2本あるので持ち運びもしやすくてよかったのだが、使わなくなってしまった。
一番の理由は、ニコチン無しのリキッドで試したので、タバコからの完全移行はできなかったこと。
リキッドが白く結晶化することがあるようで、カートリッジ部が透明なのもあり、それが目立ってしまうのがイマイチだった。


glo




IQOSはタバコ葉を直接加熱するが、gloはタバコのスティック全体を外側から加熱するタイプ。
IQOSは頻繁にお手入れが必要で臭いもあるらしいので手を出さなかったのですが、gloはお手入れも楽。
タバコのガツン感は無いけど、タバコを減らしていきたい人にはちょうどいいのかなというのが試した理由。
ただ、私には、味がイマイチ合わず。なんか変な雑味がある。
美味しさを求めてはいけないのはわかっているのだが、まずいと思って吸い続けるのもなぁ...


C-Tec



ビタミン入りの電子タバコ で、フレーバーにはニコチンは含まれてないタイプ。
本体部分はPloomTECHと互換があるので、C-Tecのフレーバーを楽しみつつ、PloomTECHも試してみようという目的で購入。

スターターキットにはいっていたエナジードリンク、クリスタルメンソールのフレーバーは美味しかった。
追加でエスプレッソを試してみたが、これが甘いコーヒー牛乳みたいな味で、マズくて、びっくりしたw



PloomTECH



今は、PloomTECH をメインで使用中。
タバコのガツンと感はほぼ無い。ペビースモーカーだった人は全く物足りないはず。
ニコチン量を減らしたい、禁煙したいって人には良い製品と思う。
味はgloに比べたら断然美味しい。

C-TecやPloomTECHのような使い捨てカートリッジという仕組みもイイ。EMILIのリキッド充填式のカートリッジが不満点が解消された。口につけるものだからなぁ うんうん。
デメリットとしては、ずーっと吸い続けてしまうトコロ。

PloomTECHはタバコ葉部分と、リキッド部分が別れているので、同時に使い切れないこともある。
それもあり、リキッドを別途購入して、カートリッジ再生もたまにしている。
リキッドのフレーバーは、青りんご味とか試したけど、やっぱりメンソールがイイ。



今使っているのは、XLVAPORのXLメンソール。
名前からして凄くキツイメンソールかとおもいきや、そうでもなく、普通に吸えるので次回は特大100ml版を買う予定。

カートリッジ部分は再生できるといっても、消耗品なので、たまに補充している。




BLIESTのカートリッジ(5本 980円)は、PloomTECHのタバコ部分をつけることができる。

移行してから2ヶ月ちょいほどだが、まだニコチンゼロだとちょっとツライ。
でも随分ニコチン量は減っているように思う。
グリセリン(VG)、プロピレングリコール(PG)の吸入量はめっちゃ増えてるけどね!



• • •

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の登録例
let subscription = CKDatabaseSubscription(subscriptionID: subscriptionID)
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: nil)
operation.modifySubscriptionsCompletionBlock = { _, _, error in
print("added Database Subscriptions")
}
database.add(operation)
CKRecordZoneSubscriptionはZoneIDを、CKQuerySubscriptionは対象のRecord Type, NSPredicate(条件),タイミング(作成、変更、削除時)などを指定できる。


通知を受ける

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

通知設定の登録
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.requestAuthorization(options:[.alert]) { (granted, error) in
  assert(granted == true)
}
UIApplication.shared.registerForRemoteNotifications()
通知の受信
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let userInfo = userInfo as? [String: NSObject] else { return }
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
guard let subscriptionID = notification.subscriptionID else { return }
if notification.notificationType == .database {
//CKDatabaseSubscription
//subscriptionID から 該当のDatabaseを判断して、
//CKFetchDatabaseChangesOperation で変更情報を取得して反映する
} else if notification.notificationType == .recordZone {
guard let zoneNotification = notification as? CKRecordZoneNotification else { return }
guard let recordZoneID = zoneNotification.recordZoneID else { return }
//CKRecordZoneSubscription
//recordZoneIDから該当のRecord Zoneを判断して、
//CKFetchRecordZoneChangesOptions で変更情報を取得して反映する
} else if notification.notificationType == .query {
guard let queryNotification = notification as? CKQueryNotification else { return }
guard let recordID = queryNotification.recordID else { return }
let recordZoneID = recordID.zoneID
//CKQuerySubscription
//対象のRecordIDがNotificationから取得できる
}
}

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


終わりに

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


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


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

var currentToken:String?
func fetchChanges(from database: CKDatabase) {
 let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: currentToken)
var newZoneIDs = [CKRecordZoneID]()
operation.changeTokenUpdatedBlock = { serverChangeToken in
//Databaseの新しいTokenを保存しておき、次回に利用する
self.currentToken = serverChangeToken
}
operation.recordZoneWithIDWasDeletedBlock = { zoneID in
//対象のRecord Zoneをキャッシュから削除する
}
operation.recordZoneWithIDChangedBlock = { zoneID in
if {キャッシュに対象のRecordZoneがあれば} {
//Record Zoneの差分を取得して更新する
} else {
newZoneIDs.append(zoneID)
}
}
operation.fetchDatabaseChangesCompletionBlock = { serverChangeToken, moreComing, error in
if CloudKitError.share.handle(error: error, operation: .fetchChanges, alert: true) != nil {
if let ckError = error as? CKError, ckError.code == .changeTokenExpired {
//トークン切れ
self.currentToken = nil
self.fetchChanges(from: database)
}
return
}
self.currentToken = serverChangeToken
guard moreComing == false else { return }
guard newZoneIDs.count > 0 else { return }
//新しいRecord Zoneの処理
let fetchZonesOp = CKFetchRecordZonesOperation(recordZoneIDs: newZoneIDs)
fetchZonesOp.fetchRecordZonesCompletionBlock = { results, error in
guard error == nil, let zoneDictionary = results else { return }
for (_, zone) in zoneDictionary {
//Record Zoneをキャッシュに追加して、内容を取得する
}
}
database.add(fetchZonesOp)
}
database.add(operation)
}




• • •

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を保存する場合は、

let database = CKContainer.default().privateCloudDatabase
let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
operation.modifyRecordsCompletionBlock = { (records, recordIDs, error) in
//TODO:
}
operation.database = database
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を実行
let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil)
operation.longLivedOperationWasPersistedBlock = {
print("Call longLivedOperationWasPersistedBlock ID=", operation.operationID)
UserDefaults.standard.setValue(operation.operationID, forKey: "LongLivedOperationID")
UserDefaults.standard.synchronize()
}
let configuration = CKOperationConfiguration()
configuration.isLongLived = true
operation.configuration = configuration
database.add(operation)
アプリ再起動時にLongLivedOperationを取得
let container = CKContainer.default()
//全てのIDを取得
container.fetchAllLongLivedOperationIDs { (operationIDs, error) in
print("called fetchAllLongLivedOperationIDs")
if let e = error { print(e) }
if let ids = operationIDs {
print("operationIDs count=", ids.count)
}
}
//保存したOperationIDから検索
if let longLivedID = UserDefaults.standard.string(forKey: "LongLivedOperationID") {
container.fetchLongLivedOperation(withID: longLivedID, completionHandler: { (operation, error) in
if let e = error {
print(e)
} else if let ope = operation {
print("find operation ID=", ope.operationID)
}
})
}
関連: 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を渡す。

let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: cachedToken)
operation.fetchDatabaseChangesCompletionBlock = { serverChangeToken, moreComing, error in
if let e = error {
if let ckError = e as? CKError, ckError.code == .changeTokenExpired {
print("token expired")
}
return
}
print("new token is ", serverChangeToken)
}
Record Zoneの場合は、CKFetchRecordZoneChangesOptionsに設定する。

let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = cachedToken
let zone:CKRecordZone = ...
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zone.zoneID], optionsByRecordZoneID: [zone.zoneID: options])
operation.recordZoneFetchCompletionBlock = { (zoneID, serverChangeToken, clientChangeTokenData, moreComing, error) in
if let e = error {
if let ckError = e as? CKError, ckError.code == .changeTokenExpired {
print("token expired")
}
return
}
print("new token is ", serverChangeToken)
}
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で保存する。
let record = CKRecord(recordType: "MyType", recordID: recordID)
record["title"] = "name" as CKRecordValue?
record["price"]= 1 as CKRecordValue?
view raw CKRecord.swift hosted with ❤ by GitHub
最初に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 などについて書いていく予定。


• • •

2018年2月11日日曜日

MKMapViewで全てのピンを表示する



こんな感じで検索結果をマップ上に表示したい。

全てのピンを表示する一番簡単な方法は、iOS7で追加された MKMapViewの showAnnotations メソッドを使うこと。

ただ、

画面下のMapの上に乗っているView部分は、対象外としてパディングして欲しい

...ので、MKMapViewの setVisibleMapRect で edgePadding を使うかなーと、MKMapRect に変換し、MKMapPointForCoordinate(座標からMapPointを算出) や MKCoordinateRegionMakeWithDistance (距離からRegionを算出)などを使ってチマチマ計算していたのですが、結論をいうと、そんなことしなくてもイケた。

MKMapViewの layoutMargins を設定すればOK!
(iOS11からはdirectionalLayoutMargins)

Marginを設定すれば、 showAnnotations だけでなく、setRegion や setCenter でマップの中心位置を指定する際にもちゃんと合った動きをしてくれます。

Margin が使えるとなれば、showAnnotations を使わずとも、自分で最大最小の緯度経度を出して制御するのも結構簡単。ググればいっぱい引っかかります。
ズームスパンを自分で決めたり、必要な時だけズームを反応させたりとか できちゃいます。

Margin って UIView にある共通のメソッドなので、そんな動きをしてくれるとは思ってなかったなーっ

ちなみに気づいたきっかけは、MKMapViewがSafeAreaのサイズは考慮されている動きをもともとしていたから、あれ、そんならMargin 反応するのか? と思った次第。

layoutMargins を使う話はあまり露出なさげだったので、書いてみました。


• • •