20210827のiOSに関する記事は7件です。

APNS のペイロードデータを struct にマッピングする

TL;DR struct APNSPayload: Decodable { struct APS: Decodable { struct Alert: Decodable { let title: String let body: String } let alert: Alert let badge: Int? } struct CustomField: Decodable { let hogeList: [String] let fuga: String? } let aps: APS let customField: CustomField? init(decoding userInfo: [AnyHashable: Any]) throws { let json = try JSONSerialization.data(withJSONObject: userInfo, options: []) // snake_caseをcamelCaseに変換する場合 let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase self = try decoder.decode(APNSPayload.self, from: json) } } // { // "aps" : { // "alert" : { // "title" : "タイトル", // "body" : "本文", // }, // "badge": 9, // }, // "custom_field": { // "hoge_list": [ // "piyo" // ] // } // } let payload: APNSPayload? = try? APNSPayload(decoding: userInfo) モチベーション APNS のペイロードデータは REST APIのレスポンスと同様に JSON 形式で表現されるのですが、クライアント側での取得インターフェイスが var userInfo: [AnyHashable : Any] { get } となっているため、ペイロード中のカスタムデータを取得するとなると、 guard let aps = userInfo["custom_field"] as? NSDictionary, let hogeList = aps["hoge_list"] as? [String] else { return } のような 辛い実装が発生することがあります。 これを、よくある REST API のレスポンスモデルへのマッピングのような形で実装したいというのがモチベーションとなります。 概要 stack overflow のSwift read userInfo of remote notification に対する回答そのままです。 userInfo: [AnyHashable: Any] 自体は JSON フォーマットを表現しているため、 userInfo を JSONObject として扱い、JSONSerialization.data で JSONData に変換 JSONDecoder で JSONData を元に Decodable へのデコードを試みる という流れで、 Decodable に適合した struct へのマッピングを実現しています。 上述の例ではペイロードのキー名が snake_case で表現されている前提なのですが、実際には APNS ペイロードデータ内で利用されるキー名に応じて、 Decodable に適合する際に enum CodingKeys: String, CodingKey を用意する or プロパティ名の調整が必要となってきます。 (APNS標準のペイロードデータでは、 content-available のように kebab-case が利用されています。) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS] URLSchemeで起動した時のAppDelegate/SceneDelegateの呼び出し順序

※本記事の前提環境:Xcode 12.5.1 AppDelegate/SceneDelegateを両方実装している場合に、URLSchemeのトリガーによって、URLパラメータ取得系のdelegateメソッドがどのように走るのかを調査しました。 結構メンドクサイなと思いました… なお、アプリ未起動状態でURLSchemeによってアプリが起動された場合のデバッグについては、以下の記事を参考にさせていただきました。 iOS 13以降 注:URLパラメータ取得系のdelegateメソッド以外については省略しています。 アプリ未起動状態 AppDelegate - application(_ :didFinishLaunchingWithOptions) SceneDelegate - scene(_ :willConnectTo :options) ※注:scene(_ :openURLContexts)は呼ばれない アプリ起動状態 SceneDelegate - scene(_ :openURLContexts) iOS 12以前 アプリ未起動状態 AppDelegate - application(_ :didFinishLaunchingWithOptions) AppDelegate - application(_ :open :options) アプリ起動状態 AppDelegate - application(_ :open options)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Metal Best Practicesの解説(6) ドローアブル

Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。 本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。 読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。 また、iOSの記事なので他のOS(MacOS, tvOS)についての記載は割愛します。 他の記事の一覧は、初回記事よりご覧下さい。 Drawables (ドローアブル) ベストプラクティス: ドローアブルをできるだけ短く保持します。 ドローアブルとは、レンダリングや書き込みして、実際に表示をすることのできるリソースです。アプリはこのドローアブルをできるだけ短く保持しましょうということです。 その理由は次のようになります。 ドローアブルは、コマンドバッファに登録されたコマンドの処理が完了するのを待ってから表示します。 一方で、ドローアブルは再利用可能なリソースプール内に存在していて、CPU側のスレッドから利用できるドローアブルは限られます。ドロアーブルが利用できないとき、CPU側のスレッドはブロックされます。 つまり、ドローアブルの利用はCPUスレッドにとっては待ち状態(ストール)が発生しやすくなるということです。このため、ドロアーブルの保持はなるべく短くして、他のCPUスレッドの処理を邪魔しないようにします。 ドローアブルをできるだけ短く保持するにはつぎの方法を使います。 できるだけ遅くドローアブルを取得する。できれば画面のレンダーパスをエンコードする直前。オフスクリーンレンダリングはドローアブルを取得する前に実行することができるので、ドローアブルの取得はその後で良い。 できるだけ早くドローアブルをリリースする。できれば、フレームのCPU作業を完了した直後。複数のドロアーブルで発生する可能性のあるデッドロックを回避するために、autorelease pool内にレンダリングプールを含める。 MTKViewはドローアブルのライフサイクルを自動的に処理してくれるので、なるべくこれを使うのが良さそうです。 MTKViewはcurrentDrawableプロパティを提供し、現在のフレームの終わりにcurrentDrawableを自動的に更新してくれます。 コードで検証してみる ドローアブルを長く保持すると、実際にストールが発生しやすくなるのかを検証してみます。 MTKViewはいい感じに処理してしまうので、あえて問題を出しやすいようにCAMetalLayerを使います。 こちらにCAMetalLayerに描画するために今回ベースにしたサンプルコードを保存してあります。 このコードを改変して、autoreleasepoolがある場合、ない場合に実行時間に差が出るかを検証してみます。 差を浮き彫りにするために、autoreleasepoolの後に無駄な処理を入れています。 時間の計測はos_signpostを使いたかったのですがなぜかうまく動作しなかったため、print文を使用します。 レンダリングループのコードは次のとおりです。テクスチャーを表示しているだけの処理ですが、ところどころに無駄な処理を入れてドローアブルを長く保持するようにしています。 CAMetalLayerView.swift @objc func draw() { // ミリ秒を表示する関数 func nowTime() -> String { let format = DateFormatter() format.dateFormat = "yyyy/MM/dd HH:mm:ss.SSSS" return format.string(from: Date()) } // ログ出力を見やすくするためにスレッドに番号を付ける threadNo += 1 let localNo = threadNo guard let texture = texture else {return} // ここからレンダリング処理 autoreleasepool { print("before = \(localNo), \(nowTime())") guard let drawable = metalLayer.nextDrawable() else { print("fail = \(localNo), \(nowTime())") return } print("after = \(localNo), \(nowTime())") let commandBuffer = metalCommandQueue.makeCommandBuffer() renderPassDescriptor.colorAttachments[0].texture = drawable.texture let w = min(texture.width, drawable.texture.width) let h = min(texture.height, drawable.texture.height) let blitEncoder = commandBuffer!.makeBlitCommandEncoder()! // レンダリングは無駄に重くする。テクスチャーを1万回コピーしている for _ in 0..<10000 { blitEncoder.copy(from: texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOrigin(x:0, y:0 ,z:0), sourceSize: MTLSizeMake(w, h, texture.depth), to: drawable.texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: MTLOrigin(x:0, y:0 ,z:0)) } blitEncoder.endEncoding() commandBuffer!.present(drawable) commandBuffer!.commit() commandBuffer?.waitUntilCompleted() } // レンダリング後の処理は無駄に重くする。 var x = 0 for _ in 0...100000 { x += 1 } } 上のレンダリング処理を500回スレッドで呼び出す処理を作ります。 CAMetalLayerView.swift for _ in 0..<500 { DispatchQueue.global(qos:.userInteractive).async { self.draw() } } 実行してみる 上のコードを使ってautoreleasepoolがある場合、ない場合を作り、それぞれ実行して処理時間を計測します。 実行するとXcodeに処理時間が表示されるので、これを集計します。 なお、CAMetalLayer.nextDrawable()はドローアブルが取得できないとブロックしますが、1秒経過するとタイム・アウトして次のようなエラーが出ていました。 2021-08-26 18:02:37.958534+0900 MetalExamples[44711:5922300] [CAMetalLayer nextDrawable] returning nil due to 1 second timeout. Set allowsNextDrawableTimeout to keep retrying. 実行結果 次のようになりました。 コード 平均値待ち時間(msec) ドロアーブル取得に失敗した回数 トータル実行時間(sec) autoreleaseあり 777 30 65 autoreleaseなし 933 31 70 autorelaseをつけたほうが待ち時間は短くなり、ドローアブル取得の失敗も少ないことがわかります。 結論 ドローアブルを保持する時間が短いほうが、CPU処理の待ち時間が減り、パフォーマンスが上がることがわかりました。レンダリングループの処理の中でautoreleasepoolで処理を囲むコードはよく見かけますが、これが理由のようです。 最後に 今回はうまいサンプルを作るのに苦労しましたが、なんとか完成してよかったです? iOSを使った3D処理やAR、ML、音声処理などの作品やサンプル、技術情報を発信しています。 作品ができたらTwitterで発信していきますのでフォローをお願いします? Twitterは作品や記事のリンクを貼っています。 https://twitter.com/jugemjugemjugem Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。 https://qiita.com/TokyoYoshida Noteでは、連載記事を書いています。 https://note.com/tokyoyoshida Zennは機械学習が多めです。 https://zenn.dev/tokyoyoshida
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

unity ios書き出しのスプラッシュを任意の色にする

環境 Unity 2019.4.15 Xcode 12.4 この環境でデフォルトで書き出すと、黒に近い灰色のスプラッシュになります Unityで黒のpngを設定しても、思った効果になりませんでした 仕様が変わったようでXcodeのStoryBoardから読んでるらしいです。 解決策 一応、ビルドが通りましたが いいのか、悪いのか、、、 1、Unityのスプラッシュ設定は、デフォルトのまま(show Splash Screen OFF)書き出し 2、Xcodeで、StoryBoardを開きRootViewの下を削除する  3、RootViewのBackgroundとTintを任意の色にする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

flutter_flavorizr + flutter_flavorでFlutterの環境切り替えした後に、iOSでbuildした時の 「Unable to load contents of file list」の解決方法

概要 Flutterの開発環境/本番環境を「flutter_flavorizr + flutter_flavor」を使って、切り替えできるように対応しました。 下記の問題が発生しました。 こちらの記事のその解決を記載しています。 iOSでbuildすると、「Unable to load contents of file list: '/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-development-input-files.xcfilelist'」のErrorが出て、buildが通らない 対応環境 (Errorが出ている。。。あとで直そう) [✓] Flutter (Channel master, 2.5.0-7.0.pre.15, on macOS 11.3.1 20E241 darwin-x64, locale ja-JP) [!] Android toolchain - develop for Android devices (Android SDK version 30.0.0) ✗ cmdline-tools component is missing Run `path/to/sdkmanager --install "cmdline-tools;latest"` See https://developer.android.com/studio/command-line for more details. ✗ Android license status unknown. Run `flutter doctor --android-licenses` to accept the SDK licenses. See https://flutter.dev/docs/get-started/install/macos#android-setup for more details. [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio (version 2020.3) [✓] VS Code (version 1.59.1) [✓] Connected device (2 available) 前提処理 下記の2つを記事を見ながら進めました。 Flutter で環境を分ける方法 flutter_flavor + flutter_flavorizr を使って Flutter の Flavor を設定する iOSでbuildする段階で問題発生 iOSでbuildすると、「Unable to load contents of file list: '/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-development-input-files.xcfilelist'」のErrorが出て、buildが通らない こちらの対策として、下記の3つの手順を行いました。 podfileの更新 Xcode上でのConfigurationsの設定 XcodeのFlutterフォルダの、「Debug-(環境名)」等の中身を修正する 1. podfileの更新 作成した環境に合わせて、Configurations先を追加しました。 Before project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } After project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, 'Debug-development' => :debug, 'Profile-development' => :release, 'Release-development' => :release, 'Debug-production' => :debug, 'Profile-production' => :release, 'Release-production' => :release, } 2. Xcode上でのConfigurationsの設定 作成した環境に合わせてConfigurationsが増えるいるので、適切なものが入るようにします。 3. XcodeのFlutterフォルダの、「Debug-(環境名)」等の中身を修正する XcodeのFlutterフォルダとは、赤枠の範囲を指します。 今回は、「Unable to load contents of file list: '/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-development-input-files.xcfilelist'」というError解消のため、 developmentDebug.xcconfigを編集します。 先頭に一行下記を加えるだけです!(環境ごとに書く内容は切り替えてください。) #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug-development.xcconfig" Before #include "Generated.xcconfig" FLUTTER_TARGET=lib/main-development.dart ASSET_PREFIX=development BUNDLE_NAME= BUNDLE_NAME After #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug-development.xcconfig" #include "Generated.xcconfig" FLUTTER_TARGET=lib/main-development.dart ASSET_PREFIX=development BUNDLE_NAME= BUNDLE_NAME 備考 こちらでXcodeからのbuildが通るようになると思います! まだAndroid Studioから直接実行できていないので、そちらの解決方法が分かりしたい追記したいと思います。 最後まで読んでいただき、ありがとうございました。 【追記】 Android Studio Error対応 Create Bridging Header調整 https://qiita.com/emaame/items/f625464f3eb38f7850ab arm64追記(これはいらなかった。これがあることで、実機buildができなかった。。。。) http://blog.be-style.jpn.com/article/187942746.html 【追記2】 i386のみでよかった!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Firebase A/Bテストをiosで使ってみる

A/Bテストやってますか?QiitaのABテストタグの量を見るかぎりあまり使われていないのかな? GoogleのFirebaseにはA/Bテストを簡単に行える機能があります。これをiosアプリに実装する方法を書きたいと思います。以下の記事を参考にしました。 今回は画面に表示されたボタンのテキストをAとBで変えてクリックされる率をとるというものを作りたいと思います。 Firebaseにはいろいろな機能があるのですが、Remote ConfigとAnalyticsという機能を使うことでA/Bテストを実装します。 Remote Configは字のごとく設定ファイルをサーバから取得するものです。その設定ファイルに含まれている button_title = "クリック" のようなデータをアプリ側でボタンのテキストに設定してあげます。 そしてボタンがクリックされたときにAnalyticsを使ってイベントをサーバに投げることでクリック率の測定が行われます。 実装の大まかな流れはこうなります。 Firebaseにプロジェクトを作成 iosアプリを新規作成しFirebaseの設定をする FirebaseにA/Bテストを追加(Remote Configが追加される) iosでRemote Configを取得しボタンの表示を分ける ボタンが押されたらAnalyticsでイベントを呼ぶ Firebaseにプロジェクトを作成 Firebase console にアクセスしプロジェクトを作成します。 プロジェクト名はabtestとして次に進みます。 とくに変更することなく次へ進みます。 Googleアナリティクスアカウントを選択もしくは作成して次に進みます。 作成が完了するとプロジェクトのページが表示されます。 Firebaseにiosアプリを登録 次にiosアプリをFirebaseに登録します。 iosアプリのBundle Idenfitierを設定し次へ進みます。他のところは空欄でかまいません。 GoogleService-Info.plist をダウンロードしてiosアプリのプロジェクトに取り込みます。 iosアプリは Bundle Idenfitier を com.akira.abtest とし、最初のビューにボタンを配置し、 ViewController.swift からボタンへのアクセスとクリックされたときの関数を追加しておきます。 VewController.swift class ViewController: UIViewController { @IBOutlet weak var button: UIButton! override func viewDidLoad() { super.viewDidLoad() } @IBAction func onButton(_ sender: Any) { } } pod でアプリにFirebaseのライブラリを追加します。 $ pod init Podfile に Firebase/Core Firebase/RemoteConfig Firebase/Analytics を追加します。 Podfile # Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'abtest' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for abtest pod 'Firebase/Core' pod 'Firebase/RemoteConfig' pod 'Firebase/Analytics' target 'abtestTests' do inherit! :search_paths # Pods for testing end target 'abtestUITests' do # Pods for testing end end ライブラリをインストールして xcode を abtest.xcworkspace で開き直します。 $ pod install Firebaseの初期化をする必要があるのですが、これは後ほど説明します。 コンソールに戻ります。 A/Bテストの登録 アプリを登録するとA/Bテストを作れるようになっています。 Remote Configを作成します。 テスト名を button_click として次へ進みます。 登録したアプリを選択し100%の利用者でテストを有効にします。ユーザー数が多いサービスの場合は全体の30%などでテストを行うのもよいでしょう。 次に目標を設定します。ここではボタンがクリックされたときに呼ぶイベントを作成します。 button_click_event とします。定着率などを計測したい場合はそちらの項目を選択してください。 次にバリアントを設定します。バリアントとはつまりAとかBです。3パターンならCもあるでしょう。ここでは button_title というパラーメータに クリック と ?クリック? を設定します。これをアプリのボタンに設定して表示を分けます。 テストを開始します。 アプリでボタンのテキストを分ける 次はアプリにFirebase A/Bテストを入れていきます。まずはFirebase Remote Configを使うクラスを作ります。 RCValues.swift import Firebase class RCValues { enum ValueKey: String { case button_title } static let sharedInstance = RCValues() private init() { loadDefaultValues() fetchCloudValues() } func loadDefaultValues() { let appDefaults: [String: Any?] = [ ValueKey.button_title.rawValue : "クリック", ] RemoteConfig.remoteConfig().setDefaults(appDefaults as? [String: NSObject]) } func activateDebugMode() { let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 RemoteConfig.remoteConfig().configSettings = settings } func fetchCloudValues() { activateDebugMode() RemoteConfig.remoteConfig().fetch { [weak self] _, error in if let error = error { print("Uh-oh. Got an error fetching remote values \(error)") return } RemoteConfig.remoteConfig().activate { _, _ in } } } func string(forKey key: ValueKey) -> String { return RemoteConfig.remoteConfig()[key.rawValue].stringValue! } } 適当な場所に置きxcodeに取り込んでください。 RCValues はシングルトンで RCValues.sharedInstance でアクセスします。インスタンスが作成されると loadDefaultValues() で初期値を設定しfetchCloudValues() でFirebaseからRemote Configを取得してきます。 activateDebugMode() はデバック用のコードです。 minimumFetchInterval = 0 にすることでインタンス作成時に必ずRemote Configを取得してきます。未設定の場合は1日おきなど、ある程度時間が立たないと更新されません。 次にFirebaseとRCValuesを初期化します。 AppDelegate.swift class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() _ = RCValues.sharedInstance return true } FirebaseApp.configure() でFirebaseを初期化し、 _ = RCValues.sharedInstance でRCValuesのインスタンスを作成します。 最後にRemote Configの値を使ってボタンのテキストを変更し、ボタンがタップされたらAnalyticsでイベントを呼び出します。 ViewController.swift import UIKit import Firebase class ViewController: UIViewController { @IBOutlet weak var button: UIButton! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let buttonTitle = RCValues.sharedInstance.string(forKey: .button_title) button.setTitle(buttonTitle, for: .normal) } @IBAction func onButton(_ sender: Any) { Analytics.logEvent("button_click_event", parameters: nil) } } RCValues.sharedInstance.string(forKey: .button_title) でRemote Configで設定したテキストを取ってきてボタンに設定できます。 そしてボタンをタップされたときに Analytics.logEvent("button_click_event", parameters: nil) でイベントを送信します。これでどちらのボタンの方がクリック率が高いのかが測定できます。 これでアプリを何回か実行してみてください。半々の確率でテキストが変わると思います。 特定のデバイスにAとBを指定する 開発時にはランダムで表示されるよりもどちらかに固定したい場合があると思います。その場合はデバイスを登録して指定できます。 AppDelegate.swift class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() Installations.installations().authToken { result, _ in print("Your instance ID token is \(result?.authToken ?? "n/a")") } _ = RCValues.sharedInstance return true } FirebaseApp.configure() の直後に Installations.installations().authToken を出力してその値をコピーしてください。 次にFirebase Consoleでデバイスを登録します。 テストデバイスを管理を選択し 先程コピーしたトークンを貼り付け、バリアントを選択し、追加してから保存します。 これでどちらのテキストを表示するのか決めることができます。 おしまい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】地域の魅力を発信できるデジタルパンフレットサービス「Openパンフレット」を作った

はじめに デジタルパンフレット作成公開サービス Openパンフレット をリリースしました。 ​ 個人開発です。 ​ バックエンドはだいたいAWSです。 ​ Android未対応?​ ​バックエンドの技術者がフロントやらインフラやらアプリやら色んな所に手を出して作ったシステムになっています。 記事本文はZennで書いているのでこちらです。 宣伝 Android版の開発のためクラウドファンディング募集しています。 よかったら見て下さい。励みになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む