20210827のSwiftに関する記事は12件です。

UIButtonを変形とIndicatorが表示されてるようなアニメーション

今回の内容                                                 コードと簡単解説 Main.storyboardでUIButtonを作成しておきます. @IBOutlet weak var indicatorOnButton: UIButton! @IBAction func indicatorOn(_ sender: UIButton) { 押した時の処理 } indicatorOnButtonの見た目 indicatorOnButton.frame = CGRect(x: view.frame.minX + 20, y: view.frame.maxY / 8, width: view.frame.maxX - 40, height: indicatorOnButton.frame.size.height) indicatorOnButton.layer.cornerRadius = 20.0 indicatorOnButton.layer.shadowOffset = CGSize(width: 5, height: 5) indicatorOnButton.layer.shadowOpacity = 0.5 indicatorOnButton.layer.shadowRadius = 0.5 indicatorOnButtonを押した時の処理(AnimationとIndicator) indicatorOnButtonが押されてから、0.5秒で.frameと.layer.cornerRadiusを変更します。 別の場所で良いと思いますが、indicatorを作成します。.startAnimating()でindicatorを回します。 3秒間、indicatorを回した後に、.stopAnimating()でindicatorを止めます。 最後に、0.5秒で.frameと.layer.cornerRadiusを元の状態に戻します。 @IBAction func indicatorOn(_ sender: UIButton) { sender.setTitle("", for: .normal) UIButton.animate(withDuration: 0.5, delay: 0, options: .allowAnimatedContent, animations: { sender.frame = CGRect(x: self.view.center.x - (sender.frame.size.width / 7) / 2, y: sender.frame.origin.y, width: sender.frame.size.width / 7, height: sender.frame.size.height) sender.layer.cornerRadius = 25.0 }, completion: nil) let indicator = UIActivityIndicatorView() indicator.frame.origin = CGPoint(x: sender.center.x, y: sender.center.y) indicator.color = .white view.addSubview(indicator) indicator.startAnimating() DispatchQueue.main.asyncAfter(deadline: .now() + 3) { indicator.stopAnimating() UIButton.animate(withDuration: 0.5, delay: 0, options: .allowAnimatedContent, animations: { sender.frame = CGRect(x: self.view.frame.minX + 20, y: sender.frame.origin.y, width: self.view.frame.maxX - 40, height: sender.frame.size.height) sender.layer.cornerRadius = 20.0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {sender.setTitle("Indicator", for: .normal)} }, completion: nil) } } 終わり ご指摘、ご質問などありましたら、コメントまでお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で続きを読む

[Mac]Windowのどこをドラッグしても移動できるアプリの実装について

