ねこ島

iOSのこととか、日常について雑に書きます

GoogleMaps+CarthageをXcodeGenで扱う

いつの間にかGoogleMaps SDKをCarthage経由でいれられるようになっていました。
個人的にCocoaPodsで管理するライブラリを減らしたいと思っていたところなのでこれは助かりますね。

developers.google.com

上記リンクに書いてあるとおりにプロジェクトファイルをいじればOKなのですが、XcodeGenを使っている場合はプロジェクトファイルではなく設定ファイルを書き換える必要があります。

追記した内容としてはこんな形です。

targets:
  HogeHogeApp:
    type: application
    platform: iOS
    sources:
      - path: HogeHogeApp
      - path: Carthage/Build/iOS/GoogleMaps.framework/Resources/GoogleMaps.bundle
        optional: true
        type: folder
    dependencies:
      - carthage: GoogleMaps
        embed: false
      - carthage: GoogleMapsBase
        embed: false
      - carthage: GoogleMapsCore
        embed: false
      - sdk: libc++.tbd
      - sdk: libz.tbd
    settings:
      base:
        OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC

それぞれ追記した内容の説明。

1. GoogleMaps, GoogleMapsBase, GoogleMapsCoreのリンク

    dependencies:
      - carthage: GoogleMaps
        embed: false
      - carthage: GoogleMapsBase
        embed: false
      - carthage: GoogleMapsCore
        embed: false

Carthageでビルドするとこれらのフレームワークを吐き出すので、dependenciesに指定します。
プレミアムプランを利用している場合はGoogleMapsM4Bの追加も必要です。

2. libc++.tbd, libz.tbdのリンク

    dependencies:
      ...
      - sdk: libc++.tbd
      - sdk: libz.tbd

依存関係の解決として必要になるのでこれも指定します。

3. Other Linker FlagsにObjCを追加

    settings:
      base:
        OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC

ドキュメントに記載があったのでそのまま追加。

4. GoogleMaps.bundleをワークスペースに追加

    sources:
      - path: HogeHogeApp
      - path: Carthage/Build/iOS/GoogleMaps.framework/Resources/GoogleMaps.bundle
        optional: true
        type: folder

こちらもドキュメントに記載がありますが、この作業を行わないとビルドは通っても起動時にGoogle Maps SDKが例外を吐いて落っこちますので必ずやりましょう。

iOS14から追加された「App Attest API」でアプリの不正使用を防ぐ

引き続きiOS14ネタとなります。

今回は、DeviceCheckフレームワークに新たに追加された「App Attest API」についてみていきます。

本記事は公開済みのドキュメントを元に作成しています。今後仕様変更などにより記載内容と異なる場合があります。予めご了承ください。

App Attest APIとはなにか

App Attest APIは雑にいうとアプリの不正使用をサーバー側で防ぐAPIです。

Appleバイスが生成した証明書とサービスアカウントの紐づけを行い、サービス利用時にそのAppleバイスだけが持つ秘密鍵で署名したデータをサーバー側で検証することで、サーバーへアクセスするデバイスを確実にできます。

また検証の際に署名やApp IDの確認が行われるため、サイドローディングされたアプリや改造アプリに対しても効果を発揮します。

サイドローディング... ストア(App Store)を経由せずにアプリを入手、インストールすること。iOSではデベロッパーアカウントを使って再署名することで可能となる。Androidでいう野良アプリのインストール。

さらにAppleが公開しているREST APIを呼ぶことで、不正行為を行っている可能性を評価したパラメータが取得できます(この評価はデバイス上の一意の証明書のおおよその数を元にしているようです)。

ただしJailbreakによってiOS自体に改変が加えられている場合、確実に不正使用を防げるわけではないため、不正対策の一環として考えておいた方が良いです。

どういったときに使えるのか

以下のようなサーバー側で制御ができるコンテンツに対して効果的です。

  • 課金した人が使えるコンテンツへのアクセス
  • オンラインゲーム

また実際に不正が確認された場合、DeivceCheckのAPIを使うことでより強力なハードウェアBANを行うことができます。

詳しくはこちら

qiita.com

どのような仕組みか

f:id:nekowen:20200810204436p:plain

ほぼWebAuthnの仕組みに沿っており(といってもWebAuthnを知ったのがつい最近なので違っていたら教えて欲しいです‥🙇‍♂️)

