- 投稿日:2019-10-24T22:03:40+09:00
NFC Automation Trigger + SESAME API による自動解錠と考察
まえおき
これからiOSを前提として話を進めていきますが、話の主題としてはiOSかどうかはあまり関係ありません。
NFC Automation Trigger
13.1のiOSリリースによりNFC Automation Triggerが使えるようになりました。NFCとは、Wikiによると次のようなものです。
狭義にはNear field communication(NFC)の訳語。NFCはRFID(Radio Frequency IDentification)と呼ばれる無線通信による個体識別の技術の一種で、近距離の無線通信技術を統一化した世界共通の規格である。ICチップを内蔵したNFCタグをNFCのリーダ・ライタ機能を有する機器で読み取ったり書き込んだりする。
※ https://ja.wikipedia.org/wiki/近距離無線通信
有名なものとしては、モバイルSuicaでしょうか。AppleWatchがリーダ機として扱われます。
今回のリリースであるNFC Automation Triggerは、このNFCを読み込むと自動的に特定のアクションをTriggerすることができます。
例えば、特定のNFCタグをiPhoneが読み取ると、3分のタイマーが起動する!とか。ま、表題の件を用意したんですけどね!(笑)とりあえずこんなの作ったよ!
登録しているNFCタグをiPhoneが読み取ると、扉に設置しているスマートロックアイテムSESAMEのSESAME APIをRequestして扉の鍵が解錠されます。
↓ 白色のNFCタグにスマホを近づけると...?
NFC Automation Triggerが作動!解錠!※ https://twitter.com/silver_birder/status/1187016726363299840
作成方法は、とってもかんたんです。iPhone標準アプリshortcutと、NFCタグ(1枚94円)があれば誰でも作れます。もちろん、SESAMEが必要ですけどね(笑)。
shortcutで、次の準備をしました。
- RESTfulAPIのショートカットを使ってSESAME APIを設定する。
- 個人用オートメーションを作成し、用意したNFCタグをスキャンする。
- 2のオートメーションを1のショートカットと紐付ける。
これの良いところは、次の2点でしょうか。
- 標準アプリだけで完結
- NFCタグ自体には何も加工しないため安全
※ SESAMEには、扉に近づいたら解錠する手ぶら解錠や、スマホをノックするだけで解錠するノック解錠というものがあります。本来は、これを使いたかったのですが精度が低いため実用性に欠けると思っています。
考察
このNFC Automation Triggerは、アイデア次第でいくらでも、便利なことができます。
今回の目的は 「個人利用で、ライフハックできるものを作りたい」というものです。ただ正直、iPhoneをわざわざNFCに近づける動作は面倒だと思います。
私の部屋にはVUIとしてGoogleHomeがあり、スマートリモコンのNatureRemoや、物理ボタン自動化のSwitchBot、コンセントのスマート化TP-LinkなどがGoogleHomeと連携しています。
手で操作するよりも、声で操作する方が、数ステップですが遥かに効率的と感じます。つまり、VUIで管理できている空間に関しては、NFCのTriggerはあまり役立ちはしないのかなと思っています。逆に、VUIの管理外の空間、私の部屋でいうと、トイレとか洗面台、玄関にはNFCが役立つかもしれません。また、声を発するのが躊躇われる環境においても、NFCの方が役立つと思っています。
外の環境では、どうでしょうか。NFCを貼れる場所なので、限定はされます。
自宅の郵便ポスト、自転車・車、衣類、カバン、傘、学校や職場のマイ机・椅子とかでしょうか。
NFCタグ(1枚94円)は、シールのように柔らかいので、コップやボールにも貼れると思います。アマゾンダッシュボタンが一時流行っていましたが、あれと似たようなこともできます。例えば、「トイレットペーパの入れ物にNFCタグを貼っておいて、なくなりそうになったらNFCタグを読み込んで注文する」みたいな。もちろん、注文する処理は、自前で組む必要がありますよ。アマゾン定期注文には向いていないものには、良いかもですね。さらに、shortcutでは変数を注入できるため、例えば、現在地をNFCトリガーに注入することで、
「現在地から自宅までのタクシーを予約する」NFCトリガーもできちゃいます。個人利用という括りであっても、NFC Automation Triggerの使い道はとても多いと思います。
商用利用と考えると、更にあると思いますが、省略します。さいごに
NFC Automation Triggerという機能は、珍しくありません。
しかし、とても使いやすくカスタマイズ性が高いため、アイデア次第でいくらでも化けれます。
一攫千金?を目指すのも良いですが、やっぱりライフハックをつきつめたいなと思う私でした。
- 投稿日:2019-10-24T18:07:11+09:00
AWS CLIを使ってAmazon SNSからPushKitのVoIPプッシュ通知を送る
これはなに
AWS CLIを使ってAmazonSNSからiOSのVoIPプッシュ通知(PushKit通知)を送る手順について、主にデバッグ用途です。
当方、AWSに詳しくないので内容に誤りがある場合はご指摘ください。準備
- AWS CLIをインストールする
- AmazonSNS full access権限のあるIAMを払い出す(もしくは最小限のアクセス権限)
- 環境変数にIAMの情報をセットする
# env | grep AWS AWS_SECRET_ACCESS_KEY=XXXXX.... AWS_DEFAULT_REGION=ap-northeast-1 AWS_DEFAULT_OUTPUT=json AWS_ACCESS_KEY_ID=YYYYY...エンドポイントの登録
試験用であればAmazonSNSのGUIから登録すればよい、手順は省略。
注意点とし、手元の環境では東京リージョンに登録したエンドポイントへはcliから送信できなかった。米国東部 (バージニア北部)にアプリケーションを作成して登録したら送信できた(要調査)。
aws cli からVoIPプッシュ通知を送る
開発環境(sandbox)の例
endpointのarnが
arn:aws:sns:us-east-1:xxxxxxxxx:endpoint/APNS_VOIP_SANDBOX/your_app_name/yyyy....
の場合のコマンドaws sns publish \ --target-arn arn:aws:sns:us-east-1:xxxxxxxxx:endpoint/APNS_VOIP_SANDBOX/your_app_name/yyyy.... \ --message-attributes \ '{ "AWS.SNS.MOBILE.APNS.PUSH_TYPE":{"DataType":"String","StringValue":"voip"}}' \ --message '{"APNS_VOIP_SANDBOX":"{\"aps\":{\"foo\":\"bar\"}}"}' \ --message-structure jsonポイントは
--message-attributes
で"AWS.SNS.MOBILE.APNS.PUSH_TYPE":{"DataType":"String","StringValue":"voip"}
を指定すること--message
でAPNS_VOIP_SANDBOX
キーにaps
を含むデータを入れること本番環境の例
基本的に開発用の場合と同じ、ただし
APNS_VOIP_SANDBOX
をAPNS_VOIP
に変更すること。要調査
- 東京リージョンに登録した場合にcliで送信できない件、仕様なのか手順ミスなのか
参考情報
- 投稿日:2019-10-24T15:54:48+09:00
iOSDC 2019 軽くまとめ
投稿するの忘れてた ('・ω・')
ランチセッション
施策・気づき
座談会を実施している
- 開発する機能に関してユーザーから直接ヒアリングする
- ユーザーからの評価が高いユーザーなどに直接ヒヤリングなどして、意見を拾っていく
- 密に連絡をとっているユーザーがいる
評価は1日が終わったあとに、まとめて評価している
- 思い出さなければならない
- ワンショット評価
- カードをスワイプで評価している
- ドライバーからヒヤリングした結果を表示している
評価フロー
- 評価の基準がわからない
- 基準が人によってぶれている
- 星3の参考値などを出すようにしている
- 5か1によるので、2択にした
体制
iOS 2
Android 2
server 1
SRE 1
PO/PM 3
Design 2
- 一週間のサイクルでスクラム
- 意味を輪読会
- 正しいものを正しく作る
- アーキテクチャはviper
- 交流イベントもあり 恵比寿 9/13(Fri) 20:00 ~
- スクラムマスターが存在したことで、スクラム化が進んでいった
- 物理カンバンを使っている
ユーザーヒヤリングについて
- 考えていることを口に出して言ってください
- 心理的安全を作る
- 一部のユーザーには先にβとして使ってもらったりもする
開発でドライバーとライダー、どっちを優先するのか
- KPIを追っていない、直接聞いて対応している
- プロダクトチームはUXに軸を置く
クリーンコード
- 提唱 -> Uncle bob
読みやすいコードとは
- わかりやすい
- 安全である
- バグに強い
- 変更に強い
KickStarterを参考にできる
クリーンコードを書くには
ジェネリック
- 具体的なデータ型に依存しない
- 汎用的に利用できる ### プロトコル
- static ジェネリティクスの型安全
- dynamic 直接方として な面がある よりジェネリックにするために、protocolを使う #### ここの問題を解決し、ジェネリックな解決策を抽出する
APIで通信する場合に、ジェネリックを利用して、共通した処理を書く
プロトコルを利用してきれいに書く
ジェネリクスの型制約
PATにぶつかったときは、protocolを配列として使うかどうかを考える
重複削除や、再利用という点で、ジェネリックにする
プロトコルにこだわるのではなく、選択する
まずは具体的な問題を具体的に解決する
パターンを考えるのではなく、具体的に考えるクリーンに書き続けるには
コードをチェックアウトしたときよりも、コードをキレイにしていく
Bluetooth
BLE
仕組み
Central -> 電波探す
Periphreral -> 電波を発信ペリフェラルになれない端末が存在する
最適なデータ交換について説明するL2CAP
端末同士を接続して、双方向でデータ送受信を行う方法
iOS 11 以上
ANdroid 10
でないと使えないメリット
BLEの仕様を知らなくても使える
接続し終わったらあとはただのストリームのプログラミングチャンネルを開くとPSM(UInt)がとれる
もう片方の端末で、チャンネルのオープンを試みる
(コルーチン?)双方向通信
inputStream, outputStream
どのくらい送れるのか
普通のBLE: 20バイト
L2CAP: 2048バイトシーケンスをまとめる
最適なバイト数がわからない場合、送信されたデータをペリフェラルから返す
ペリフェラルは複数の端末から接続される場合がある
iOS のプロパティにBLEを必ず保持しておく
バイトオーダーの問題があるAppStoreReview
- fastlaneのプラグインに、ipaのリジェクトチェックができる
- PrivateFrameworkを使っていないか
- frameworkをインストールすると、iPhoneの中に入る、アプリはそのヘッダーを利用している iOKitというのは、使っているとリジェクトされる
- warningが出ていれば、リジェクトされる可能性があり
- AppCheckerを使って、CIツールでビルド後にチェックする
- ガイドラインはdiffをとることで、わかりやすい
- IDFAを使っているかどうかチェックできるのか? -> 不明
EAR(輸出規制)
- iOSはappleが配信しているため、輸出になる
- httpsは暗号化技術
- 判定チャートがある
- 一般に入手可能化?一般なものはマスマーケット品目となる
- EARで規制されない
- 自己番号分類報告を提出しないといけない
- 報告は、2箇所のメールアドレスに送る
- 暗号化 -> 「はい」 満たしているか -> 「はい」となる
XCFrameworks
- xcode11から、フレームワークをバイナリ配布するようになった
- Swift 5.1 module stability -> バージョンの違いがなくなる
設定プロファイルはplist
- 投稿日:2019-10-24T15:38:01+09:00
iOSの有名リポジトリのコードの小技
うまい人の研究してわかったこと
- アクセス修飾子をちゃんと書く
- アクセス修飾子ごとにextentionを切る
internal var heroIDToSourceView = [String: UIView]()
というふうに、propertyをoptionalにするようなことはしない
- optinalはoptinalにする必要にするときだけ
- delegateはweakにする
UIView(frame: transitionContainer?.bounds ?? .zero)
nullのcheckに演算子を使うfor v in animatingToViews { _ = context.snapshotView(for: v) }
- 1行for文もあり
deferの使い方が上手い(loadingを消したり、初期化処理をしたりする)
- deferとは、途中でreturnしても実行される文
funcに対するコメントを書く
/** Update the progress for the interactive transition. - Parameters: - progress: the current progress, must be between 0...1 */
- propertyのViewControllerの宣言が、くっそうまい(関数的で、他に影響がないように書いている)
lazy var mmPlayerLayer: MMPlayerLayer = { let l = MMPlayerLayer() l.cacheType = .memory(count: 5) l.coverFitType = .fitToPlayerView l.videoGravity = AVLayerVideoGravity.resizeAspectFill l.replace(cover: CoverA.instantiateFromNib()) return l }()Layerのアニメーションに関して
fileprivate func setFrameWith(quadrant: VideoPositionType, dismissVideo: Bool) { let margin = self.config.margin var rect = self.config.playLayer?.playView?.frame ?? .zero let size = UIScreen.main.bounds switch quadrant { case .leftTop: rect.origin.x = dismissVideo ? -rect.size.width : margin rect.origin.y = margin case .rightTop: rect.origin.x = dismissVideo ? size.width : size.width-rect.size.width-margin rect.origin.y = margin case .leftBottom: rect.origin.x = dismissVideo ? -rect.size.width : margin rect.origin.y = size.height-rect.size.height-margin case .rightBottom: rect.origin.x = dismissVideo ? size.width : size.width-rect.size.width-margin rect.origin.y = size.height-rect.size.height-margin } to?.view.alpha = 0.0 UIView.animate(withDuration: self.config.duration, animations: { if dismissVideo { self.config.playLayer?.playView?.alpha = 0.0 } self.config.playLayer?.playView?.frame = rect }) { [unowned self] (_) in if dismissVideo { (self.config as? MMPlayerPassViewPresentConfig)?._dismissGesture = true self.config.playLayer?.setCoverView(enable: true) self.to?.dismiss(animated: true, completion: nil) } } }willSet + newValueを使う
@IBOutlet weak var okButton: UIButton! { willSet { // OKボタンの修飾 newValue.layer.borderColor = UIColor.white.cgColor newValue.layer.borderWidth = 1.0 newValue.layer.cornerRadius = 5.0 } }
- 投稿日:2019-10-24T15:06:22+09:00
BitriseのOrganaizationでAppleとの連携が表示されてなくて超絶焦った話
やりたいこと
BitriseではApple Developer Accountと連携することで、iOSのProvisioning Profileの管理を簡単にできる!
https://devcenter.bitrise.io/jp/getting-started/connecting-apple-dev-account/年に1回更新が発生して面倒な作業ですが、Bitriseの神対応によって楽に実施ができる!
すごい!これはやるしかない!さぁやるぞ!早速つまづいた...。
あれ...?Organaizationのページだと連携のパネルが表示されていないぞ?
解決策?
そもそも仕様で、Organaizationに所属しているいずれかの人の認証情報を使用するようです。
個人のアカウントがApple Developer Accountと連携していれば、プロジェクトのTeamタブから選択できるようです!Organaizationはチームに所属しているユーザの情報を利用できるんですね!
個人開発している人とかは迂闊にアカウント登録すると、Organaizationに入った時にややこしくなりそうです!
みなさん気をつけましょう!
- 投稿日:2019-10-24T15:06:22+09:00
BitriseのOrganaizationでAppleとの連携が表示されてなくて超絶ハマった話
やりたいこと
BitriseではApple Developer Accountと連携することで、iOSのProvisioning Profileの管理を簡単にできる!
https://devcenter.bitrise.io/jp/getting-started/connecting-apple-dev-account/年に1回更新が発生して面倒な作業ですが、Bitriseの神対応によって楽に実施ができる!
すごい!これはやるしかない!さぁやるぞ!早速つまづいた...。
あれ...?Organaizationのページだと連携のパネルが表示されていないぞ?
解決策?
そもそも仕様で、Organaizationに所属しているいずれかの人の認証情報を使用するようです。
個人のアカウントがApple Developer Accountと連携していれば、プロジェクトのTeamタブから選択できるようです!Organaizationはチームに所属しているユーザの情報を利用できるんですね!
個人開発している人とかは迂闊にアカウント登録すると、Organaizationに入った時にややこしくなりそうです!
みなさん気をつけましょう!
- 投稿日:2019-10-24T15:02:05+09:00
【Swift】Combineで一つのPublisherの出力結果を共有するメソッドやクラス�の違い(share, multicast, Future)
Combine.frameworkを使用していると
複数のSubscriber
で同じ処理結果を共有したい場合があります。例えば
- ネットワーク通信を介したデータの取得
- 画像のダウンロード
といった重い処理を繰り返し実行すると
メモリをたくさん使用したり
時間がかかるためユーザ体験を損なってしまう可能性もあります。Combine.frameworkでは
そのような時に利用できるメソッドやクラスが用意されています。今回はそういったメソッドやクラスの違いについて
見てみたいと思います。下記のサンプルコードは全てPlayground上で実行しています。
share
share
メソッドでは
subscribeする度に新しいPublisher
を生成するのではなく
Publisher
の参照を共有するようにします。
Publisher
はだいたいがstruct
であるため
関数の引数で使用される場合やプロパティに代入する際は
コピーを生成します。shareメソッドを呼ぶと戻り値には
Publishers.Share
という
クラスのインスタンスが返ってきます。
Publishers.Share
https://developer.apple.com/documentation/combine/publisher/3204754-shareこのクラスはクラスが生成されるよりも
前(上流)のPublisher
を保持することで
既存のPublisher
を使用することができます。最初のsubscribeで
Publisher
は処理を開始し値を流し始めます。そしてそれ以降のsubscribeからは
同じPublisher
からの値の出力を受け取ることができます。それでは具体的な例として
ネットワークリクエストの結果を
複数のSubscriber
で共有する例を見ていきたいと思います。var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .share() // **ここでshareを呼んでいる** print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 2回目 shared: receive value: (13761 bytes) subscription1 receiveValue: '13761 bytes' subscription2 receiveValue: '13761 bytes' shared: receive finished出力結果を見てみると
1回目のsubscribeではsubscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimitedと(上流の)
Publisher
へsubscribeしていますが2回目の場合
subscribe 2回目 shared: receive value: (13761 bytes) subscription1 receiveValue: '13761 bytes' subscription2 receiveValue: '13761 bytes' shared: receive finishedと
1回目のsubscribeで出力されていた
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
がなく
subscribeされていない
ことがわかりました。それでも2回目のsubscribeも値を受け取っています。
このような結果から
Publisher
は一つしか存在していないことがわかります。では
share
がなかった場合を見てみます。var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") //.share() // **コメントアウト** print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13761 bytes) subscription2 receiveValue: '13761 bytes' shared: receive finished shared: receive value: (13763 bytes) subscription1 receiveValue: '13763 bytes' shared: receive finishedとなり
2回目のsubscribe時でもsubscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimitedと出力されています。
つまり
Publisher
のコピーが生成され
Publisher
が2つ存在していることがわかります。このように
share
を使うことで
不要なPublisher
インスタンスを生成しなくなりました。ただし
share
には注意点もあります。
share
では
subscribeする前に出力された値を再度出力しません。つまり
参照を保持するタイミングによっては
期待したい値が得られないかもしれません。※
参照を保持する時点で
Publisher
がcompletionしていた場合は
finished(completionイベント)のみが返ってきます。
※ sink(receiveCompletion:)の方に値が流れてきます。下記の例を見てみます。
var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .share() print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { print("subscribe 2回目") shared .sink(receiveCompletion: { print("subscription2 receiveCompletion \($0)")}, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13750 bytes) subscription1 receiveValue: '13750 bytes' shared: receive finished subscribe 2回目 subscription2 receiveCompletion finished出力結果からもわかるように
2回目のsubscribe
のタイミングを遅らせると
Publisher
はすでにcompletionしているため
finished(completionイベント)のみを受け取っていることがわかります。multicast
上記の
share
ではタイミングによっては
値を受け取れない可能性がありました。そこで
Combineでは値の出力を能動的にコントロールできる
multicast
というメソッド(戻り値はPublishers.Multicast
クラス)があります。https://developer.apple.com/documentation/combine/publisher/3204734-multicast
https://developer.apple.com/documentation/combine/publisher/3204733-multicast
https://developer.apple.com/documentation/combine/publishers/multicastこれは
ConnectablePublisher
プロトコルに適合しています。/// A publisher that uses a subject to deliver elements to multiple subscribers. final public class Multicast<Upstream, SubjectType> : ConnectablePublisher where Upstream : Publisher, SubjectType : Subject, Upstream.Failure == SubjectType.Failure, Upstream.Output == SubjectType.Output {https://developer.apple.com/documentation/combine/connectablepublisher
このプロトコルは
connect
というメソッドを有しており
connect
を呼び出して初めてPublisher
はSubscriber
を受け取りを処理を開始します。
https://developer.apple.com/documentation/combine/connectablepublisher/3204394-connect
share
の例を
multicast
に変えて違いを見てみます。var cancellables: Set<AnyCancellable> = [] let multicasted = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .multicast { PassthroughSubject<Data, URLError>() } print("subscribe 1回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) multicasted .connect() .store(in: &cancellables) // 出力結果 subscribe 1回目 subscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13741 bytes) subscription1 receiveValue: '13741 bytes' subscription2 receiveValue: '13741 bytes' shared: receive finished上記の結果では
share
と同じ結果を取得できました。では
ここでconnect
をコメントアウトしてみると... //multicasted // .connect() // .store(in: &cancellables) // 出力結果 subscribe 1回目 subscribe 2回目となり
subscribe
してはいるものの
Publisher
はまだ処理を実行していません。このことから
multicast
では
connect
を呼ばないと
処理を実行しないことがわかります。では
2回目のsubscribeを少し遅らせ
connect
を呼び出してみた場合を今度は見てみます。var cancellables: Set<AnyCancellable> = [] let multicasted = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .multicast { PassthroughSubject<Data, URLError>() } print("subscribe 1回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { print("subscribe 2回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("connect") multicasted .connect() .store(in: &cancellables) } // 出力結果 subscribe 1回目 subscribe 2回目 connect shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13745 bytes) subscription1 receiveValue: '13745 bytes' subscription2 receiveValue: '13745 bytes' shared: receive finished
connect
が呼ばれる前にsubscribeしているため
2回目のsubscribeでもデータがきちんと取得できています。これで
share
で起きていた問題は緩和できそうですが
ちょっと書き方が複雑であったり
connect
を呼ぶタイミングによっては
subscribeし損ねてしまう可能性もまだ残っています。他のライブラリでは出力された値を再度流してくれるメソッドがあります。
例えば
RxSwiftではshareReplay
というメソッドと利用することで
指定したサイズ分の流れてきたデータをキャッシュすることができ
subscribe時にその値が流れてくるようにできますが
Combineでは現状存在しません。もし必要な場合は↓のように独自の実装が必要になります。
https://github.com/tcldr/Entwine/blob/master/Sources/Entwine/Operators/ShareReplay.swift※
補足になりますが
ConnectablePublisher
プロトコルには
autoconnect
というメソッドもありこれを呼び出した場合は
subscribeすると自動で処理を開始して値の出力を始めます。https://developer.apple.com/documentation/combine/connectablepublisher/3235788-autoconnect
Future
少し形が違いますが
share
やmulticast
と同様に
処理の結果を複数のSubscriber
で共有できます。https://developer.apple.com/documentation/combine/future
他と違う特徴としては
処理が実行されるタイミングが初めてSubscribeされたタイミング
ではなく
Future
インスタンスが生成されたタイミングになります。
下記の例を見ていきます。
var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { promise in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { promise(.failure(error)) } }, receiveValue: { promise(.success($0)) }).store(in: &cancellables) } // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (14676 bytes) shared: receive finished上記のようにsubscribeしていない状態でも
クロージャ内の処理が実行されていることがわかります。ではsubscribeしてみます。
var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { promise in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { promise(.failure(error)) } }, receiveValue: { promise(.success($0)) }).store(in: &cancellables) } print("subscribe 1回目") future .sink(receiveCompletion: { print("subscription1 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") future .sink(receiveCompletion: { print("subscription2 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 1回目 subscribe 2回目 shared: receive value: (14676 bytes) subscription1 receiveValue: '14676 bytes' subscription1 receiveCompletion: 'finished' subscription2 receiveValue: '14676 bytes' subscription2 receiveCompletion: 'finished' shared: receive finishedこのように
Future
内の処理は1回しか行われていませんが
2つのSubscriber
はどちらも値を受け取ることができています。では処理が完了した後に
subscribeした場合はどうでしょうか?var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { fulfill in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { fulfill(.failure(error)) } }, receiveValue: { fulfill(.success($0)) }).store(in: &cancellables) } print("subscribe 1回目") future .sink(receiveCompletion: { print("subscription1 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("subscribe 2回目") future .sink(receiveCompletion: { print("subscription2 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 1回目 shared: receive value: (14676 bytes) subscription1 receiveValue: '14676 bytes' subscription1 receiveCompletion: 'finished' shared: receive finished subscribe 2回目 subscription2 receiveValue: '14676 bytes' subscription2 receiveCompletion: 'finished'上記のように2回目のsubscribeでも
値を受け取れていることがわかりました。
Future
の注意点としては
処理が実行されるタイミングが
Future
インタンスが生成された時点になりますので
subscribeされた後に必要な処理などがある場合は
予期せぬ挙動に遭遇する可能性があります。まとめ
出力結果を共有できるメソッドやクラスについて見ていきました。
メモリの使用量や通信量を抑えることができるという点で
非常に有用なものですがいつPublisherは処理を開始して値を出力し始めるのか?
いつsubscribeすると値を受け取ることができるのか?といったことを知らないと
「何が起きているんだ。。。?」
と悩んでしまうような事象に出くわすかもしれません。そのような自体にならないためにも
違いを比較して見ていくことは大切だなと感じています。Combine.frameworkはメソッドやクラスがたくさんあり
全てをは把握することは大変ですが
ある分類に分けて少しずつみていくと
効率的に把握できるのかなと思います。?もし間違いなどございましたらご指摘いただけますと助かります??♂️
- 投稿日:2019-10-24T12:35:15+09:00
iOS 13以上でサイレントプッシュのdidReceiveRemoteNotificationが呼ばれない問題
最近、個人的な都合で英語生活してるので、本記事も両言語で書いてみます。と言っても大した量じゃありませんが。
I'd like to write this article in both language Japanese and English due to personal reasons these days.
日本語版
didReceiveRemoteNotificationメソッドがiOS 13以上で呼ばれない理由
特定の手続きをしないと、iOS 13以上でサイレントプッシュを使っている時にAppDelegateのdidReceiveRemoteNotificationが呼ばれないことがわかりました。
AppDelegate.swiftfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("PUSH arrived.") completionHandler(.noData) }サーバー側でヘッダーに
apns-push-type: background
を付けるか、もしくは以下のようにペイロードJSONのaps
部分に空のalert
を付けると、無事呼ばれるようになりました。body.json{ "aps": { "alert": {}, "content-available": 1 } }WWDC2019で発表された正式な方法らしい前者が理想ですが、もしサーバーがカスタムヘッダーをサポートしていない場合は後者のほうが簡単です。
参考文献
- iOS 13 beta 7 Silent Push Bug |Apple Developer Forums
- iOS 13 and Xcode 11 Changes That Affect Push Notifications
- APNS Priority must be set to 5 for background notifications
English ver.
The reason your didReceiveRemoteNotification method won't work on iOS 13 or above
I found that didReceiveRemoteNotification method on AppDelegate without a specific procedure won't be called if you use a background push on iOS 13 or above.
AppDelegate.swiftfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("PUSH arrived.") completionHandler(.noData) }The solution I finally found is adding
apns-push-type: background
to your header field, or adding an emptyalert
field in theaps
section in your custom payload as follows.body.json{ "aps": { "alert": {}, "content-available": 1 } }The former is ideal because it's an official way that was revealed in WWDC2019, but the latter would be easier if your server doesn't support any customizations of push headers.
References
- iOS 13 beta 7 Silent Push Bug |Apple Developer Forums
- iOS 13 and Xcode 11 Changes That Affect Push Notifications
- APNS Priority must be set to 5 for background notifications
- 投稿日:2019-10-24T02:04:04+09:00
iOS アプリからニフクラに現在時刻を問い合わせる
はじめに
ニフクラを利用した iOS アプリを開発していた際に、ニフクラのデータストアのレコードに対する読み出し・書き込みの処理に現在時刻を使用する必要があり、実装方法を検討しました。
端末側で取得する「現在時刻」は当てにならない
iOS デバイス側では時刻を任意に設定できてしまうため、iOS アプリ側で次のようにして時刻を取得すると、実際の現在時刻とは異なる時刻を現在時刻として扱ってしまう可能性があります。
// 端末の時間を任意に設定できるため // 本当の現在時刻が取得できるとは限らない let date = Date()ニフクラ側で現在時刻を取得する
ニフクラ側から現在時刻を返してもらえるような機能1が用意されているか少し調べてみましたが見つかりませんでした2。そこで今回は、ニフクラのスクリプト機能により iOS アプリからニフクラに現在時刻を問い合わせるという方法をとることにしました。
現在時刻を返すスクリプト
ニフクラから現在時刻を返すためのスクリプトの例を以下に示します。
getCurrentTime.jsmodule.exports = function (req, res) { res.status(200).json(new Date()); }このスクリプトをニフクラのコンソールからアップロードします。アップロード時に選択するメソッドは
GET
にしておくものとして、以下でこのスクリプトをアプリ側から実行する方法について説明します。iOS アプリからスクリプトを実行する
上に示したスクリプト
getCurrentTime.js
を iOS アプリから実行する方法を説明します。iOSアプリの開発環境は以下のとおりです。
- Xcode v11.1 (11A1027)
- ニフクラ mobile backend iOS SDK v3.0.23
ニフクラが提供する SDK を使ってスクリプトを実行し、スクリプトが返す値をデコードすればよいのですが、デコードする際には注意が必要です。
JSONDecoder
はデフォルトではyyyy-MM-dd'T'HH:mm:ssZ
形式の日時を扱うことを前提としているらしく、ニフクラが返すyyyy-MM-dd'T'HH:mm:ss.SSSZ
形式(ミリ秒が含まれている)の日時をデコードすることができません。以下のようにして、想定している日付フォーマットを
JSONDecoder
クラスのdateDecodingStrategy
に指定してからデコードすると、期待どおりの結果が得られるようになります。let script = NCMBScript(name: "getCurrentTime.js", method: .executeWithGetMethod)! script.execute(nil, headers: nil, queries: nil) { data, error in if let error = error { // TODO: エラー処理 return } guard let data = data else { // TODO: エラー処理 return } // デフォルトのJSONDecoderでは // ニフクラが返す日時情報をデコードできないため、 // dateDecodingStrategyの設定が必要 let decoder = JSONDecoder() let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" decoder.dateDecodingStrategy = .formatted(formatter) do { let date = try decoder.decode(Date.self, from: data) // TODO: 得られた現在時刻を利用する } catch { // TODO: エラー処理 } }
Cloud Firestore で提供されている
serverTimestamp()
のようなAPIがあるのが理想的でした。 ↩古い情報ですが、ニフクラのユーザコミュニティでもサーバ側の現在時刻を取得する方法について言及がありました。そこでは、サーバ時間の取得方法として、データストアで適当なデータを作成して
createDate
やupdateDate
の値を参照する方法が提案されていました。
https://github.com/NIFCloud-mbaas/UserCommunity/issues/260 ↩ニフクラ mobile backend Swift SDK も提供されていますが、そちらは Deployment Target が iOS 12.0 のため、今回はより古い OS バージョンを対象とするアプリでも利用可能は ニフクラ mobile backend iOS SDK の方を使用しています。(将来的には Swift SDK の方も使っていきたいと思っています。) ↩