作成したアプリの紹介 Macのウィンドウの任意の場所をCMDキーを押しながらドラッグして、ウィンドウの移動やリサイズを可能にするアプリDokodemoDragを作成しました。 設計・実装の記録も兼ねて、技術要素の知見を共有します。 ※ DokodemoDragの実装は、Rectangleというキーボードショートカットでウィンドウ配置を行うアプリを参考にしています。 主な技術的要素 DokodemoDragが行っている事の概要は、 OSログイン時にアプリを自動起動し アプリをMacOSに常駐させ アプリ外のイベント(マウスイベント)を監視・取得し そのイベント情報をもとに、DokodemoDragアプリ外のウィンドウの情報を取得・操作する になります。 ここで紹介する技術要素は、上と順序が異なりますが マウス操作などのイベント監視 アプリ外のウィンドウ操作 常駐アプリ化 OS起動時の自動起動 です。次のそれぞれ解説します。 1. マウス操作などのイベント監視 アプリ外のイベント監視は、次のメソッドを利用することで実現できます。 NSEvent.addGlobalMonitorForEvents(matching:handler:) このメソッドは、アプリ外で発生したイベント(マウスイベントなど)を監視するためのメソッドです。 あくまでもイベントを監視するのみで、イベント内容を変更したり、イベントをキャンセル(元のアプリにイベントを発生させない)したりはできません。 キー関連のイベントを監視する場合は、macOSの「システム環境設定」「セキュリティとプライバシー」から「アクセシビリティ」が許可されている必要があります。 また監視できるイベントが限定されています。 (ドキュメントに監視可能なイベントの一覧が記載されています) DokodemoDragでは、MouseHookService.swift #24でイベント監視を開始するstartメソッドを定義し、アプリ起動時や機能を有効にした場合に監視を開始しています。(AppDelegate.swift #31) // MouseHookService.swift public func start() { stop() eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) { event in self.handleMouseEvent(event) } } public func stop() { guard let eventMonitor = eventMonitor else { return } NSEvent.removeMonitor(eventMonitor) self.eventMonitor = nil self.element = nil } 2. アプリ外のウィンドウ操作 App Sandboxの無効化とアプリにAccessibility APIの利用を許可する必要があります。 2.1. App Sandboxの無効 アプリ外のウィンドウの操作を行うために、Xcodeプロジェクトにある <プロジェクト名>.entitlementsファイルの項目App SandboxをNOにする にする必要があります。 App Sandboxは、ドキュメントに 「macOSアプリケーションのシステムリソースとユーザーデータへのアクセスを制限し、アプリケーションが侵害された場合の被害を抑制します。」 とあります。(DeepL翻訳) アプリ外のウィンドウの位置変更やリサイズはAccessibilityのAPIを利用する必要があり、このAPIを利用するためにApp Sandboxを無効にする必要があります。 但しApp Sandboxを無効にすると、そのアプリはMac App Storeから配信することはできなくなります。 App Sandboxを無効にした場合、Mac App Storeから配信することはできませんが、Developer IDを利用することでAppleの公証(Notarization)を受けることができ、macOSで安全にアプリを開くことができるようになります。参考: Mac でAppを安全に開く Accessibility APIを利用しているにも関わらず、一部のアプリ(Cinch, BetterSnapTool, PopClipなど)はStoreに配信されているようですが、これらはstack overflowのによると、Sandboxがリリースされる以前に公開されたアプリのようで特例のようです。 stackoverflow: How to use Accessibility with sandboxed app? (これらのアプリが、2021年現在もAccessibility APIの機能が利用可能なのかは把握していません) DokodemoDragでは、Rectangleで定義されているAccessibilityElementクラスを利用して、Accessibility APIを利用しています。 AccessibilityElementはElement単位での操作が用意されているので、私のようにAccessibility APIの詳細を把握できていなくても直感的に利用できるようになっています。 実際のウィンドウ移動/リサイズ箇所はMouseHookService.swift #49, #51 になります。 2.2 アクセシビリティの許可 アプリがAccessibility APIを利用するために、 このアプリをmacOSの「システム環境設定」「セキュリティとプライバシー」から「アクセシビリティ」の許可する必要があります。 3. 常駐アプリ化 LSUIElementの設定とステータスバーへのメニューの配置を行います。 3.1 LSUIElement Dockにも現れず、強制終了一覧にも表示されないアプリを作成するには、Info.plistでLSUIElementをYESにします。 3.2 ステータスバーへのメニュー配置 ステータスバーへのメニュー配置は、Rectangleの実装をベースとしています。StatusBarItem #56 また私自身Macアプリ開発については素人同然なので、常駐化の把握については「ステータスバー常駐アプリ」という記事を参考にしました。 4. OSログイン時のアプリ自動起動 MacでのOSログイン時の複数あるようで、実装の際には「 「ログイン時に起動」を実装する」とRectangleの実装を参考にしました。 記事には、自動起動の選択肢として Launch Services を使う方法と Helper Application を使う方法 などが紹介されています。 Launch Serviceを利用する場合、OSのログイン項目に登録する方法となっていて、システム環境設定の「ユーザとグループ」の「ログイン項目」に表示されます。 一方Helper Applicationは、主アプリのmain bundleのContents/Library/LoginItems/内にあるヘルパーアプリが、主アプリを起動する仕組みとなっています。 自動起動の注意点としては、自動ログインは、Mac App Storeのガイドラインにより、「ユーザへの確認なしの自動起動はしてはならない」ため、アプリ起動時や設定で、ユーザが自動起動を有効にする必要があります。 Mac App Store Guidelines(以下は原文まま) (iii) They may not auto-launch or have other code run automatically at startup or login without consent nor spawn processes that continue to run without consent after a user has quit the app. They should not automatically add their icons to the Dock or leave shortcuts on the user desktop. DokodemoDragでは、Rectangleの実装を参考にしてHelper Applicationによる実装となっています。 Helper Applicationによる実現方法の概要は 主アプリを起動するHelper Application(ここではDokodemoDragLauncher)を作成する。 Helper Applicationは、主アプリを起動する。(AppDelegate.swift #35) ※ 現状のコードでは警告が出ます。またこのアプリの起動方法はSandboxを無効にしている場合のみ動作します。 Helper ApplicationのInfo.plistの「Application is agent(UIElement)か「Application is background only」を有効にする 主アプリ側 Build PhaseにCopy Files Build Phaseを追加し、Contents/Library/LoginItems/にヘルパーアプリケーションを追加する SMLoginItemSetEnabledを呼び出す。(SettingService.swift #36) です。 DokodemoDragでは、このCopy Files Build Phaseの影響で「Developer Idを利用したNotarizationが行えない」という問題が発生していて、解決していません。 (ヘルパーアプリのコピー後に再度、codesignが必要なのだと思いますが検証できていません)。 関連:stackoverflow How can I add secondary files to my macOS .app archive and still pass Apple's notarization? つらつらHelpr Applicationについて書きましたが、自動起動についてはHerlp Applicationをライブラリ化されているsindresorhus/LaunchAtLoginを利用を検討した方が良いかもしれません。 (このライブラリは検証できていませんが、Notarizationが行えない問題もscriptによるcodesignで回避していそうに見えます) 記録も兼ねて長々書きましたが、常駐アプリを作成する際の参考になれば幸いです。 良ければDokodemoDragも触ってみてください。 以上!!
  • このエントリーをはてなブックマークに追加
  • 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追記 http://blog.be-style.jpn.com/article/187942746.html
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Swift バイナリツリー②

