kumamotone’s blog

iOS/Android アプリエンジニアです https://twitter.com/kumamo_tone

FlutterKaigi 2021 Day 1 #FlutterKaigi

YouTubeアーカイブがある(神)

www.youtube.com

一通り見たが以下の2本がとくに参考になった。

  • FlutterでのMaterial You対応の状況と今後の展望
  • Flutterにおける過不足のないセキュリティの実装

FlutterでのMaterial You対応の状況と今後の展望

speakerdeck.com

  • Material Design 2と3の違い
    • 2はブランドごと、3はユーザーごとのカスタマイズが可能になった(Material You)

Color Schemeがどう変わったか

f:id:kumamotone:20211129224806p:plain

大変そうだがKey Colorsだけ5色とりあえず決めてそこから機械的に生成すれば良い

f:id:kumamotone:20211129225235p:plain

Key Colorを決めて

f:id:kumamotone:20211129231214p:plain

ユーザーが設定している壁紙から5つのKey Colorsを抽出できる(特定のメーカーのAndroid 12端末のみ)

アプリ全体に適用する必要はなく、プロフィール画面やアカウント画面など、部分的に適用するのもアリ

f:id:kumamotone:20211129231452p:plain

Buttonに新しいタイプが加わり、形もちょっと変わった

f:id:kumamotone:20211129231535p:plain

FABがかわいくなった うれしい

f:id:kumamotone:20211129231615p:plain

2021年11月29日現在、stableにまだ全然取り込まれてない←Oh....

ThemeDataにMaterial3をOptinするためのフラグuseMaterial3は追加されていて、これをtrueにすると、Material3対応が完了しているものは3になる

f:id:kumamotone:20211129231924p:plain

Material2のものがMaterial3になるとどうなるかというのは決められているので、予め3の想定で作っておけば、dart fixコマンドであとで一括修正できる予定

色の生成にはmaterial-theme-builderが使える Androidxml ではすでに出力できる Flutter もできると嬉しい

material-foundation.github.io

Flutterにおける過不足のないセキュリティの実装

これ使えばKeyStoreに保存してくれるっぽい

pub.dev

Windows. Linux, macOS, Web の実装もある すご

若干ややこしいpath_providerの使い方も説明してくれている

apkの難読化は以下打てばできる

f:id:kumamotone:20211129231006p:plain

M1 Mac(Big Sur)にHoRNDISをインストールする

MacAndroid のUSBテザリングを使うために必要なカーネル拡張HoRNDISをインストールするのに、GitHubのissueに書いてある方法を色々試しに試し再起動に再起動を重ね、時間を食ってしまった。うまくいった方法だけここに書き残しておく。

github.com

結論からいうと thpryrchn 氏の方法でインストールできた。

1. リカバリモードに入る

リカバリモードに入る方法は、起動時にcmd+Rを押しっぱなしにする方法ではなく、Touch IDを長押しする方法に変わったっぽい。表面にふれるだけじゃなくてガッツリ押し込む必要がある。

終了している最中に電源ボタン長押しは不安になるので再起動じゃなくてシステム終了にしたほうがいい。

2. カーネル拡張を無効にする

リカバリモードに入ったら optionsを選択して、メニューバーのユーティリティからターミナルを起動する。

csrutil enable --without kext

そのままrebootと打ち込んで再起動する。

3. HoRNDISをインストール

git clone https://github.com/thpryrchn/HoRNDIS.git -b BigSur

thpryrchn氏のリポジトリからソースコードを取得して make する。

build/pkg に HoRNDIS-kext.pkg ができるのでダブルクリックしてインストールする。

このタイミングでセキュリティとプライバシーが何かダイアログを出してきたらうまくいっている。そこから再起動して、テザリングする端末を繋げば、自動的に接続されている。

f:id:kumamotone:20210724103809p:plain

Wi-Fiアクセスポイントの方はエコのため切っておくと良さそう。USBテザリングのすぐ上のメニューから変更できる。

Skebの中途採用に「求める条件」の感想

Skebの中途採用に関するツイートを見かけた。

自分はSkebに対しては素晴らしいサービスだと思っていて、クリエイターさんを支援したいという気持ちから生まれたという話も聞いたことがあったので、その点共感していて応援している。