流れとしてはこんな形です。

  1. 初回起動またはアカウント作成時にキーペアを生成し、サーバーサイドに送ります。
  2. サーバー側は送られてきたデータから検証を行い、問題なければ公開鍵とレシートをユーザー情報と紐付けます。
  3. クライアントは認可が必要なAPIにアクセスするタイミングで署名を行ったデータを一緒に送信します。
  4. サーバー側は送られてきたデータから検証を行い、問題なければ通信成功とします

実装方法(クライアント)

1. サポートされているかどうかの確認

まずはAPIが使えるデバイスかどうかを確認する必要があります。

サポートされているかどうかはDCAppAttestServiceのisSupportedを確認します

let service = DCAppAttestService.shared
if service.isSupported {
    // サポートされている
}

2. キーペアの生成と検証を行う(インストール・アカウント作成時のみ)

クライアント側でgenerateKey(completionHandler:)を呼びキーペアを生成します。 キーペアは生成と同時にSecure Enclaveに保存されており、アプリ側から読み取ったり変更はできません。

keyIdが引数に渡されますが、これは生成されたキーペアに紐づいており、今後署名するときに必ず使うのでキーチェーン、あるいはファイルに保存します。

service.generateKey { keyId, error in
    guard error == nil else {
        //  Error
        return
    }
    
    //  Save KeyId
}

次に、サーバーからChallengeを受け取り、クライアント側でSHA256のハッシュ値を生成します。

let hash = Data(SHA256.hash(data: fromServerData))

keyId・ハッシュ値が揃ったら、attestKey(_:clientDataHash:completionHandler:)を呼びキーペアが有効であることをAppleに検証してもらいます。

成功するとDate型のAttestation Objectが渡ってくるのでこれをサーバーに送ります。

service.attestKey(keyId, clientDataHash: hash) { attestation, error in
    guard error == nil else {
        return
    }
    
    let attestationBase64 = attestation.base64EncodedString()
    // Base64に変換するかバイナリとしてそのままサーバーへ送る
}

※ちなみに、iOS14 beta 3まではこのメソッドを呼ぶと必ずエラーが帰ってきていました。beta 4にアップデートしたところ改善したので古いバージョンをお使いの場合は注意してください

サーバー側は送られたAttestation Objectを検証し、問題がなければ公開鍵とレシートをアカウントに紐づけてキーペアの生成は完了です。

3. アプリの有効性を検証する

全ての通信、あるいはビジネス上不正が起こっては困る通信に対してクライアント側で署名を行ったオブジェクトを送信します。

署名にはgenerateAssertion(_:clientDataHash:completionHandler:)を呼びます。

service.generateAssertion(keyId, clientDataHash: hash) { assertion, error in
    guard error == nil else {
        return
    }
    
    let assertionBase64 = assertion.base64EncodedString()
    // Base64に変換するかバイナリとしてそのままサーバーへ送る
}

ここで得られたAssertion Objectをサーバーに送ります。 サーバー側は送られたAssertion Objectを検証し、問題がなければ処理を進められます。

サーバー側の実装・仕様はAppleのドキュメントがあるのでこちらをどうぞ。

https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server

参考

https://developer.apple.com/documentation/devicecheck

https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity

engineering.mercari.com

iOS14で戻るボタンに長押しアクションが追加された

iOS14では、UINavigationBarの戻るボタンを長押しすることでスタックされているViewControllerの一覧がメニューとして自動的に表示されるようになります。

f:id:nekowen:20200802004222p:plain

iOS13までは戻るボタンを押すと、一つ前の画面にしか戻ることができませんでしたが、この機能により、ユーザーが戻りたい画面を自由に選択することができるようになりました。

メニューに表示されるタイトルは、以下に設定されているタイトルから自動で選ばれます。

  • .backBarButtonItem.title
  • .backButtonTitle
  • .title

何も設定されていない場合、「戻る」がメニューに表示されます。

また現状この機能は無効にすることができませんので、戻るボタンを自作していない限りは必ず表示されます。

既存アプリの対応について

特別な対応をする必要はほとんどありませんが、アプリによってはいくつか確認すべき点がありました。

titleViewを使っていないか?

titleに直接タイトルを設定せず、代わりにtitleViewを使用しているケースがあると思いますが、この場合メニューにタイトルが設定されません(戻るになる)。