Swift バイナリツリー② 前回のバイナリツリーに挿入する,検索する関数 //挿入 public func insert(value: T) { //小さければ左に行く if value < self.value { //if let構文 if let left = left { left.insert(value: value) } else { //値がなければノードを作成し、親ノードを設定する left = BinarySearchTree(value: value) left?.parent = self } //右に行く場合 } else { //if let 構文 if let right = right { right.insert(value: value) } else { //値がなければノードを作成し、親ノードを設定する right = BinarySearchTree(value: value) right?.parent = self } } } //検索 public func search(value: T) -> BinarySearchTree? { //左をサーチする再帰処理 if value < self.value { return left?.search(value) } else if value > self.value { //右をサーチする再帰処理 return right?.search(value) } else { return self //発見! } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テキストフィールドにピッカービューを設定してリスト選択する方法

今回作成するものはこちら   そして今回の全てのソースはこちら。 import UIKit class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { @IBOutlet weak var ageTextField: UITextField! //ピッカービュー var pickerView: UIPickerView = UIPickerView() //120までの数字を格納した配列 var list = Array(0...120) override func viewDidLoad() { super.viewDidLoad() pickerView.delegate = self pickerView.dataSource = self textFieldをタップしたときにピッカービューが表示されるように変更 textField.inputView = pickerView } //表示する列の数 func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } //表示する数 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { //今回は120個 return list.count } //最初に表示される数字 func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String { 0〜120までの数字 return list[row] } //選択された時 func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { self.textField.text = list[row] }    それぞれ説明します。 class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { pickerViewを使用する際に、UIPickerViewDelegateとUIPickerViewDataSourceを使います。 なので作成したいクラスに上記を付け加えてください。   多分何も手を加えていなければ、初期状態はこんな感じだと思います。 class ViewController: UIViewController そこにUIPickerViewDelegateとUIPickerViewDataSourceを加えてください。   上記をつけた後にピッカービューの設定を加えていき var pickerView: UIPickerView = UIPickerView() pickerView.delegate = self pickerView.dataSource = self 次にtextFieldをタップしたときにpickerViewが表示されるように変更します textField.inputView = pickerView   もしtextFieldをOutlet接続してないと言う方はtextFieldをOutlet接続してください。 やり方がわからないと言う方はこちらを参考に学習しましょう。   上記が完了したらpickerViewで必要な関数を加えていきます。 //表示列 func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } //表示する数 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return list.count } //表示内容 func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String { return list[row] } //選択された時 func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { self.textField.text = list[row] } 上記の関数がないとエラーでビルドできません。   何か問題があればコメントをお願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

guard let について

今回は、よく出てくるguard let について調べてみたので、自分なりにわかりやすくまとめたいと思います 参考記事 https://programfromscratch.com/swift%E5%85%A5%E9%96%80-guard-let%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/ #どんな時になぜ使うのか guard letは、オプショナル型から値を安全に取り出したい時に使います。 Swiftでは、オプショナル型から値としてnilをそのまま取り出すと、エラーになってしまうので、 nilならこの処理、nilじゃなければこの処理と処理を明確にしなければいけないのです。 guard letは日本語訳すると『nilなら処理する』みたいなイメージだとわかりやすいです。 つまり、guard letは値がまだ決まっていない部分の変数宣言の時に使います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めてSwiftで電卓アプリを作った感想とわからなかった知識のまとめ

初めまして。今回は、初めてSwiftで電卓アプリを作った感想と知識のまとめをしていこうと思います。 まず今回で理解した知識は、オプショナル型についてです。 参考記事 オプショナル型 https://www.sejuku.net/blog/35070 https://wp-p.info/tpl_rep.php?cat=swift-biginner&fl=r17 ・オプショナル型 通常では持てない、値のない状態を保持する変数の型のことです。 つまり、オプショナル型は自動で最初からnilが変数として代入されるということです。 その時は、?をつけてコードを書きます。 ただし、nilではない形を持った変数を表すときは、アンラップする必要があるため、『!』をつける必要があります。 出ないと自動的にnilが代入されてしまっているので、エラーが起きてしまいます。 僕のイメージでは、nilをラップで包んでいて、そのラップを剥がしてnilと実体を保つ変数と入れ替えるイメージがアンラップです。 主に、オプショナル型はif型と用いて、『Binding』を行います。 Bindingとは、オプショナル型の、値がnilならfalse、それ以外ならtrueと返す特性を使って、比較することを言います。 感想 何もかもが初めてで、とにかく写経みたいな形になってしまって理解がとても浅い状態になってしまっているので、自分のローカル環境で改めて実践してみようと感じました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FrameとBoundsの違いを理解する

はじめに UIKitを触ったときにframeとかboudnsとかで情報が散乱していました。 今回はframe, boundsについて情報を整理します。 環境 Swift 5.4.2 Xcode 12.5.2 macOS Big Sur 11.5.2 frameとboundsの違い 画像で説明したらわかりやすかったです。 frameの場合、起点がルートビューの左上です。 一方、boundsの起点は、自身の左上でした。 frameの使用例 ViewController.swift // X方向の最小値を取得する view.frame.minX // X方向の中央値を取得する view.frame.midX // X方向の最大値を取得する view.frame.maxX // NavigationBarのHeightを取得する navigationController?.navigationBar.frame.height // TabBArのHeightを取得する tabBarController?.tabBar.frame.height boundsの使用例 ViewController.swift // スクリーンのサイズを取得する UIScreen.main.bounds NavigationBarの下にボタンを追加する frameを使って、NavigationBarの下にボタンを追加してみます。 ViewController.swift // NavigationBarのy座標を取得します let navBarBottom = navigationController.navigationBar.frame.maxY // Viewの幅を利用して、ボタンの幅を取得します let buttonWidth = view.bounds.width - 20 // UIButtonを定義する際に、NavigationBarのbottomを利用します let button = UIButton(frame: CGRect(x: 10, y: navBarBottom + 10, width: buttonWidth, height: 50)) ... view.addSubview(button) 参考リンク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む