ただ主にこの「推しのVTuberやクリエイターが「いない」方」という部分に関して、文脈的には「アイドルのマネージャーにオタクを採用しない方がトラブルにならなくて良い」みたいな話で、意味のある意図が想像できる一方、クリエイターが好きなエンジニアとして、かなりもやっとしてしまった。

まず1つ目に、クリエイターを支援したいという気持ちから生まれたサービスなのであれば、Skebはクリエイターが好きな、クリエイターを応援したい人を仲間に募集しています、という呼びかけが妥当なのでは?好条件とはいえ、そこまでドライに採用者をただの作業者扱いするようなところで働きたいか?

2つ目に、宗教などと同じで、採用で特定の思想を差別的に扱うのは社会的にNGなのでは?なぜクリエイターのファンというだけで犯罪者予備軍のような扱いを受けなければならないのか?

Skebアカウントはユーザー向けのアカウントで、クリエイターさんが基本的にはお客さんだというところ、また、エッジの効いた文章を入れたことによって拡散されるので、炎上前提の告知としては上手なのかもしれないが、ただ単にあんまり深く考えてないような気もする。

potatotips #70 に参加しました (iOS/Androidブログまとめ) #potatotips

potatotips #70iOS ブログまとめ枠で参加させていただきました(Androidのもまとめました)。

今回はLINE Fukuokaさん主催で、Zoomのウェビナーを使ったオンラインでの開催となりました。

f:id:kumamotone:20200710200640p:plain

自分で見直すときのわかりやすさ重視でガッツリ元資料を引用していますが問題あればご連絡ください。

[iOS] Contributing to XcodeGen / @freddi

XcodeGen という YAMLでxcodeprojを管理できる OSS にコントリビュートした。

f:id:kumamotone:20200710201421p:plain

Swift Packageの依存が、Remoteにあるものしか解決できなかった。これをLocalでもできるようにした。

f:id:kumamotone:20200710201558p:plain

気をつけること、CONTRIBUTING.md というのがあるので読むでからPRする、後方互換性に気をつけるなど。

xcodegenの自動生成の機能もあるけどまだ実装中。

[Android] Jetpack Composeでテキストを装飾する話 / @uecchi

Jetpack Composeでテキストを装飾する方法を紹介。

テキスト全体を修飾

ふつうにTextの引数で設定する。

@Composable
fun StyledGreeting(name: String){
Text(
text = "Hello $name!",
color = Color.Green,
fontSize = 30.sp,
fontFamily = FontFamily.Cursive,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold
)}

テキストの一部を修飾

従来のやつでは Spanned に対応するやつだが、 Annotated String というクラスを使う