WWDCのセッションでは、この場合backButtonTitleの使用を検討して欲しいと説明されています。

backButtonTitleはiOS11からのプロパティとなるため、iOS10以下をサポートしている場合はbackBarButtonItemを使用しましょう。

戻れてはいけない画面がメニューに表示されていないか?

少なくとも僕の周りでこのような実装をしているアプリは見たことがないのでレアケースだと思うのですが、いろんな事情で特定の画面に戻れないように実装している場合。

コード的にはhidesBackButton = trueが存在している場合です。

戻るボタンを隠していたとしても、その先の画面で戻るボタンが表示される機会があり尚且つスタックに積まれているとメニューから戻れてしまいます。

その場合はOSが提供する戻るボタンを使用しないか、UINavigationControllerのviewControllersから直接Viewを取り除くという荒技がありますが、推奨しません。

最後に

上記に当てはまらない場合でもQAは必須です。タイトルがおかしくなっていないか?想定しない画面は入ってないか?この辺りは最低限見たほうが良いです。

画面数が多いと大変ですが、予期せぬ不具合を起こさないためにもやっていきましょう。

VRで遊ぶときやヘッドホン装着時にお勧めのメガネ「Short Temple」を買った

僕は普段メガネをしているのだが、長時間ヘッドホンをしていると耳が痛くなってしまう。

耳にひっかけているテンプルがヘッドホンによって押さえつけられてしまうことが原因なのだが、メガネ着用者あるあるだと思う。

この問題を解決する方法としてはコンタクトにするか、メガネを斜めにかけるか、そもそもヘッドホンを使用しないなど…いろいろあるのだが、どれも決定打に欠ける。

もうちょっと快適なヘッドホン生活ができないかと調べていたら、こんな商品をみつけた。

www.jins.com

普通のメガネに比べてテンプルが短いのが特徴。

短いと当然耳には引っかけられないので、こめかみに挟んでメガネを固定している。なるほど、確かにこれならヘッドホンと被らないし、使えるかも!?と思い購入してみた。

普通のメガネに比べると小さいし、おもちゃか?と思ったのだが、装着すると案外悪くない。

ちゃんと固定できているし、下を向いても落っこちない。ちょっと踊ってみても落ちない。

ただ、人を選ぶと思う。

特に頭が大きい人は押さえつけられている感じがきついかもしれない。

あと、このメガネを汎用的に使おうと考えている人もきついかもしれない。

いくらこめかみで固定しているとはいえ、安定性はノーマルなメガネのほうが100倍良いので、このメガネをかけたまま走ったりするとこのホラーゲーム並みにメガネを落とすことになる

store.steampowered.com

www.youtube.com

僕はヘッドホン、VRで遊ぶ時のみこのメガネを使っており、逆にそれ以外で今まで使っていたメガネを着用している。

ということで、ヘッドホンをしていて耳が痛くなる人、VRやっててメガネきつい!ってなってる人、「Short Temple」おすすめですよ!

1Passwordのプラン変更の罠にハマった

1Passwordの個人アカウントをファミリープランに変更しようとしたら詰まりかけた。

TL;DR

  • 1Passwordでファミリープランを使う予定があるならアプリ内課金で支払いしちゃダメ
  • ちゃんとクレジットカード登録しよう
  • 1Passwordのサポートはめちゃくちゃ返信が早くて良いぞ

何が起きたのか

前から1Passwordの個人アカウントを使い続けていたのですが、ある日ヨッメが1Passwordを使いたいと言い出したことから始まった。

個人アカウントは名前の通り個人でしか使えず、例えば他の人を招待して使おうとすると、ファミリープランあるいはビジネスプランに変更する必要がある。

で、今回は家族で使うためファミリープランに変更しようとしたところ…そのような項目がない。

支払い項目を見ても、「サブスクリプションがアクティブです」とあるだけで変更ができない状態となっており、どうなっとんじゃとHelpを見るとこんな一文が

support.1password.com

If you started your subscription with an in-app purchase, you won’t see the option to invite people. For help upgrading your account, contact 1Password Support.

「アプリ内課金から定期購読(サブスクリプション)を始めた場合、招待のオプションが出ないよ!アップグレードしたい場合はサポートに連絡してね!」

なん…だと?

そういえば1Passwordを始める時、アプリ内課金の方がカード支払いより安かったのでそっちを選んだんだっけな…orz

