20191024のiOSに関する記事は9件です。

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がリーダ機として扱われます。

IMG_20802.JPG

今回のリリースであるNFC Automation Triggerは、このNFCを読み込むと自動的に特定のアクションをTriggerすることができます。
例えば、特定のNFCタグをiPhoneが読み取ると、3分のタイマーが起動する!とか。ま、表題の件を用意したんですけどね!(笑)

とりあえずこんなの作ったよ!

登録しているNFCタグをiPhoneが読み取ると、扉に設置しているスマートロックアイテムSESAMEのSESAME APIをRequestして扉の鍵が解錠されます。

スクリーンショット 2019-10-24 20.07.20.png
↓ 白色のNFCタグにスマホを近づけると...?
スクリーンショット 2019-10-24 20.08.24.png
NFC Automation Triggerが作動!解錠!

https://twitter.com/silver_birder/status/1187016726363299840

作成方法は、とってもかんたんです。iPhone標準アプリshortcutと、NFCタグ(1枚94円)があれば誰でも作れます。もちろん、SESAMEが必要ですけどね(笑)。

shortcutで、次の準備をしました。

  1. RESTfulAPIのショートカットを使ってSESAME APIを設定する。
  2. 個人用オートメーションを作成し、用意したNFCタグをスキャンする。
  3. 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という機能は、珍しくありません。
しかし、とても使いやすくカスタマイズ性が高いため、アイデア次第でいくらでも化けれます。
一攫千金?を目指すのも良いですが、やっぱりライフハックをつきつめたいなと思う私でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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"} を指定すること
  • --messageAPNS_VOIP_SANDBOX キーに aps を含むデータを入れること

本番環境の例

基本的に開発用の場合と同じ、ただし APNS_VOIP_SANDBOXAPNS_VOIP に変更すること。

要調査

  • 東京リージョンに登録した場合にcliで送信できない件、仕様なのか手順ミスなのか

参考情報

https://docs.aws.amazon.com/sns/latest/dg/sns-send-custom-platform-specific-payloads-mobile-devices.html#mobile-push-send-message-apns-background-notification

https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BitriseのOrganaizationでAppleとの連携が表示されてなくて超絶焦った話

やりたいこと

BitriseではApple Developer Accountと連携することで、iOSのProvisioning Profileの管理を簡単にできる!
https://devcenter.bitrise.io/jp/getting-started/connecting-apple-dev-account/

年に1回更新が発生して面倒な作業ですが、Bitriseの神対応によって楽に実施ができる!
すごい!これはやるしかない!さぁやるぞ!

早速つまづいた...。

あれ...?Organaizationのページだと連携のパネルが表示されていないぞ?
スクリーンショット 2019-10-24 14.41.28.png

個人のページだと表示されているぞ...?
スクリーンショット 2019-10-24 14.41.06.png

解決策?

そもそも仕様で、Organaizationに所属しているいずれかの人の認証情報を使用するようです。
個人のアカウントがApple Developer Accountと連携していれば、プロジェクトのTeamタブから選択できるようです!

スクリーンショット 2019-10-24 14.55.05.png

確かに表示されて選択できるようになってる!
名称未設定.png

Organaizationはチームに所属しているユーザの情報を利用できるんですね!
個人開発している人とかは迂闊にアカウント登録すると、Organaizationに入った時にややこしくなりそうです!
みなさん気をつけましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BitriseのOrganaizationでAppleとの連携が表示されてなくて超絶ハマった話

やりたいこと

BitriseではApple Developer Accountと連携することで、iOSのProvisioning Profileの管理を簡単にできる!
https://devcenter.bitrise.io/jp/getting-started/connecting-apple-dev-account/

年に1回更新が発生して面倒な作業ですが、Bitriseの神対応によって楽に実施ができる!
すごい!これはやるしかない!さぁやるぞ!

早速つまづいた...。

あれ...?Organaizationのページだと連携のパネルが表示されていないぞ?
スクリーンショット 2019-10-24 14.41.28.png

個人のページだと表示されているぞ...?
スクリーンショット 2019-10-24 14.41.06.png

解決策?

そもそも仕様で、Organaizationに所属しているいずれかの人の認証情報を使用するようです。
個人のアカウントがApple Developer Accountと連携していれば、プロジェクトのTeamタブから選択できるようです!

スクリーンショット 2019-10-24 14.55.05.png

確かに表示されて選択できるようになってる!
名称未設定.png

Organaizationはチームに所属しているユーザの情報を利用できるんですね!
個人開発している人とかは迂闊にアカウント登録すると、Organaizationに入った時にややこしくなりそうです!
みなさん気をつけましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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を呼び出して初めてPublisherSubscriberを受け取りを処理を開始します。
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

少し形が違いますが
sharemulticastと同様に
処理の結果を複数の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はメソッドやクラスがたくさんあり
全てをは把握することは大変ですが
ある分類に分けて少しずつみていくと
効率的に把握できるのかなと思います。?

もし間違いなどございましたらご指摘いただけますと助かります??‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift
func 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で発表された正式な方法らしい前者が理想ですが、もしサーバーがカスタムヘッダーをサポートしていない場合は後者のほうが簡単です。

参考文献

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.swift
func 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 empty alert field in the aps 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS アプリからニフクラに現在時刻を問い合わせる

はじめに

ニフクラを利用した iOS アプリを開発していた際に、ニフクラのデータストアのレコードに対する読み出し・書き込みの処理に現在時刻を使用する必要があり、実装方法を検討しました。

端末側で取得する「現在時刻」は当てにならない

iOS デバイス側では時刻を任意に設定できてしまうため、iOS アプリ側で次のようにして時刻を取得すると、実際の現在時刻とは異なる時刻を現在時刻として扱ってしまう可能性があります。

// 端末の時間を任意に設定できるため
// 本当の現在時刻が取得できるとは限らない
let date = Date()

ニフクラ側で現在時刻を取得する

ニフクラ側から現在時刻を返してもらえるような機能1が用意されているか少し調べてみましたが見つかりませんでした2。そこで今回は、ニフクラのスクリプト機能により iOS アプリからニフクラに現在時刻を問い合わせるという方法をとることにしました。

現在時刻を返すスクリプト

ニフクラから現在時刻を返すためのスクリプトの例を以下に示します。

getCurrentTime.js
module.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: エラー処理
    }
}

  1. Cloud Firestore で提供されている serverTimestamp() のようなAPIがあるのが理想的でした。 

  2. 古い情報ですが、ニフクラのユーザコミュニティでもサーバ側の現在時刻を取得する方法について言及がありました。そこでは、サーバ時間の取得方法として、データストアで適当なデータを作成して createDateupdateDate の値を参照する方法が提案されていました。
    https://github.com/NIFCloud-mbaas/UserCommunity/issues/260 

  3. ニフクラ mobile backend Swift SDK も提供されていますが、そちらは Deployment Target が iOS 12.0 のため、今回はより古い OS バージョンを対象とするアプリでも利用可能は ニフクラ mobile backend iOS SDK の方を使用しています。(将来的には Swift SDK の方も使っていきたいと思っています。) 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む