@Composable
fun StyledGreeting2(name: String){
val greetingText = annotatedString {
pushStyle(SpanStyle(color = Color.Green))
append ("Hello ")
// 前にpushしたstyleは引き継がれる
pushStyle(SpanStyle(
fontSize = 40.sp,
// Snip fontFamily, fontStyle, fontWeight
fontSynthesis = FontSynthesis.All,
textDecoration = TextDecoration.Underline
append("$name!!")
pop()// 直近でpushしたstyleはpopできる
Text(text =
greetingText, fontSize = 30.sp)

こうなる

f:id:kumamotone:20200710203021p:plain

テキスト内にカスタム画像を表示

inline content というのがある。

@Composable
fun StyledGreeting3(name: String){
val inlineContentId = "InlineContent"
val greetingText = annotatedString {
append("Hello $name!!")
// inline contentを挿入
appendInlineContent(id = inlineContentId, alternateText = ":droid:")
// snip

inlineContent: map<String, InlineTextContent> を Text に渡す

@Composable
fun StyledGreeting3(name: String) {
val inlineContentId = "InlineContent"
val greetingText = // snip
val inlineContent = map0f(
inlineContentId to InlineTextContent(
Placeholder(
width = 30.sp,
height = 30.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
){ alternateText ->
// このlambdaはComposable関数
CustomEmojiImage(imageResId = R.drawable.ic_android_robot)
})
Text(text = greetingText, fontSize = 30.sp, inlineContent = inlineContent)
}
@Composable
fun CustomEmojiImage(imageResId: Int){
Image(
modifier = Modifier.fillMaxSize(),
asset = vectorResource(id = imageResId),
contentScale = ContentScale.FillWidth
)}

こうなる

f:id:kumamotone:20200710203003p:plain

Jetpack Composeはこのようにプレビューが横に表示される。便利

f:id:kumamotone:20200710202859p:plain

サンプルコード

https://github.com/tsuyosh/JetpackComposeTextDemogithub.com

[iOS] ARKit 4.0 / @TachibanaKaoru

speakerdeck.com

f:id:kumamotone:20200710203115p:plain

ARKitの歴史

ARKit4 新機能を紹介していく。

Geo Anchor

f:id:kumamotone:20200710203555p:plain

AppleがMap情報を取得するときに収集した実際の建物の3D情報と、カメラ画像から分析した3D情報と、端末のGPS情報を組み合わせた仕組みで、非常に正確な位置測定が可能。

セカイカメラと同じようなことが簡単に実装できる。

現時点ではサンフランシスコ、シカゴ、マイアミ、ニューヨーク、ロサンゼルスだけで使える。

f:id:kumamotone:20200710203707p:plain

使い方

Depth with LIDAR (Scene Geometry with LIDAR?)

f:id:kumamotone:20200710203800p:plain

新しいiPad Pro だけで使える、センサーから光を照射してその反射光を使って物体までの距離や物体の種類がわかるようになる。

判定可能な物体は天井、ドア、床、椅子、テーブル、壁、窓、その他、の8種類。

3.5でも距離と種類は取得できたのだが、APIが使いやすくなった。

Face Tracking

TrueDepthカメラ搭載のみだったが、A12以上のプロセッサでも使えるようになった。(新しいiPhoneSEとか)

Reality Converter

Reality Converterというのが2020年1月にリリースされて、他のモデリングツールのファイルからUSDZファイルへ簡単に変換できるようになった。

f:id:kumamotone:20200710211458p:plain

次のiPhoneにLiDARが搭載されなかったらTachibanaさんのカシオミニがもらえます

[Android] license-list-pluginを使ってOSSライセンス画面を自動生成する / @syarihu

speakerdeck.com

OSSライセンス画面を手軽に作るためのツール、いくつかある

github.com

以下のような特徴がある。

  • ライセンスリストを自動生成し、yamlに定義ファイルを出力人が読みやすい形なので、ライセンスの追加/変更/削除も簡単にできる
  • oss-license-pluginと違いライセンスリストが隠蔽されていないため、差分も分かりやすい
  • 定義ファイルを元に表示用のhtmlを自動生成したり、カスタマイズした画面を提供するために利用可能なjsonの出力も可能
  • ローカルライブラリにも対応
  • ライセンスリストから除外したいライブラリがある場合は.artifactignoreに記述することで簡単に除外可能
  • cookpad/LicenseToolsPluginを使っている場合はcookpadプラグインで利用するlicenses.ymlからの移行も可能

f:id:kumamotone:20200710204930p:plain

json だけでなく HTML の形式でも吐ける。従来のより見た目も良い。

Github Actions に組み込んで自動でライセンス定義を更新することもできる。

聞いた範囲たいへん良さそうだが This plugin is still under development. とのこと

[iOS] Speech framework tips / @tsuzuki817

note にセルフテキストまとめがあるので詳しくはこちら!

note.com

Apple純正の音声認識フレームワーク。ローカル(iOS13〜)とサーバー版がある。

サーバーのAPIの制限は1リクエスト60秒(音声の長さ)。1時間に1000回まで。

NSSpeechRecognitionUsageDescription と NSMicrophoneUsageDescription を Info.plist に入れて使う。

f:id:kumamotone:20200710210028p:plain

共有会って喋って認識させてみた結果。

通話中は録音ができないのでデバッグ時には注意。

[Flutter] Creating Flutter Animations with Rive

docs.rive.app

Rive は Flutterで使えるアニメーションツール。前まで flare という名前だった。ツール flr ファイルを読み込んで使うだけ。

フリー版でも機能が使えるが、作ったファイル public にしなければならない。 private にしたければ、 21ドル/月か、年間プランだと14ドル/月。

iOSには対応しているけどAndroidには対応していない?(調べてない)

[iOS] UICollectionView iOS14 / @shiz

speakerdeck.com

iOS14 での UICollectionView の更新について。

SectionSnapShot

f:id:kumamotone:20200710211810p:plain

f:id:kumamotone:20200710211828p:plain

SectionSnapShot というのが追加された。セクションごとの折りたたみの状態を保存できたり、それのイベントハンドラがあったりする。

あと並び替えのサポートがされた。

Lists

f:id:kumamotone:20200710211949p:plain

UITableViewのような外見で、スワイプできる、デフォルトのレイアウトが用意されている、Compositional Layoutの上に作成できるなどの特徴がある。

Cell(View)の設定

struct で設定できるようになった。

f:id:kumamotone:20200710212413p:plain

いい感じに型を使ってセルの登録もできるようになった(これUITableViewにはないってことなのかな)

より深く理解したい人はAppleの公式サンプルコードがおすすめ。

Qiita にもまとめてくださっている。

qiita.com

[Android] getChangePayload in DiffUtil / @rmakiyama

speakerdeck.com

音声版

DiffUtil に getChangePayload というのがある。これはareItemsTheSameがtrueかつareContentsTheSameがfalseで呼ばれる。

f:id:kumamotone:20200710213204p:plain

変更に関するペイロードをオブジェクトで返せるので、細かい更新の制御が可能。画像の再読み込みが起こらずチラツキが起こらず、スマート。

f:id:kumamotone:20200710213252p:plain

Groupieにも同様の機能が提供されている。

サンプルコード

github.com

[iOS] Swift UIで勘違いした話 / @koher

speakerdeck.com

Stepper に $counter.count を渡すように、 NumberDisplay にも @Binding の $counter.count を渡していたら、コンパイルエラーでうまくいかなかった

というのも、Stepperでは双方向バインディングなんだが、NumberDisplayは単方向バインディングなので、

Viewでは @Binding ではなくシンプルにプロパティを持っておいて、プロパティを渡せば
単方向バインディングは実現できるということがフォーラムに聞いたらわかった 質問するのは大事ですね

[Android] Material Components for Android 1.2.0 and 1.3.0 / @NabeCott

speakerdeck.com

まだ開発中だが Material Components に入りそうな変更について。

f:id:kumamotone:20200710215506p:plain

SliderはSeekBarのMaterial Component版 目盛りの表示やTooltipの表示、範囲の選択などができる

f:id:kumamotone:20200710215529p:plain

ShapeableなImageViewなど

ほかにも複数行のタイトルが表示できるCollapsingToolbarやProgressBarのMaterial Component版、新しいTimePicker(In development)などが入りそう

[iOS] iOSアプリでWebRTC / @SatoHikaruDev

qiita.com

WebRTCのクライアント実装について理解を深めたい。

github.com

サンプルを参考に、どういう処理がされているのか見ていく。接続、SDPのやりとり、映像キャプチャなどについて実際のコードを交えて紹介。

[Android] Adaptive Cardsを使ってみた / @nakasho_dev

www.slideshare.net

adaptivecards.io

Adaptive Cards は、JSONで作成された、プラットフォームに依存しないUIのスニペット。特定のアプリに配信されると、JSONは周囲に自動的に適応するネイティブUIに変換する。Microsoftが推進していて、 Android iOS JavaScript ASP.NET .NET WPF Windows ReactNative などで使えるもよう

Adaptive Card Designer というのがあって、ノンプログラマーでもいい感じに編集できる。

感想

前回に続きリモート開催だったが、運営さんの頑張りなどによって、なんだったらリモートのほうがいい面あるんではと思えるぐらい良い感じだった ありがとうございます

App Clips の短期間通知を有効にする

kumamotone.hatenadiary.jp

上記の続きになります。

App Clips の短期間通知を有効にする

App Clipの中には、価値を提供するためにスケジュールや通知を送る必要があるものもあります。

配達で食べ物を注文できるApp Clipを考えてみましょう。通知を送信することで、App Clipは予定されている配達についてユーザーに通知します。

通知がApp Clipの機能にとって重要な場合は、各起動後最大8時間まで通知をスケジュールまたは受信できるようにします。さらに、複数のビジネス向けにApp Clipを作成する場合は、通知のペイロードに変更を加えるようにしましょう。

一時的な通知の設定

App Clipが起動後、最大8時間まで通知をスケジュールしたり受信したりできるようにするには、まず、App ClipのInfo.plistファイルにDictionary型のNSAppClipキーを追加します。

次に、NSAppClipRequestEphemeralUserNotificationというキーを持つエントリをDictionaryに追加します。このキーの型はBooleanに、値をtrueに設定します。

f:id:kumamotone:20200625065245p:plain
https://developer.apple.com/documentation/app_clips/enabling_notifications_in_app_clips

App Clipの Info.plist に NSAppClipRequestEphemeralUserNotification のエントリを追加すると、App Clipの起動時に表示されるClip Cardには、App Clipが通知を受信したりスケジュールしたりすることができることをユーザーに知らせる追加のnoteが含まれています。

この権限はデフォルトで有効になっていますが、ユーザーはClip Cardのnoteをタップすることで無効にすることができます。

f:id:kumamotone:20200625065309p:plain
https://developer.apple.com/documentation/app_clips/enabling_notifications_in_app_clips

ユーザーはClip Cardで通知を無効にすることができるので、App Clipが通知をスケジュールして受信する権限を持っているかどうかをチェックするコードを追加します。

以下のコードは、ユーザーが短時間の通知送信の許可を与えているかどうかをチェックしています。

let center = UNUserNotificationCenter.current()
center.getNotificationSettings { (settings) in {
    if settings.authorizationStatus == .ephemeral {

        // ユーザーがアプリのクリップカードで通知を無効にしていなかった場合
        // ここにスケジュールや通知を受信するためのコードを追加します
        return
    }
}

通知を送信するための明示的な許可を要求する

App Clipの機能が1日以上に及ぶ場合は、通知を送信するためのユーザーの許可を明示的に要求します。

例えば、レンタカー会社のApp Clipでは、レンタカーを返却する必要があるときにユーザーに通知を送信する許可を求めることができます。

しかし、この許可を求めるべきかどうかは慎重に検討してください。ユーザーはこの要求を拒否して、App Clipが各起動後最大8時間まで通知を受信してスケジュールする能力を上書きしてしまう可能性があります。

通知ペイロードを変更する

複数のビジネス向けのApp Clipを作成することができます。

例えば、飲食店向けのプラットフォームを提供している企業で、多くの異なる飲食店にサービスを提供するApp Clipを作成することができます。

ユーザーが短時間のうちに複数のビジネス向けのApp Clipを連続して起動した場合、App Clipのインスタンスがデバイス上に複数存在することがあります。

この場合、システムは、通知を受信すると、適切なApp Clipのインスタンスに通知をルーティングする必要があります。

その結果、システムは通知のペイロードに URL をtarget content identifierとして含めることを要求します。

次のコードは、複数のビジネスにサービスを提供するApp Clipの通知ペイロードを示しています。

{
    "aps" : {
        "alert" : {
            "title" : "Order Status",
            "subtitle" : "Restaurant A",
            "body" : "Your order is ready."
        },
        "category" : "order_status",
        "target-content-id" : "https://example.com/restaurants/restaurant_a/order/1234"
    }
} 

target-content-id の値は、対応するAdvanced App Clip Experienceに一致するURLである必要があります。

レストランの例では、これらのURLの両方をApp Store Connectに登録します。

呼び出しURLとtarget content identifiersは、次のようになります。

一般的に、可能な限り具体的なtarget content identifiersを使用します。

同様に、ローカル通知をスケジュールするためにApp Clipを有効にする場合は、通知のペイロードにtarget content identifiersを設定します。

詳細については、App Clipsの起動方法の設定Generating a Remote Notification Scheduling a Notification Locally from Your App を参照してください。

出典

Configure and link your app clips

WWDC 2020 のセッション、 Configure and link your app clips に関しては akatsuki174 さんの Qiita にわかりやすくまとまっていたので当ブログは下記の記事を推薦します。

qiita.com

以上

App Clipsの起動方法の設定とテストについて

kumamotone.hatenadiary.jp

上記の続きになります。

デフォルトの起動方法の設定

App Clip は単独でリリースできず、常に対応する本体アプリが必要で、アプリの一部としてApp Store Connectに提出します。App Store Connectでは、新しいアプリバージョンのページで、Default App Clip Experience を設定し、Clip Cardに次のメタデータを提供する必要があります。

  • ヘッダー画像
  • アプリのクリップの詳細情報を提供するサブタイトル
  • アクションボタンをタップしたときに呼ばれるアクション

設計指針については、Human Interface Guidelines を参照してください。

Advanced App Clip Experience

Webサイトの Smart App Banner や Message アプリの共有リンクからの呼び出しは、前述の Default App Clip Experience が呼び出されます。

しかし、NFCタグなどをサポートしたい場合は、 Advanced App Clip Experience を設定する必要があります。呼び出しURLを設定し、App Store Connectで呼び出しと呼び出しURLの両方をAdvanced App Clip Experienceとして設定します。

次のような場合に、Advanced App Clip Experienceを設定します。

  • NFC タグやビジュアルコードからの呼び出しを含む、すべての可能な呼び出しをサポートするようにしたい場合
  • 物理的な場所に関連付ける必要がある場合
  • 複数の事業者が利用する場合

Advanced App Clip Experience を設定するには、呼び出しURLを登録する必要があります。また、Advanced App Clip Experience ごとに異なる画像やメタデータを指定することもできます。

Advanced App Clip Experience に設定すべきURL

Advanced App Clip Experience に登録すべきURLの数は、ユースケースによって異なります。たとえば、App Clipが常に同じ画面を表示する場合、1つ登録すれば十分でしょう。

通常の呼び出し(WebのSmart App Banner、Messageアプリの共有リンク)以外の呼び出しをサポートしたくない場合は、Advanced App Clip Experienceを作成する必要すらありません。その他のユースケースでは、逆にAdvanced App Clip Experienceを設定することが最も重要な場合があります。

一般的には、登録するURLはできるだけ少なくして、できるだけ汎用的なURLを登録してください。

次に、最もふさわしい接頭辞を持つURLを考えます。

たとえば、https://example.com/menu を URL として設定すると、https://example.com/menu/lunchhttps://example.com/menu/dinner などの同じURLで始まるすべてのURLをカバーすることができます。

ただし、ビジネスが複数の物理的な拠点を持つ場合は、1 つまたは複数の物理的な拠点用に それぞれ Advanced App Clip Experience を設定し、各拠点ごとに異なるヘッダー画像、メタデータ、および呼び出し URL を使用することができます。

(例: https://example.com/location1https://example.com/location2 など)

Advanced App Clip Experiences の作成

App Store Connect で Advanced App Clip Experienceを作成するには、呼び出し URL を登録する必要があります。異なるドメインの URL を使用している場合は、アプリとApp Clipの両方のターゲットの Associated Domains Entitlement に適切なドメインエントリを追加して、登録した URL を認証します(参考: Associated Domains Entitlement の追加)。

次に、App Clipと完全なアプリの両方のコードで、登録した URL に応答します。最後に、App Clipが物理的な場所に関連する機能を提供している場合は、Advanced App Clip Experience を物理的な場所に関連付けます(参考: ユーザーの位置情報の確認)。

Smart App Banner とリンクから呼び出せるようにする

WebサイトのSmart App Banner から、 App Clipを起動できるようにしましょう(注釈:Smart App Banner というのは、下記画面にもある、スマホサイトの上部に、アプリへの誘導を表示するアレ)。サイトにSmart App Bannerがすでに設置されている場合は、既存のmetaタグに app-clip-bundle-id=appClipBundleID 属性を追加し、その値にApp Clipのbundle identifierを使用します。

Smart App Bannerを表示したくない場合は、次のHTML metaタグを各ページに追加します。

<meta name="apple-itunes-app" content="app-id=myAppStoreID, app-clip-bundle-id=appClipBundleID, affiliate-data=myAffiliateData, app-argument=myAppArgument">

プレースホルダを適切な値に置き換えます。metaタグのcontent属性には、app-clip-bundle-idとapp-idの両方が含まれていることに注意してください。app-idを含めることで、iOS 13以前のデバイスでアプリを開いたり、プロモーションしたりできるようになります。

f:id:kumamotone:20200625025453p:plain
https://developer.apple.com/documentation/app_clips/configuring_your_app_clip_s_launch_experience

Smart App BannerをWebサイトに設置するには、Apple App Site Association ファイルを設定し、Webサイトのドメインを関連付けられたドメインのリストに追加する必要があります(参考: Associated Domains Entitlement の追加)。

App Clipでは、NSUserActivity オブジェクト経由で、呼び出し元の Smart App Bannerを設置したWebサイトのURLを取得できます。

Web サイトにSmart App Bannerを追加するには、Advanced App Clip Experience は必ずしも必要ありません。しかし、Smart App Bannerを表示できるWebサイトであれば、どのようなWebサイトにも設定することができます。

Advanced App Clip Experience の一部としてバナーの URL を登録したとします。このとき、バナーからApp Clipを起動すると、関連するClip Cardが表示されます。

一方、Default App Clip Experienceのみを設定した場合、バナーからApp Clipを起動すると、Default App Clip ExperienceのClip Cardが表示されます。

App Clip の呼び出しの設定

アプリとApp Clipのどちらもが、スムーズな起動を提供するために、呼び出しURL に応答して、UIを更新する必要があります。

たとえば、複数の物理的な場所を持つビジネスがあったとします。

https://example.com (何のパラメータもないURL)を設定した場合、起動後にユーザーにリストから物理的な場所を選択してもらうこともできますが、操作回数が増えてしまいます。呼び出し URL を適切に設定し、追加の URL パラメータを呼び出しに渡すことで、余計な操作を減らし、ユーザーの体験を良くすることができます。

アプリとApp Clipの両方について、

  1. Associated Domains capability を有効にし、App Clipを起動する各ドメインのエントリを追加します。上記の例では、example.comを追加します。
  2. ユーザーがある場所から別の場所に切り替えた場合に備えて、保存されていないデータを永続化します。
  3. 追加のパラメータを含む呼び出し URL を使用します(例:https://example.com/location1https://example.com/location2 など)。

起動時には、URL に応答して、UIを場所に合わせて更新します。

マップアプリからの呼び出しや「Siriからの提案」からの位置情報ベースのサジェストでは、App Clip Experienceに登録したURLが呼び出しURLとして使用されます。呼び出しURLのプレフィックスとしてのみ使用する場合でも、App Clipとアプリの両方でこのURLを処理できるようにしておく必要があります。

たとえば、Advanced App Clip Experience の1つとして https://example.com/menu を登録したとします。

その場合、NFC タグからの呼び出しには、https://example.com/menu/dinner/item/1234https://example.com/menu/dinner/item/5678 のような URL を使用し、 https://example.com/menu は使用しないかもしれません。

そのような場合でも、App Clipとアプリは https://example.com/menu は扱えるようにする必要があります。

呼び出しURLへのアクセス

App Clip が起動されるタイミングで、NSUserActivityオブジェクトが起動時のライフサイクル関数に渡ってきます。

任意の状態とデータを永続化し、起動時にNSUserActivityオブジェクトにアクセスするために、以下のそれぞれの場合に対応する必要があります。

  • SwiftUIの場合

    • SwiftUIのライフサイクル関数を実装します
    • たとえば、onContinueUserActivity(_:perform:) を使用します。
  • UIKit + UISceneDelegate の場合

    • UISceneDelegateで定義されている関数を実装します。
    • たとえば、scene(:willConnectTo:options:)、scene(:willContinueUserActivityWithType:)、scene(_:continue:)などを実装します。
  • UIKit + UIApplicationDelegate の場合

    • UIApplicationDelegateで定義されている関数を実装します。
    • application(:didFinishLaunchingWithOptions:)ではNSUserActivityオブジェクトにアクセスできないので、必ずapplication(:continue:restorationHandler:)を実装してください。

起動時に、呼び出しの型が NSUsuserActivityTypeBrowsingWeb であることを確認してから、App Clip を起動した URL にアクセスします。

次のコードは、呼び出しURLから情報を抽出する関数です。

func respondTo(_ activity: NSUserActivity?) {
        
    // 異常なデータのための guard 文
    guard activity != nil else { return }
    guard activity!.activityType != NSUserActivityTypeBrowsingWeb else { return }
    guard let incomingURL = activity?.webpageURL else { return }
    guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { return }

    // App Clipに渡されたURLに基づいてUIを更新
}

ユーザーの位置情報の検証

物理的な場所で呼び出す App Clipを作成する場合、タスクを実行する前に、システムがユーザーの場所を検証する必要がある場合があります。

f:id:kumamotone:20200625031813p:plain
https://developer.apple.com/documentation/app_clips/responding_to_invocations

App Clipは、素早く起動するために、軽量なアルゴリズムでユーザーが特定の場所にいることを検証します。

  1. App Clip の Info.plist に NSAppClip という Dictionary 型のキーを追加します。
  2. NSAppClipRequestLocationConfirmation という Bool 型のキーを追加し、値をtrueにします。このエントリはアプリのInfo.plistには追加する必要はありません。 アプリをインストールしてApp Clipを置き換えたとき、App Clipがすでにユーザーの位置情報を確認できていた場合は、アプリもユーザーの位置情報を確認できるようになります。
  3. 位置情報をApp Clipに提供します。この情報を取得するには、App Clipを起動するURLにIDを含めて、そのIDを使用してデータベース内のビジネスの位置情報を検索することができます。または、App Clipを起動する URL に位置情報自体をエンコードして含めることもできます。
  4. 位置情報を使って、半径500mまでのCLCircularRegionオブジェクトを作成し、confirmAcquired(in:completionHandler:) に渡します。

Info.plist に NSAppClipRequestLocationConfirmation キーを追加すると、App Clipの呼び出し時に表示されるClip Cardには、App Clipが自分の位置情報を確認できることをユーザーに伝える追加のnoteが含まれています。この権限はデフォルトで有効になっていますが、ユーザーはClip Cardのnoteをタップすることで無効にすることができます。

次のコードは、App Clipを起動すると、ユーザーの位置情報を検証します。ユーザーがデバイス上の位置情報サービスの権限を拒否した場合も含めた、すべての可能性に合わせてUIを更新するようにしてください。

import UIKit
import AppClip
import CoreLocation

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    // 該当するすべてのライフサイクル関数で verifyUserLocation(_:) を呼ぶ
    func verifyUserLocation(_ activity: NSUserActivity?) {
        
        // 異常なデータのための guard 文
        guard activity != nil else { return }
        guard activity!.activityType != NSUserActivityTypeBrowsingWeb else { return }
        guard let payload = activity!.appClipActivationPayload else { return }
        guard let incomingURL = activity?.webpageURL else { return }

        // CLRegion オブジェクトの作成
        guard let region = location(from: incomingURL) else {
            // ここで解析エラーに対応してください。
            return
        }
        
        // 呼び出しが期待した場所で発生したことを検証
        payload.confirmAcquired(in: region) { (inRegion, error) in
            guard let confirmationError = error as? APActivationPayloadError else {
                if inRegion {
                    // NFCタグの位置が、ユーザーの位置と一致した場合
                } else {
                    // NFCタグの位置が、ユーザーの位置と一致しなかった場合
                    // (例)だれかが NFC タグを移動したとき
                }
                return
            }
            
            if confirmationError.code == .doesNotMatch {
                // スキャンしたURLがApp Clipに登録されていなかった場合
            } else {
               // ユーザーが位置情報へのアクセスを拒否したか、
               // 呼び出し元がNFCタグやビジュアルコードではなかった場合
            }
        }
    }

    func location(from url:URL) -> CLRegion? {
        let coordinates = CLLocationCoordinate2D(latitude: 37.334722,
                                                 longitude: 122.008889)
        return CLCircularRegion(center: coordinates,
                                radius: 100,
                                identifier: "Apple Park")
    }
}

App Clip の起動のテスト

環境変数を追加して App Clip の呼び出しをローカルでデバッグしたり、TestFlightでテストするための呼び出しを設定したりします。

App Clipを作成する際には、Advanced App Clip Experienceに登録したURLにApp Clipが応答できるように、可能性のあるすべての呼び出しをテストすることが重要です。

デバッグ時のローカルテスト

App Clip のデバッグ時に呼び出しをテストするには、以下の工程を行います。

  1. Xcodeで、[Product] > [Scheme] > [Edit Scheme] を選択し、App Clip の Scheme を選択します。
  2. [Run] を選択します。
  3. Argument タブで、新しい環境変数を追加し、名前を _XCAppClipURL とし、その値をテストするURLに設定し、変数を有効にします。プロジェクトにApp Clip Targetを追加すると、Xcodeはこの環境変数を追加します。
  4. テストしたいURLのドメインが、Associated Domains Entitlement に追加されていることを確認します(参考: Associated Domains Entitlement の追加)。
  5. NSUserActivity オブジェクトから構成したテスト URL にアクセスするために、App Clipをビルドして実行します。

TestFlight を使った呼び出しURLのテスト

新しいバージョンのApp Clipを App Store Connect に提出し、TestFlight でテスターが利用できるようにすると、テスト用に最大 3 つの異なる呼び出しを設定できます。

TestFlightアプリでは、設定された3つの呼び出しのいずれかからApp Clipを起動し、App Clipが期待通りに応答するかどうかを検証します。TestFlight アプリからApp Clipを起動した場合、Clip Cardは表示されないことに注意してください。

出典

続き

kumamotone.hatenadiary.jp