サポートに連絡

11時ごろ

とりあえずここから「I want to upgrade to 1Password Families」を選択。名前とメアドを入力して、詳しい内容を記載して送った。

support.1password.com

すると1分ぐらい後に自動メールがきた。 内容は、

「1Password Familiesに変更したいときは以下の手順を踏んでね! もしアプリ内課金で定期購読を始めた場合はこのメールに返信してね!」

とのことだったので、↑で書いた内容をそのまま返信。

15時ごろ

メールが返ってきた。内容としては、

「君のアカウントではアプリ内課金を使ってるから、サブスクリプションをキャンセルして1Passwordの決済システムに移行する必要があるよ!その際一瞬アカウントが凍結するけど、サインインして1Password Familiesに変更することによって復活できるので安心してね。 理解したら、iTunesから1Passwordのサブスクリプションをキャンセルして連絡をくれよな!」

とのことだったので、AppStore→アカウント→サブスクリプションから1PasswordのAnnual Subscriptionをキャンセル。 そして対応したことを伝える。

20時ごろ

1Passwordからクレジットが追加されたメールと試用期間が終了したというメールが来る。 元々年払いをしていてまだ期限まで残っていたので、その分をクレジットとして追加してもらえたようだ。

サポートからもメールが来ていて、

「アプリ内課金で残っていた分をクレジットに追加したよ! 1Passwordにサインインして、招待からファミリーアカウントにアップグレードしてくれよな!」

とあったので1Passwordにサインインするとサイドバーに招待が追加されている状態となっていた。 そして無事にファミリーアカウントへ変換できましたとさ。

所感

アプリ内課金は楽だけどこういう罠があるんだなーということを思い知りました。

あとこの大変な時期にも関わらず、サポートの返信が爆速で本当に助かりました。

上記には書いていないのですが、返信メールの最初の方に、「大変な時期ですが、あなたとあなたの周りの人が元気でいられることを願っています」の一文があってすごい配慮だなと思いました。

ありがとう1Passwordの中の人。

XcodeGenでEmbedded Frameworkを扱う

今日初めてEmbedded Frameworkを使おうとしたのですが、XcodeGenと組み合わせた時どうするんだっけ?となったのでメモとして残しておきます。

Embedded Frameworkとは

直訳すると「埋め込みフレームワーク」となるEmbedded Frameworkは、アプリのコードの一部をFrameworkとして扱うことができます。

この機能を使う利点は複数あり、

  • 必要なFrameworkだけビルドを行うのでビルドの高速化が期待できる
  • AppExtensionとコードを共有できる
  • 名前空間が分かれるため、テストを意識したコードがかける(はず)

と、アプリ開発する上で最初に導入しておくと後々効いてきそうだと思いました。

XcodeGenと組み合わせる

Embedded FrameworkはXcodeのTargetから追加できますが、前述の通りXcodeGenでプロジェクトデータを管理しているため、FrameworkもXcodeGenで管理の対象とする必要があります。

なので、project.ymlをこんな感じにしました。

name: TestApp
options:
  bundleIdPrefix: com.example
targetTemplates:
  EmbeddedFramework:
    platform: iOS
    type: framework
    sources:
      - path: ${target_name}
  EmbeddedFrameworkTests:
    type: bundle.unit-test
    platform: iOS
    dependencies:
      - target: TestApp
    sources:
      - path: ${target_name}
targets:
  TestApp:
    type: application
    platform: iOS
    sources:
      - path: TestApp
    dependencies:
      - target: TestAppData
      - target: TestAppDomain
      - target: TestAppExtension
  TestAppTests:
    templates:
      - EmbeddedFrameworkTests
  TestAppData:
    templates: 
      - EmbeddedFramework
  TestAppDataTests:
    templates:
      - EmbeddedFrameworkTests
  TestAppDomain:
    templates: 
      - EmbeddedFramework
  TestAppDomainTests:
    templates:
      - EmbeddedFrameworkTests
  TestAppExtension:
    templates: 
      - EmbeddedFramework
  TestAppExtensionTests:
    templates:
      - EmbeddedFrameworkTests
  ...

ここではFrameworkをData, Domain, Extension…と分けており、それぞれ同じBuild Settingsを使いたいので、targetTemplatesを使って共通で使えるようにしています。

参考

github.com techlife.cookpad.com