Yappli Tech Conference 2023 における発表内容です。 https://yappli.connpass.com/event/295001/
Yappliにおける パーミッション要求の課題と改善
View Slide
Speakerプロダクト開発本部 開発1部 iOSグループiOSエンジニア菅 直之2022年9⽉からヤプリでiOSエンジニアとして 働いています 趣味は登⼭と将棋で、最近は電⼦ピアノにハマっています@Nao_RandD
0102030405iOSにおけるパーミッション要求Yappliにおけるパーミッション要求基盤改善における課題と今後実現したいこと改善までの道のりまとめ
01 iOSにおけるパーミッション要求
0 1iOSにおけるパーミッション要求iOSにおけるパーミッション要求パーミッション要求とは?• アプリがデバイスの特定機能やデータに アクセスするために必要となる 例:位置情報、カメラ、プッシュ通知など • ユーザーがアプリにどのような特定機能‧ アクセスを許可するかを明⽰的にコントロールできる
0 1iOSにおけるパーミッション要求iOSにおけるパーミッション要求開発者に期待されること• 必要最低限のパーミッションのみを 要求すること • プライバシーの側⾯でデータや リソース利⽤の透明性を担保するhttps://developer.apple.com/design/human-interface-guidelines/privacy
0 1iOSにおけるパーミッション要求iOSにおけるパーミッション要求パーミッション要求のダイアログ• ダイアログ表⽰処理はアプリで呼び出し システムによって実⾏される • 同時に複数呼び出すとダイアログが重複する 動作となりユーザー体験を損なう • ホーム画⾯など要求処理が集中する場所では 呼び出しに配慮が必要になる
02 Yappliにおけるパーミッション要求
0 2Yappliにおけるパーミッション要求Yappliにおけるパーミッション要求Yappliはノーコードのアプリプラットフォーム• 650社以上で導⼊されており、約800アプリを提供している• アプリ要件に合わせて40種以上の機能がある
アプリごとのパーミッション• 同じプラットフォーム上でアプリごとに必要な パーミッションが異なる0 2Yappliにおけるパーミッション要求YappliYappliにおけるパーミッション要求
03 基盤改善における課題と今後実現したいこと
12基盤改善前の課題今後実現したいこと• 事前説明画⾯の機能追加03 基盤改善における課題と今後実現したいこと• 可読性‧保守性の低さ• 要求処理のボイラープレート化• 同時に複数の要求を扱うのが難しい
パーミッション要求はシステム実⾏であり、 順次実⾏のためにコールバックで実装しているコールバックの実⾏は開発者の責務となり、 呼ばないことによるコンパイルエラーは出ない1 基盤改善前の課題03 基盤改善における課題と今後実現したいこと可読性‧保守性の低さfunc hoge() {requestPermissionA {// ॏཁͳޙଓॲཧ}}func requestPermissionA(completion: @escaping () -> Void) {// PermissionAͷύʔϛογϣϯཁٻॲཧ// ...if isAuthorize {// ޙଓͷॲཧ͕࣮ߦ͞ΕΔcompletion()} else {// Կ͠ͳ͔ͬͨ}}パーミッションに関わる処理はストア審査リジェクト、機能提供できないなど⼤きな問題になる可能性が⾼い
パーミッションの種類によって 要求処理に必要な実装は異なる機能単位で要求処理を別々に実装しており処理として共通化されていない1 基盤改善前の課題03 基盤改善における課題と今後実現したいこと要求処理のボイラープレート化 struct ModuleA {typealias Callback = (Bool) -> Voidfunc requestLocationPermission(_ completion: @escaping Callback) {// Ґஔใͷύʔϛογϣϯཁٻॲཧ}func requestATTPermission(_ completion: @escaping Callback){// ATT(AppTrackingTransparancy)ͷύʔϛογϣϯཁٻॲཧ}}struct ModuleB {typealias Callback = (Bool) -> Voidfunc requestLocationPermission(_ completion: @escaping Callback) {// Ґஔใͷύʔϛογϣϯཁٻॲཧ}}同じパーミッションの実装が必要な機能ごとに増えていく
ホーム画⾯など同時に複数のパーミッションを組み合わせて要求したい場⾯がある1 基盤改善前の課題03 基盤改善における課題と今後実現したいこと同時に複数の要求を扱うのが難しいfunc requestMultiPermission(_ completion: @escaping () -> Void) {requestLocationPermission { _ inrequestATTPermission { _ inrequestBluetoothPermission { _ in// ޙଓॲཧ}}}}要求処理が増えるたびにネストが深くなってしまう
それぞれのパーミッション要求前に事前説明が欲しい要望があった2 今後実現したいこと03 基盤改善における課題と今後実現したいこと事前説明画⾯の機能追加https://developer.apple.com/jp/design/human-interface-guidelines/privacy#Pre-alert-screens-windows-or-viewsパーミッション要求前にユーザーに対して、 アプリで⽤いる⽤途や提供する機能を説明する画⾯のこと許諾率の向上となりAppleも推奨している事前説明画⾯とは?
2 今後実現したいこと03 基盤改善における課題と今後実現したいこと事前説明画⾯の機能追加パーミッション要求事前説明画⾯表⽰ユーザーが選択後に後続の処理
2 今後実現したいこと03 基盤改善における課題と今後実現したいこと事前説明画⾯の機能追加パーミッション要求事前説明画⾯表⽰ユーザーが選択後に後続の処理パーミッション要求前後の処理の差し込みにも柔軟に対応できる実装を⽬指す
3つの課題今後実現したいこと⬜︎可読性‧保守性の低さ⬜︎要求処理のボイラープレート化⬜︎同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加03 基盤改善における課題と今後実現したいこと✅ 改善におけるチェックリスト
04 改善までの道のり
STEP 2STEP 1STEP 3Swift Concurrency導⼊インターフェースを共通化04 改善までの道のり要求処理のワークフロー化
STEP 2STEP 1STEP 3Swift Concurrency導⼊インターフェースを共通化要求処理のワークフロー化04 改善までの道のり
12Swift Concurrencyのメリット導⼊における考慮• Swift Concurrencyについて• async/awaitによる可読性向上• パーミッションごとの要求処理の差分• Delegateパターンの吸収04 改善までの道のり- STEP1Swift Concurrency導⼊課題今後実現したいこと⬜︎可読性‧保守性の低さ⬜︎要求処理のボイラープレート化⬜︎同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加✅ 改善におけるチェックリスト
Swift5.5から⾔語機能として登場した⾮同期処理‧並列処理のコードを簡潔かつ安全に記述できるasync/awaitを使⽤することができる1 Swift Concurrencyのメリット04 改善までの道のり- STEP1Swift Concurrency導⼊Swift Concurrencyについてhttps://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/
async/awaitを⽤いることで⾮同期処理を 同期処理と同じような書き⽅で実装できるコールバックのネストが深い処理などを 可読性⾼く実装する助けになる1 Swift Concurrencyのメリット04 改善までの道のり- STEP1Swift Concurrency導⼊async/awaitによる可読性向上// ίʔϧόοΫͰͷ࣮ߦfoo { x inbar(x) { y inbaz(y) { z in// ޙଓͷॲཧ}}}// async/awaitͰͷ࣮ߦlet x = await foo()let y = await bar(x)let z = await baz(y)パーミッション要求処理をasync/awaitに落とし込んでいく
async/awaitが対応したものがAPIが 提供されていれば素直に実装するプッシュ通知のUNUserNotificationなどはasync/awaitで呼び出せるAPIがあるrequestAuthorization(for: )2 導⼊における考慮04 改善までの道のり- STEP1Swift Concurrency導⼊async/await の I/F が⽤意されている ⭕func request() async -> Bool {do {let nc = UNUserNotificationCenter.current()return try await nc.requestAuthorization(options: [.badge,.sound,.alert])} catch {// ΤϥʔϋϯυϦϯά}}https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649527-requestauthorization
代表的なケース:Delegateでパーミッションのステータス変化を 検知する必要がある例:Bluetoothや位置情報そのままasync/awaitで置き換えられない2 導⼊における考慮04 改善までの道のり- STEP1Swift Concurrency導⼊async/await の I/F が⽤意されていない ❌class BluetoothPermission: NSObject,CBCentralManagerDelegate {private var centralManager: CBCentralManager?typealias Status = CBManagerAuthorizationfunc request() {// ύʔϛογϣϯεςʔλεʹԠͯ͡ૣظϦλʔϯ// ...// CBCentralManagerΛΠϯελϯεԽ͢ΔͱμΠΞϩάදࣔcentralManager = CBCentralManager(delegate: self,queue: nil)}/// εςʔλε͕มߋ͞ΕΔͱݺΕΔDelegateϝιουfunc centralManagerDidUpdateState(_ central: CBCentralManager) {let status = CBManager.authorization// ύʔϛογϣϯεςʔλε͝ͱͷॲཧ}}
代表的なケース:Delegateでパーミッションのステータス変化を 検知する必要がある例:Bluetoothや位置情報そのままasync/awaitで置き換えられない2 導⼊における考慮04 改善までの道のり- STEP1Swift Concurrency導⼊async/await の I/F が⽤意されていない ❌https://developer.apple.com/documentation/swift/withcheckedcontinuation(function:_:)func request() async -> Status {// εςʔλεʹԠͯ͡ૣظϦλʔϯ// ...// CBCentralManagerΛΠϯελϯεԽ͢ΔͱμΠΞϩάදࣔcentralManager = CBCentralManager(delegate: self,queue: nil)return await withCheckedContinuation {[weak self] (continuation: Continuation) inguard let self = self else { return }self.continuation = continuation}}func centralManagerDidUpdateState(_ central: CBCentralManager) {let status = CBManager.authorizationif status == .notDetermined { return }// resumeʹεςʔλεΛ͢continuation?.resume(returning: status)continuation = nil}withCheckedContinuationを利⽤する
12Swift Concurrencyのメリット導⼊における考慮• Swift Concurrencyについて• async/awaitによる可読性向上• パーミッションごとの要求処理の差分• Delegateパターンの吸収04 改善までの道のり- STEP1Swift Concurrency導⼊課題今後実現したいこと✅ 可読性‧保守性の低さ⬜︎要求処理のボイラープレート化⬜︎同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加✅ 改善におけるチェックリスト
STEP 1STEP 3STEP 2Swift Concurrency導⼊インターフェースを共通化要求処理のワークフロー化04 改善までの道のり
12共通化において実現したいこと実装• 異なるパーミッション要求処理の共通化• Managerクラスを定義してI/Fも共通化• 全体図• コードに落とし込む04 改善までの道のり- STEP2インターフェースを共通化課題今後実現したいこと✅ 可読性‧保守性の低さ⬜︎要求処理のボイラープレート化⬜︎同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加✅ 改善におけるチェックリスト
種別によって別々に実装されている処理を「パーミッション要求」として共通化したい要求処理を含むプロトコルとして定義する パーミッション種別ごとの処理に呼び元が依存しないようにする1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化異なるパーミッション要求を共通化する
パーミッション要求を共通化するのに合わせて 処理実⾏も1つに集約する異なる機能から同時に複数のパーミッション要求がされた場合にも順次実⾏する(前述のダイアログ重複を防⽌するため)1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化ManagerI/FManagerにパーミッションを渡した順序で順次実⾏する
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化ManagerI/FManagerにパーミッションを渡した順序で順次実⾏する要件同時に複数のパーミッション要求がされた場合にも順次実⾏
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能AパーミッションAパーミッションB機能BパーミッションCManagerI/FManagerにパーミッションを渡した順序で順次実⾏する要件同時に複数のパーミッション要求がされた場合にも順次実⾏
キューでパーミッション要求処理をまとめて管理して追加された順に実⾏する1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能AパーミッションB機能BパーミッションCManagerI/Fキューに追加キュー実⾏タスクパーミッションA
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能AパーミッションA機能BパーミッションCManagerI/Fキューに追加キュー実⾏タスクパーミッションBキューでパーミッション要求処理をまとめて管理して追加された順に実⾏する
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能A機能BパーミッションCManagerI/Fキューに追加キュー実⾏タスクパーミッションBパーミッションAパーミッションAを実⾏中(ダイアログ表⽰状態)キューでパーミッション要求処理をまとめて管理して追加された順に実⾏する
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能A機能BパーミッションCManagerI/Fキューに追加キュー実⾏タスクパーミッションBパーミッションAパーミッションAを実⾏中(ダイアログ表⽰状態)Managerが実⾏中にパーミッション要求処理 → キューへの追加キューでパーミッション要求処理をまとめて管理して追加された順に実⾏する
1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化機能A機能BManagerI/Fキューに追加キュー実⾏タスクパーミッションBパーミッションAキューに追加され順番に実⾏されるパーミッションCキューでパーミッション要求処理をまとめて管理して追加された順に実⾏するパーミッションAを実⾏中(ダイアログ表⽰状態)
キューでパーミッション要求処理をまとめて管理して追加された順に実⾏する1 共通化において実現したいこと04 改善までの道のり- STEP2インターフェースを共通化Managerクラスを定義してI/Fも共通化ManagerI/Fキューに追加キュー実⾏タスクパーミッションBパーミッションAキューに追加され順番に実⾏されるパーミッションCパーミッションAを実⾏中(ダイアログ表⽰状態)この動作を満たすように実装していく
パーミッション要求を共通化したPermissionRequestをプロトコルで定義するPermissionManagerにaddされたものを順次実⾏していくようにする2 実装04 改善までの道のり- STEP2インターフェースを共通化全体図
パーミッション要求をPermissionRequestとしてプロトコルに定義パーミッションステータスもPermissionStatusとしてプロトコルに定義2 実装04 改善までの道のり- STEP2インターフェースを共通化コードに落とし込む(パーミッション要求)
2 実装04 改善までの道のり- STEP2インターフェースを共通化// MARK: PermissionRequestprotocol PermissionRequest {func request() async -> PermissionStatus}// MARK: PermissionStatusprotocol PermissionStatus {}パーミッション要求をPermissionRequestとしてプロトコルに定義パーミッションステータスもPermissionStatusとしてプロトコルに定義コードに落とし込む(パーミッション要求)
2 実装04 改善までの道のり- STEP2インターフェースを共通化コードに落とし込む(Managerクラス)パーミッション要求(PermissionRequest)を実⾏するPermissionMangerを実装する
PermissionRequestをキューで管理する外部からキューに追加するメソッドを公開する実⾏中を⽰すステータスとタスクを実⾏するメソッドを⽤意する2 実装04 改善までの道のり- STEP2インターフェースを共通化コードに落とし込む(Managerクラス)
必要な要素• PermissionRequestを⼊れるキュー• 実⾏中を管理できるOperationStatus• I/Fとして外部からキューに追加するadd()• キューにあるPermissionRequestを 順次実⾏するexecuteRequest()2 実装04 改善までの道のり- STEP2インターフェースを共通化@MainActorfinal class PermissionManager {static let shared = PermissionManager()private init() {}private enum OperationStatus {case runningcase waiting}private var queue: [PermissionRequest] = []private var operationStatus: OperationStatus = .waitingfunc add(request: PermissionRequest) {// ...// ࣮ߦதͰͳ͚Ε executeRequest()}private func executeRequest() async {// ...}}コードに落とし込む(Managerクラス)
@MainActorfinal class PermissionManager {static let shared = PermissionManager()private init() {}private enum OperationStatus {case runningcase waiting}private var queue: [PermissionRequest] = []private var operationStatus: OperationStatus = .waitingfunc add(request: PermissionRequest) {// ...// ࣮ߦதͰͳ͚Ε executeRequest()}private func executeRequest() async {// ...}}必要な要素• PermissionRequestを⼊れるキュー• 実⾏中を管理できるOperationStatus• I/Fとして外部からキューに追加するadd()• キューにあるPermissionRequestを 順次実⾏するexecuteRequest()2 実装04 改善までの道のり- STEP2インターフェースを共通化排他的なキューへのアクセスとなるようにPermissionManagerを@MainActorにするコードに落とし込む(Managerクラス)
実装後• PermissionManagerにパーミッション要求を 追加するだけで実⾏できる• 同時に複数の要求処理が追加されても順次実⾏で パーミッション要求を⾏える2 実装04 改善までの道のり- STEP2インターフェースを共通化// MARK: AsIsfunc requestMultiPermission() {requestLocationPermission { _ inrequestATTPermission { _ in}}}// MARK: ToBePermissionManager.shared.add(request: LocationPermission())PermissionManager.shared.add(request: ATTPermission())コードに落とし込む(Managerクラス)
実装後• PermissionManagerにパーミッション要求を 追加するだけで実⾏できる• 同時に複数の要求処理が追加されても順次実⾏で パーミッション要求を⾏える2 実装04 改善までの道のり- STEP2インターフェースを共通化// MARK: AsIsfunc requestMultiPermission() {requestLocationPermission { _ inrequestATTPermission { _ in}}}// MARK: ToBePermissionManager.shared.add(request: LocationPermission())PermissionManager.shared.add(request: ATTPermission())同時に呼び出す要求処理が増えてもネストが深くならないコードに落とし込む(Managerクラス)
12共通化において実現したいこと実装• 異なるパーミッション要求処理の共通化• 組み合わせても使⽤できる仕組み• コードに落とし込む04 改善までの道のり- STEP2インターフェースを共通化課題今後実現したいこと✅ 可読性‧保守性の低さ✅ 要求処理のボイラープレート化✅ 同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加✅ 改善におけるチェックリスト
12要求処理を組み合わせるワークフロー実装• 要求処理に紐づいた処理も共通化したい• 全体図• コードに落とし込む04 改善までの道のり- STEP3要求処理のワークフロー化課題今後実現したいこと✅ 可読性‧保守性の低さ✅ 要求処理のボイラープレート化✅ 同時に複数の要求を扱うのが難しい⬜︎事前説明画⾯の機能追加✅ 改善におけるチェックリスト
パーミッション要求と紐づいた処理も PermissionMangerのI/Fで使えるようにしたい1 要求処理を組み合わせるワークフロー要求処理に紐づいた処理も共通化したい04 改善までの道のり- STEP3要求処理のワークフロー化
パーミッション要求と紐づいた処理も PermissionMangerのI/Fで使えるようにしたいワークフローを利⽤できるようにする1 要求処理を組み合わせるワークフロー要求処理に紐づいた処理も共通化したい04 改善までの道のり- STEP3要求処理のワークフロー化
パーミッション要求と紐づいた処理も PermissionMangerのI/Fで使えるようにしたいワークフローを利⽤できるようにするワークフローとパーミッション要求を共通化したPermissionActionを新たに定義する1 要求処理を組み合わせるワークフロー要求処理に紐づいた処理も共通化したい04 改善までの道のり- STEP3要求処理のワークフロー化
ワークフローとパーミッション要求を共通化したPermissionActionを定義する2 実装全体図04 改善までの道のり- STEP3要求処理のワークフロー化
ワークフローとパーミッション要求を 共通化したPermissionActionを定義するPermissionRequestはPermissionAction に準拠する2 実装全体図04 改善までの道のり- STEP3要求処理のワークフロー化
ワークフローとパーミッション要求を 共通化したPermissionActionを定義するPermissionRequestはPermissionAction に準拠するPermissionManagerはPermissionAction を順次実⾏する2 実装全体図04 改善までの道のり- STEP3要求処理のワークフロー化
2 実装// MARK: PermissionActionprotocol PermissionAction {func execute() async}// MARK: PermissionRequestprotocol PermissionRequest: PermissionAction {func request() async -> PermissionStatus}// Protocol ExtensionͰrequestΛݺͿ͜ͱͰ // PermissionRequestଆ࣮ʹ͓͍ͯ // PermissionActionͷ࣮Λҙࣝ͠ͳͯ͘ྑ͘͢Δextension PermissionRequest {func execute() async {_ = await request()}}protocol PermissionStatus {}コードに落とし込む(PermissionRequestプロトコル)04 改善までの道のり- STEP3要求処理のワークフロー化
2 実装// PermissionRequest͜Ε·Ͱ௨Γͷ࣮Ͱྑ͍ʢલड़ʣstruct PermissionA: PermissionRequest {func request() async -> PermissionStatus {// PermissionAͷύʔϛογϣϯཁٻॲཧ}}struct PermissionB: PermissionRequest {func request() async -> PermissionStatus {// PermissionBͷύʔϛογϣϯཁٻॲཧ}}// ෳͷύʔϛογϣϯΛऔಘ͢ΔύλʔϯϫʔΫϑϩʔʹͰ͖Δstruct MultiPermissionWorkflow: PermissionAction {func execute() async {let statusA = await PermissionA().request()let statusB = await PermissionB().request()}}コードに落とし込む (ワークフローとパーミッション要求)04 改善までの道のり- STEP3要求処理のワークフロー化
実装後• 各機能からPermissionManagerに パーミッション要求とワークフローを キューに追加するのみで実⾏できる2 実装struct LocationPermission: PermissionRequest {func request() async -> PermissionStatus {// Ґஔใͷύʔϛογϣϯཁٻॲཧ}}// ෳͷύʔϛογϣϯΛཁٻ͢ΔϫʔΫϑϩʔstruct MultiPermissionWorkflow: PermissionAction {func execute() async {await BluetoothPermission().request()// Bluetoothͷཁٻॲཧޙʹඞཁͳॲཧawait ATTPermission().request()}}// ϫʔΫϑϩʔ୯ମͷύʔϛογϣϯಉ͡Α͏ʹݺͼग़ͤΔPermissionManager.shared.add(action: MultiPermissionWorkflow())PermissionManager.shared.add(action: LocationPermission())04 改善までの道のり- STEP3要求処理のワークフロー化コードに落とし込む (ワークフローとパーミッション要求)
// MARK: ࣄલઆ໌ը໘ΛؚΜͩύʔϛογϣϯཁٻstruct PreAlertScreenWorkflow: PermissionAction {func execute() async {// ͜͜ʹࣄલઆ໌ը໘ͷॲཧʢҐஔใʣawait LocationPermission().request()// ͜͜ʹࣄલઆ໌ը໘ͷॲཧʢATTʣawait ATTPermission().request()// ޙଓॲཧ}}// ݺͼݩPermissionManager.shared.add(action: PreAlertScreenWorkflow())実装後• 各機能からPermissionManagerに パーミッション要求とワークフローを キューに追加するのみで実⾏できる2 実装04 改善までの道のり- STEP3要求処理のワークフロー化コードに落とし込む (ワークフローとパーミッション要求)事前説明画⾯処理もワークフローとしてPermissionManagerで扱える
12要求処理を組み合わせるワークフロー実装• 要求処理に紐づいた処理も共通化したい• 全体図• コードに落とし込む04 改善までの道のり- STEP3要求処理のワークフロー化課題今後実現したいこと✅ 可読性‧保守性の低さ✅ 要求処理のボイラープレート化✅ 同時に複数の要求を扱うのが難しい✅ 事前説明画⾯の機能追加✅ 改善におけるチェックリスト
05 まとめ
取り組んだことYappliにおけるパーミッション要求処理の課題と改善Swift Concurrency導⼊パーミッション要求処理の共通化要求処理のワークフロー化現状の課題改善‧事前説明画⾯の下準備に!05 まとめ
ご清聴ありがとうございましたYappliにおけるパーミッション要求の課題と改善