- 投稿日:2021-12-03T23:14:16+09:00
Fat Singletonを救いたい【Swiftでデザインパターン攻略 #2】
はじめに 今回は「FatSingletonを救いたい」と題して、Singletonパターンについて書きました。 自分は、Singletonパターンを結構使うので、かなり馴染み深いデザインパターンです。 Singletonパターンは便利なのですが、気をつけて使わないと、何でもありコードになってしまうので、そのあたりを記事にできたらと思い書いてみました。 この記事で学べること SwiftのSingletonパターンの書き方 Singletonのメリット、デメリット FatSingletonにならないようにするためのリファクタリング <- Point UnitTestに都合の良いSingletonクラスにしておく <- Point Singletonパターンとは 概要 特定のクラスのインスタンスがアプリケーションの中で1つしか生成されないことを保証するデザインパターンのことです。 Wikipediaより SwiftにおけるSingletonパターン iOSアプリ開発においては、アプリ内の共通利用データを保持させるために用いられることが多いパターンです。 複数のクラスから参照が必要なものの、同じクラスの複数のインスタンスが乱立するとデータ整合性が保ちづらくなり、バグが発生することを防ぎたいときに使うことが多いです。 SessionManagerやLoginManagerがあるあるです。 メリット アプリ全体から利用できるため、アプリ内で共通利用したいデータをメモリに持たせたいときに活用することができます。 また、インスタンスが1つしか生成されないので、データの整合性が保証されます。 デメリット メリットの性質がそのまま仇となるのですが、「どこからでも参照可能」という便利がゆえに、本来の役割を超えたプロパティやメソッドが書かれてしまい、FatViewControllerならぬFatSingletonが完成されてしまいます。 また、Singletonクラスに色々なプロパティやメソッドを追加しすぎたあまり、知らない間に密結合なコードになり、UnitTestがままならなくなります。(←体験談) Singletonパターンの雛形を実装する // 1. 継承を防ぐためにfinalをつける final class FatDataStore { // 2. 外部から参照するためにstatic変数でインスタンスを返すプロパティを用意する static let shared = FatDataStore() // 3. 外部からインスタンス化されないためにinitをprivateにする private init() {} // 4. 必要に応じて変数を追加する(この2つの値はAPIなどから取得した値をSetするものとする) var realMoney = 0 var electricMoney = 0 // 5. 必要に応じてメソッドを追加する func calcTotalMoney() -> Int { return realMoney + electricMoney } func calcRealMoneyRatio() -> Int { return realMoney / calcTotalMoney() * 100 } func calcBalance(payMoney: Int) -> Int { return calcTotalMoney() - payMoney } } // 利用する時 print(FatDataStore.shared.realMoney) // 0 FatDataStore.shared.realMoney = 1000 print(FatDataStore.shared.realMoney) // 1000 1〜3は、Singletonクラスを作る上で定番のパターンで、とりあえずこのようにしておけばSigletonパターンとして使えると言って問題ないです。 "Fat" Singleton問題 結論から言うと、上記のSingletonクラスはFatSingletonになる危険性があります。 なぜなら、上記のSingletonクラスはあくまでDataStoreとしての位置づけですが、自身のプロパティを用いた計算、アクセス元から渡された引数を用いた計算もSingletonクラスで行われています。つまりこのクラスは、DataStoreとしての責務を超えて「なんでも実行クラス」になってしまっていると言えます。 このように、Singletonはアプリ内で共有する利用頻度の高いプロパティを持つことから、それに関するメソッドがSingletonクラス内に乱立してしまうという危険性があるということです。 というわけで、計算に関するメソッドはちゃんと計算クラスを定義することでFatSingletonを防ぐことができます。 class Calcrator { let dataStore: FatDataStore // Singletonクラスのプロパティを利用したければ初期化時にセットする init(dataStore: FatDataStore) { self.dataStore = dataStore } func calcTotalMoney() -> Int { return dataStore.realMoney + dataStore.electricMoney } func calcRealMoneyRatio() -> Int { return dataStore.realMoney / calcTotalMoney() * 100 } func calcBalance(payMoney: Int) -> Int { return calcTotalMoney() - payMoney } } UnitTestを考慮してもう一歩踏み込んでみる FatSingleton問題以外のもう1つのデメリットであるこれも解消してしまいましょう。 また、Singletonクラスに色々なプロパティやメソッドを追加しすぎたあまり、知らない間に密結合なコードになり、UnitTestがままならなくなります。(←体験談) サンプルコードのrealMoneyやelectricMoneyがAPIから取得した値をセットするなどという仕様の場合、UnitTest時には、本来期待される値が入っていないことが多いです。 全財産の合計計算、残高計算のテストをしたいのに、わざわざAPIを投げてサーバーから値を受け取ってテストするというのは、正しい単体テストのあり方ではありません。 そんな問題点を解決するためのコードがこれです。 // DataStoreProtocolを準拠させる final class SlimDataStore: DataStoreProtocol { static let shared = SlimDataStore() private init() {} var realMoney: Int = 0 var electricMoney: Int = 0 } class Calcrator { let dataStore: DataStoreProtocol // 引数で受け取る型を「DataStoreProtocol」に変更する init(dataStore: DataStoreProtocol) { self.dataStore = dataStore } func calcTotalMoney() -> Int { return dataStore.realMoney + dataStore.electricMoney } func calcRealMoneyRatio() -> Int { return dataStore.realMoney / calcTotalMoney() * 100 } func calcBalance(payMoney: Int) -> Int { return calcTotalMoney() - payMoney } } まず、DataStoreProtocolを定義して、Singletonクラスに必ず必要なプロパティやメソッドを確定させます。 そのプロトコルをSlimDataStoreというSingletonへ準拠させます。 最後に、Singletonを利用したいクラスでは、SlimDataStoreを直接受け取る形ではなく、DataStoreProtocolを初期化時の引数として受け取るようなロジックへ変更します。 こうすることで、UnitTestの時は、DataStoreProtocolを準拠したTestDataStoreクラスに差し替えるだけでいいので、計算テストが単体テストとして実行できるようになります。 class TestDataStore: DataStoreProtocol { var realMoney: Int = 11111 var electricMoney: Int = 88888 } let calcTest = Calcrator(dataStore: TestDataStore()) print(calcTest.calcTotalMoney()) // 99999 こんな感じです。 おわりに Singletonパターンはデザインパターンについて知る前から、プロジェクトでよく使われていました。 なんで、わりと思い入れのあるデザインパターンで長くなってしまいましたが、皆さんの参考になれば幸いです。 参考にさせていただいた記事
- 投稿日:2021-12-03T21:07:53+09:00
アイコン作成等のツールリンク集
たまたまどこかで知り、便利じゃん!と思ってブックマークに入れたは良いが、 本当に必要なときに結局どこにしまったか忘れているので、 個人的にたまに使ったりするツール系のリンクを備忘録としてまとめてみました。 Android/iOS AndroidAssetStudio(各種アイコン作成) MakeAppIcon(アイコン作成) resizeappicon(アイコン作成) (Android用)BundleToolを使用してaabファイルを実機にインストール 画像系 RemoveBackground(透過画像作成) MethodDraw(SVGエディタ) SvgPathEditor(SVGエディタ) フォント系 Google Fonts MaterialDesignIcons その他 Regexper(正規表現の可視化)
- 投稿日:2021-12-03T19:36:06+09:00
iOS アプリで使っている OSS の情報をいい感じにとる
CocoaPods で OSS を管理している iOS アプリで使っている OSS のリストを "いい感じ" に作ります。 お題 開発中の iOS アプリ内で使っている OSS を次の情報を含めてリストアップしたい。 OSS 名 バージョン ライセンス 公開サイト 作者 変なのが混入してないかとか、なんやかんやあるんです、はい。 解答 こうする。 解法 なんかいい感じのコマンドとかツールとかないか探した結果、スクリプトをちょこっと書くだけで済みそうだったのでそうしました。以下はその辺の説明です。 LicensePlist OSS のリストと言えば LicensePlist が思い浮かびます。アプリ上で OSS の表示義務を果たすのにとてもありがたいツールです。 ただ、出力が、、、 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>PreferenceSpecifiers</key> <array> <dict> <key>FooterText</key> <string>The AWS Mobile SDK for iOS is generally licensed under the Apache 2.0 License, with the AWSLex/Bluefront folder under the AWS Customer Agreement (https://aws.amazon.com/agreement/ ). </string> <key>Type</key> <string>PSGroupSpecifier</string> </dict> </array> </dict> </plist> バージョン番号がないし、ライセンス表記もフリースタイルすぎるので、、、ね。 pod コマンド そもそも CocoaPods で管理してるんで、pod コマンドでなんとかなるんちゃうの、と考えるのは至極自然です。でも世の中そんなに甘くはありませんでした。 今ご案内できる商品はこちらでございまーす。 $ pod search --simple --ios GoogleMaps -> GoogleMaps (5.1.0) Google Maps SDK for iOS. pod 'GoogleMaps', '~> 5.1.0' - Homepage: https://developers.google.com/maps/documentation/ios/ - Source: https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0.tar.gz - Versions: 5.1.0, 5.0.0, 4.2.0, 4.1.0, 4.0.0, 3.10.0, 3.10.0-beta1, 3.9.0, 3.8.2, 3.8.0, 3.7.0, 3.6.0, 3.5.0, 3.4.0, 3.3.0, 3.2.0, 3.1.0, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 2.7.0, 2.6.0, 2.5.0, 2.4.0, 2.3.1, 2.3.0, 2.2.0, 2.1.1, 2.1.0, 2.0.1, 2.0.0, 1.13.2, 1.13.1, 1.13.0, 1.12.3, 1.12.2, 1.12.1, 1.12.0, 1.11.1, 1.11.0, 1.10.5, 1.10.4, 1.10.3, 1.10.2, 1.10.1, 1.10.0, 1.9.2 [trunk repo] - Subspecs: - GoogleMaps/Base (5.1.0) - GoogleMaps/Maps (5.1.0) - GoogleMaps/M4B (5.1.0) -> GoogleMapsDirection (0.1.1) Wrapper around GoogleMaps Direction API. pod 'GoogleMapsDirection', '~> 0.1.1' - Homepage: https://github.com/Djengo/GoogleMapsDirection - Source: https://github.com/Djengo/GoogleMapsDirection.git - Versions: 0.1.1, 0.1.0, 0.0.2 [trunk repo] ... いやいや、先頭のポッドの情報だけ欲しいんだけど。 ではこちらでいかがでしょうか? $ pod trunk info GoogleMaps GoogleMaps - Versions: - 1.10.0 (2015-05-27 21:46:03 UTC) ... - 5.1.0 (2021-06-28 18:50:55 UTC) - Owners: - Google <cocoapods@google.com> ... これだとさっき出してた情報が削れちゃってるやん。 なるほどですねー。もしかして、お探しのものはこれですか? $ pod spec cat GoogleMaps { "authors": "Google, Inc.", "changelog": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0-CHANGELOG.md", "default_subspecs": [ "Maps" ], "description": "Use the Google Maps SDK for iOS to enrich your app with interactive maps, immersive Street View panoramas, and detailed information from Google's Places database.", "homepage": "https://developers.google.com/maps/documentation/ios/", "license": { "text": "Copyright 2021 Google", "type": "Copyright" }, "name": "GoogleMaps", "platforms": { "ios": "11.0" }, "preserve_paths": [ "Example/GoogleMapsDemos.xcodeproj/project.pbxproj", ... "SwiftExample/Podfile" ], "readme": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0-README.md", "source": { "http": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0.tar.gz" }, "summary": "Google Maps SDK for iOS.", "version": "5.1.0", "subspecs": [ { ... }, { "dependencies": { "GoogleMaps/Maps": [ ] }, "name": "M4B", "vendored_frameworks": [ "M4B/Frameworks/GoogleMapsM4B.framework" ] } ] } そうそう、こんな感じ。でも、コマンドの引数ってどうすんの? それはもうお客様の方で Podfile.lock をパースするなりしてご用意いただくしか、、、 pod プラグイン pod も何でもできる訳じゃないので、足りないものはプラグインで補充すればいいんですよ。 なんか期待しちゃうプラグインが 2 つ。試すしかない。 $ pod plugins Downloading Plugins list... ... -> Pod info Shows information on installed Pods. - Gem: cocoapods-podfile_info - URL: https://github.com/cocoapods/cocoapods-podfile_info ... -> CocoaPods Query Shows all CocoaPods in a project filtered by search term (author name, source file, dependency, etc.) - Gem: cocoapods-query - URL: https://github.com/square/cocoapods-query ... CocoaPods のプラグインは gem なので gem install でインストールします。CocoaPods は macOS ネイティブの Ruby の使用を推奨しているので、システムにインストールしちゃいます。 % pod podfile-info --all --csv Pods used: name,version,homepage,summary,license 何も出てこない。はい、残念。次。 $ pod query --to-yaml=pod-query.yml Loading targets... ... GoogleMaps ... $ cat pod-query.yml --- ... - :name: GoogleMaps :version: 4.2.0 :authors: - :name: Google, Inc. :is_local: false :root_directory: "/***/Pods/GoogleMaps" :license: :text: Copyright 2021 Google :type: Copyright :summary: Google Maps SDK for iOS. :description: Use the Google Maps SDK for iOS to enrich your app with interactive maps, immersive Street View panoramas, and detailed information from Google's Places database. :homepage: https://developers.google.com/maps/documentation/ios/ :uses_swift: false :readme_file: README.md :platforms: - :name: :ios :version: '10.0' :source_files: [] ... これやな。 最後の一手間 pod query の出力は情報過多なので、要らないものを削りたい。ていうか、このプラグインと同じデータの取り方をすれば欲しい情報だけを出力できるかも、と考えてソースコードを見てみます。150 行ほどありますが、知りたいことは all_targets() の 30 行ちょっとに集約されてます。これを参考にして冒頭のスクリプトができました。 という訳で、めでたし x2。
- 投稿日:2021-12-03T19:36:06+09:00
iOS アプリで使ってる OSS の情報をいい感じにとる
CocoaPods で OSS を管理している iOS アプリで使っている OSS のリストを "いい感じ" に作ります。 お題 開発中の iOS アプリ内で使っている OSS を次の情報を含めてリストアップしたい。 OSS 名 バージョン ライセンス 公開サイト 作者 変なのが混入してないかとか、なんやかんやあるんです、はい。 解答 こうする。 解法 なんかいい感じのコマンドとかツールとかないか探した結果、スクリプトをちょこっと書くだけで済みそうだったのでそうしました。以下はその辺の説明です。 LicensePlist OSS のリストと言えば LicensePlist が思い浮かびます。アプリ上で OSS の表示義務を果たすのにとてもありがたいツールです。 ただ、出力が、、、 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>PreferenceSpecifiers</key> <array> <dict> <key>FooterText</key> <string>The AWS Mobile SDK for iOS is generally licensed under the Apache 2.0 License, with the AWSLex/Bluefront folder under the AWS Customer Agreement (https://aws.amazon.com/agreement/ ). </string> <key>Type</key> <string>PSGroupSpecifier</string> </dict> </array> </dict> </plist> バージョン番号がないし、ライセンス表記もフリースタイルすぎるので、、、ね。 pod コマンド そもそも CocoaPods で管理してるんで、pod コマンドでなんとかなるんちゃうの、と考えるのは至極自然です。でも世の中そんなに甘くはありませんでした。 今ご案内できる商品はこちらでございまーす。 $ pod search --simple --ios GoogleMaps -> GoogleMaps (5.1.0) Google Maps SDK for iOS. pod 'GoogleMaps', '~> 5.1.0' - Homepage: https://developers.google.com/maps/documentation/ios/ - Source: https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0.tar.gz - Versions: 5.1.0, 5.0.0, 4.2.0, 4.1.0, 4.0.0, 3.10.0, 3.10.0-beta1, 3.9.0, 3.8.2, 3.8.0, 3.7.0, 3.6.0, 3.5.0, 3.4.0, 3.3.0, 3.2.0, 3.1.0, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 2.7.0, 2.6.0, 2.5.0, 2.4.0, 2.3.1, 2.3.0, 2.2.0, 2.1.1, 2.1.0, 2.0.1, 2.0.0, 1.13.2, 1.13.1, 1.13.0, 1.12.3, 1.12.2, 1.12.1, 1.12.0, 1.11.1, 1.11.0, 1.10.5, 1.10.4, 1.10.3, 1.10.2, 1.10.1, 1.10.0, 1.9.2 [trunk repo] - Subspecs: - GoogleMaps/Base (5.1.0) - GoogleMaps/Maps (5.1.0) - GoogleMaps/M4B (5.1.0) -> GoogleMapsDirection (0.1.1) Wrapper around GoogleMaps Direction API. pod 'GoogleMapsDirection', '~> 0.1.1' - Homepage: https://github.com/Djengo/GoogleMapsDirection - Source: https://github.com/Djengo/GoogleMapsDirection.git - Versions: 0.1.1, 0.1.0, 0.0.2 [trunk repo] ... いやいや、先頭のポッドの情報だけ欲しいんだけど。 ではこちらでいかがでしょうか? $ pod trunk info GoogleMaps GoogleMaps - Versions: - 1.10.0 (2015-05-27 21:46:03 UTC) ... - 5.1.0 (2021-06-28 18:50:55 UTC) - Owners: - Google <cocoapods@google.com> ... これだとさっき出してた情報が削れちゃってるやん。 なるほどですねー。もしかして、お探しのものはこれですか? $ pod spec cat GoogleMaps { "authors": "Google, Inc.", "changelog": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0-CHANGELOG.md", "default_subspecs": [ "Maps" ], "description": "Use the Google Maps SDK for iOS to enrich your app with interactive maps, immersive Street View panoramas, and detailed information from Google's Places database.", "homepage": "https://developers.google.com/maps/documentation/ios/", "license": { "text": "Copyright 2021 Google", "type": "Copyright" }, "name": "GoogleMaps", "platforms": { "ios": "11.0" }, "preserve_paths": [ "Example/GoogleMapsDemos.xcodeproj/project.pbxproj", ... "SwiftExample/Podfile" ], "readme": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0-README.md", "source": { "http": "https://dl.google.com/dl/cpdc/3cd76dc5e4525f42/GoogleMaps-5.1.0.tar.gz" }, "summary": "Google Maps SDK for iOS.", "version": "5.1.0", "subspecs": [ { ... }, { "dependencies": { "GoogleMaps/Maps": [ ] }, "name": "M4B", "vendored_frameworks": [ "M4B/Frameworks/GoogleMapsM4B.framework" ] } ] } そうそう、こんな感じ。でも、コマンドの引数ってどうすんの? それはもうお客様の方で Podfile.lock をパースするなりしてご用意いただくしか、、、 pod プラグイン pod も何でもできる訳じゃないので、足りないものはプラグインで補充すればいいんですよ。 なんか期待しちゃうプラグインが 2 つ。試すしかない。 $ pod plugins Downloading Plugins list... ... -> Pod info Shows information on installed Pods. - Gem: cocoapods-podfile_info - URL: https://github.com/cocoapods/cocoapods-podfile_info ... -> CocoaPods Query Shows all CocoaPods in a project filtered by search term (author name, source file, dependency, etc.) - Gem: cocoapods-query - URL: https://github.com/square/cocoapods-query ... CocoaPods のプラグインは gem なので gem install でインストールします。CocoaPods は macOS ネイティブの Ruby の使用を推奨しているので、システムにインストールしちゃいます。 % pod podfile-info --all --csv Pods used: name,version,homepage,summary,license 何も出てこない。はい、残念。次。 $ pod query --to-yaml=pod-query.yml Loading targets... ... GoogleMaps ... $ cat pod-query.yml --- ... - :name: GoogleMaps :version: 4.2.0 :authors: - :name: Google, Inc. :is_local: false :root_directory: "/***/Pods/GoogleMaps" :license: :text: Copyright 2021 Google :type: Copyright :summary: Google Maps SDK for iOS. :description: Use the Google Maps SDK for iOS to enrich your app with interactive maps, immersive Street View panoramas, and detailed information from Google's Places database. :homepage: https://developers.google.com/maps/documentation/ios/ :uses_swift: false :readme_file: README.md :platforms: - :name: :ios :version: '10.0' :source_files: [] ... これやな。 最後の一手間 pod query の出力は情報過多なので、要らないものを削りたい。ていうか、このプラグインと同じデータの取り方をすれば欲しい情報だけを出力できるかも、と考えてソースコードを見てみます。150 行ほどありますが、知りたいことは all_targets() の 30 行ちょっとに集約されてます。これを参考にして冒頭のスクリプトができました。 という訳で、めでたし x2。
- 投稿日:2021-12-03T17:19:48+09:00
【Flutter】バッテリーの充電状態を検知する方法
本記事はFlutter Advent Calendar2021 3日目の記事です。 スマホを使うにあたって、切っても切れない関係にあるのがバッテリーですよね。 今回はFlutterでバッテリーの充電状態を検知する方法を提供するパッケージ、 battery_plusを紹介します。 こちらについて日本語での解説記事が見つからないため、 今回紹介しようと考えた次第です。 本記事では、 buttery_plusパッケージでできること 実装サンプル を記載します。 参考になれば幸いです。 buttery_plusパッケージでできること buttery_plusパッケージでできることは以下の3つです。 充電量(%)を取得する 充電状態(後述)の変化を検知する パワーセーフモードか否かを取得する やれることはシンプルですね。 一つ一つ解説していきます。 充電量(%)を取得する batteryLevelのメソッドで現在の充電量を%で取得します。 このメソッドの返り値はFuture型なのは注意です。 //Batteryのインスタンスを取得 battery = Battery(); //充電量をawaitで取得 int batteryLevel = await battery.batteryLevel; 充電状態の変化を検知する 充電状態(BatteryState)はenumで定義されており、 以下の4つの状態があります。 charging:充電中の状態 discharging:充電されておらず、バッテリーが減っている状態 full:バッテリーが満タンの状態 unknown:充電状態が検知できない状態 これらの変化を検知するメソッドonBatteryStateChangedが用意されています。 返り値がStream型なのがポイントです。 (使用例は実装サンプルをご覧ください。) パワーセーフモードか否かを取得する スマホがパワーセーフモードか否かを取得します。 取得はisInBatterySaveModeのメソッドで行います。 これも返り値がFuture型なのに注意です。 //Batteryのインスタンスを取得 battery = Battery(); //パワーセーブモードか否かをawaitで取得 bool isInPowerSaveMode = await _battery.isInBatterySaveMode; 実装サンプル 実装サンプルを紹介します。 公式のExampleをベースに、 不要部分の削除、日本語のコメントを追加したものとなります。 まず最初にpubspec.yamlに以下のコードを追加し、pub getを行なってください。 (バージョンはパッケージの紹介ページを見て最新に更新してください。) battery_plus: ^2.0.1 サンプルコード import 'dart:async'; import 'package:flutter/material.dart'; import 'package:battery_plus/battery_plus.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Battery Plus Demo', home: BatteryPage(title: 'Battery Plus Demo'), ); } } class BatteryPage extends StatefulWidget { const BatteryPage({Key? key, this.title}) : super(key: key); final String? title; @override _BatteryPageState createState() => _BatteryPageState(); } class _BatteryPageState extends State<BatteryPage> { //バッテリーのインスタンスを取得 final Battery _battery = Battery(); //バッテリーの状態(充電中等)をStateとして保持 BatteryState? _batteryState; //StreamSubscriptionで監視する(初期化はinitStateで行う) StreamSubscription<BatteryState>? _batteryStateSubscription; @override void initState() { super.initState(); //Batteryの状態の変化を検知し、setStateするように設定 _batteryStateSubscription = _battery.onBatteryStateChanged.listen((BatteryState state) { setState(() { _batteryState = state; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //バッテリーの状態を表示 Text('$_batteryState'), ElevatedButton( onPressed: () async { //充電残量の取得 final batteryLevel = await _battery.batteryLevel; showDialog<void>( context: context, builder: (_) => AlertDialog( content: Text('Battery: $batteryLevel%'), actions: <Widget>[ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('OK'), ) ], ), ); }, child: const Text('Get battery level'), ), ElevatedButton( onPressed: () async { //パワーセーブモードか否かの取得 final isInPowerSaveMode = await _battery.isInBatterySaveMode; showDialog<void>( context: context, builder: (_) => AlertDialog( content: Text('Is on low power mode: $isInPowerSaveMode'), actions: <Widget>[ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('OK'), ) ], ), ); }, child: const Text('Is on low power mode')) ], ), ), ); } @override void dispose() { super.dispose(); //Subscriptionのストップ if (_batteryStateSubscription != null) { _batteryStateSubscription!.cancel(); } } } ポイント StreamSubscription<BatteryState>で充電状態の検知を行なっているのがポイントです。 listenでリスナーを設定し、 充電状態が変化するたびにsetStateされるように設定しています。 disposeでSubscriptionを停止するのを忘れないようにしましょう。 実行時の注意点 このアプリを実行するときは、flutter run --releaseで実行することをお勧めします。 iPhoneでDebugモードで実行すると接続コードを抜いた瞬間にアプリが落ちてしまいます。 releaseモードで実行することで、接続コードを抜いてもアプリを使用することができます。 充電コードの抜き差しを確認するには必要テクニックですので、ぜひやってみてください。 GitHub 本記事で紹介したサンプルコードはGitHubにて公開しています。 気軽にクローンいただければと思います。 まとめ 今回はFlutterでバッテリー周りの情報を提供するパッケージ、battery_plusの紹介を行いました。 buttery_plusパッケージでできること 実装サンプル を記載しました。 バッテリーアプリを作成することはなかなかないかもしれませんが、 バッテリーの数値を表示する、とか、 充電されている時に〜するとか、 実装できることを知っているだけでも アプリ制作の幅が広がって良いかと思います。 Flutterのアドベントカレンダーで3日目がちょうど空いていたので書いた記事でしたが、いかがでしたでしょうか。 本記事が参考になれば幸いです。
- 投稿日:2021-12-03T14:48:33+09:00
Swifアプリケーション開発に、NoSQLドキュメント指向モバイルデータベースCouchbase Liteを使ってみる
はじめに 本稿では、Swiftアプリケーション開発に、NoSQLドキュメント指向モバイルデータベース Couchbase Liteを使うための具体的な方法について解説します。 本稿では、Couchbase Lite 3.0.0ベータ版をKotlinアプリケーションで利用する際の動作確認までを行います。 Couchbaseをモバイルアプリケーションで利用する意義については、以下の記事をご参考ください。 確認環境 macOS Catalina Version 10.15.7 Xcode Version 12.4 実行手順 プロジェクト作成・Couchbase Liteインストール 本稿では、Swiftパッケージマネージャーを使用して、Couchbase Liteをセットアップする手順を紹介します(Swiftパッケージマネージャーを使用してCouchbaseLiteSwiftをインストールするにはXcode12 +が必要です)。 Xcodeで、新しいプロジェクトを作成します:File > New > Project  今回は、「SwiftUI」ではなく、「UIKit」を使います。  下記のように、Swift Packages画面を表示します。  「+」アイコンを押下し、「Choose Package Repository」ダイアログを表示します。 入力欄に次のURLを入力します。 https://github.com/couchbase/couchbase-lite-swift-ee.git 「Next」ボタンを押下します。  「Choose Package Options」ダイアログが表示されます。 下記のように「Version」入力欄に、3.0.0-beta02と入力します。 「Next」ボタンを押下します。 パッケージ追加が始まります。 下記のように、「CouchbaseLiteSwift」パッケージが表示されます。チェックし、「Finish」ボタンを押下します。  下記のように、「CouchbaseLiteSwift」パッケージがプロジェクトに登録されます。 追加されたパッケージの名前、バージョン、URLが表示されています。  これで、CouchbaseLiteSwiftをインポートして、アプリで使用できます。 プログラミング・実行結果確認 ソースコード編集 今回は、稼働確認のため、ViewController.swiftを以下のように編集しました。 import UIKit import CouchbaseLiteSwift class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. getStarted(testReplication: false) } func getStarted (testReplication: Bool) { // データベース作成または取得(再実行時) let database: Database do { database = try Database(name: "mydb") } catch { fatalError("Error opening database") } // ドキュメント作成 let mutableDoc = MutableDocument() .setString("user", forKey: "type") // ドキュメント保存 do { try database.saveDocument(mutableDoc) } catch { fatalError("Error saving document") } // ドキュメント取得、変更、保存 if let mutableDoc = database.document(withID: mutableDoc.id)?.toMutable() { mutableDoc.setString("田中", forKey: "name") do { try database.saveDocument(mutableDoc) let document = database.document(withID: mutableDoc.id)! print("ドキュメント更新完了 - ドキュメントID: \(document.id), 名前: \(document.string(forKey: "name")!)") } catch { fatalError("Error updating document") } } // クエリ print("クエリ実行") let query = QueryBuilder .select(SelectResult.all()) .from(DataSource.database(database)) .where(Expression.property("type").equalTo(Expression.string("user"))) do { let result = try query.execute() print("ドキュメント数 : \(result.allResults().count)") } catch { fatalError("Error running the query") } } } ログ 以下のログが出力されます。 ドキュメント更新完了 - ドキュメントID: -YamrzlmIFovzOXYUp1YWDo, 名前: 田中 クエリ実行 ドキュメント数 : 1 データベースファイル ターミナルで、作成されたデータベースファイルを確認します。 $ cd ~/Library/Developer/CoreSimulator/Devices $ find . -name "mydb.cblite2" ./5C245945-C956-495A-B7CB-D646905E95C7/data/Containers/Data/Application/4E0B4959-9D0B-4181-A2E6-F3D3A233D5A9/Library/Application Support/CouchbaseLite/mydb.cblite2 最後に 本稿では、Swiftアプリケーション開発のために、プロジェクトにCouchbase Liteをダウンロードし、基本的な操作が行えるところの確認までを行いました。 モバイルアプリケーション開発に組込データベースを使った経験のある方であれば、以降の応用は、比較的簡単に可能かと思います。 最後に、さらなるステップに進むための情報を示して、本稿の締め括りとしたいと思います。 Couchbase Liteについての記事を以下の投稿で整理していますので、ご関心に応じて、参照してみてください。 本稿の記事の内容(インストール、動作確認)に関する、一次情報として、下記のドキュメントがあります(Swiftパッケージマネージャーを使う以外の方法も説明されています)。 Couchbase Lite(のみを「スタンドアローン」で)を使ったチュートリアルとして、以下が公開されています。 上記チュートリアルからはじめ、さらに進んだトピックとして、「クエリ」、「Couchbase Serverとの同期」、「バックグラウンドでのフェッチ」までがカバーされています。 また、上記とは別に、サンプルアプリケーションを動作させる環境の構築を通じて、Couchbase Serverとのデータ同期をカバーした、以下のチュートリアルがあります。 上述のチュートリアルは、全てCouchbase社によるものでしたが、それとは別に下記のようなチュートリアルも公開されています(こちらも、Couchbase Serverとの同期までをカバーしています)。 本稿で扱っていない、Couchbase Serverについては、日本語で読むことができるまとまった情報として、次の拙著を紹介させていただきます。
- 投稿日:2021-12-03T14:14:41+09:00
Alamofire+Codableをasync/awaitを使ってリクエストする
Swiftにも5.5でついにSwift Concurrencyが導入されたので一番有用そうなAPI取得周りをasync/awaitで記述できるようにしてみます。 今回はSwiftの通信ライブラリとしてスタンダードであろうAlamofire+Codableを環境として用いて実装しています。 (そのうちAlamofire公式にもasync/awaitで受け取れる口は出来るとは思いますが現時点ではiOS15以降が要求されるのでしばらくは入らないかと思います) AlamofireのExtensionを記述する Alamofireはクロージャで結果を受け取とれるので、これをasync/awaitに変換します。 旧来のクロージャ構文を変換するにはwithCheckedThrowingContinuationを利用します。 DataRequestExtension.swift import Foundation import Alamofire extension DataRequest { func publish<T>(_ type: T.Type) async throws -> T where T : Decodable { try await withCheckedThrowingContinuation { continuation in self.response { response in switch response.result { case .success(let element): do { let decodedResponse = try JSONDecoder().decode(type, from: element!) continuation.resume(returning: decodedResponse) } catch { continuation.resume(throwing: error) } case .failure(let error): continuation.resume(throwing: error) } } } } } continuation.resumeを呼ぶことで呼び出し元へ結果を返して処理をすすめる事ができます。 実際に呼び出してみる あとは生やしたメソッドを呼び出すだけでasync/awaitへの変換は完了します。 普段どおりCodableを実装したstructを用意してください。 GithubUser.swift import Foundation struct GithubUser: Codable, Equatable, Identifiable { let id: Int let name: String let htmlUrl: String let avatarUrl: String enum CodingKeys: String, CodingKey { case id = "id" case name = "name" case htmlUrl = "html_url" case avatarUrl = "avatar_url" } } その上で、Alamofireのリクエストクラスから作成したpublish()を呼びだしてください。 GithubApi.swift struct GithubApi { func fetchUser(userName: String) async throws -> GithubUser { try await AF.request("https://api.github.com/users/\(userName)").publish(GithubUser.self) } } withCheckedThrowingContinuationとwithCheckedContinuation 上記ではクロージャの変換用にwithCheckedThrowingContinuationを紹介しましたが、 これとは別に withCheckedContinuationという関数もあります。 こちらはErrorをThrowすることがない場合に利用できます。こちらの関数ではtryの指定が不要になるので処理が失敗しうるかどうかで使い分けてください!
- 投稿日:2021-12-03T14:01:26+09:00
SwiftUIを実務で使ってわかったアレコレ
この記事はGoodpatchエンジニアアドベントカレンダー5日目の記事です。 こんにちは!iOSエンジニアのとうようです。2日目の記事も書いているのですが、あまり技術的な内容は触れなかったのと、まだ空いていたので急遽この枠で技術的な記事も書いていこうということにしました。 多分このアドベントカレンダーはほとんどQiitaに書く人がいないので、一個ぐらいはQiitaに書き残しておこうかなとここで書き進めています。 今回書く内容は、自分が実際に参加した案件での実例を通したSwiftUI周りのお話になります。実際どのアプリなのかは伏せさせていただきますが、以下のような前提条件での学びになるためそこを踏まえた上で、実プロダクトに現段階でSwiftUIをどう取り入れるべきなのか?といったところの参考にしてもらえればいいなと思います。 途中までiOS13対応も考慮されていた アプリのリニューアルに際して導入されたため、ベースはUIKitであり、実際の見た目の部分をUIHostingControllerを使ってSwiftUIで実装している また、画面遷移はStoryboard + Wireframeパターン1で作られており、全画面にStoryboardファイルが存在している また、自分が担当した役割の性質上、デザイン寄りの話題もだいぶ多いことも踏まえた上でお読みいただければと思います。 Listのセパレーターを消すには UITableViewのような表示をしたい時、Listだとセパレーターが入ってしまいます。普通であれば別に問題ないですが、デザイン上消したいということもあるでしょう。そのようなときどのような方法があるでしょうか? まず、通常コンポーネントで各iOSでの解決法を見ていきます。 解決法 iOS13 iOS14 iOS15 Listのままの解決法 なし なし .listRowSeparator(.hidden)を使う それ以外の方法 VStackを使う LazyVStackを使う LazyVStackを使う この表から分かるように、iOS15以降でしか正式な手段は提供されていません。 またそれ以外の方法もLazyVStackを使えるiOS14以降はいいですが、iOS13だとただのVStackになってしまうため大量の要素を表示する際のパフォーマンス面が気になります。 これらを全てのバージョンについて解決する方法としては、二つの手法を組み合わせるアプローチを取りました。 一個が、基本的に内部でUIKitがレンダリングに使われているiOS13に対応するためのSwiftUI-Introspect、もう一つがiOS14のためのちょっとしたハックModifierです。 iOS13については完全にUITableViewのパラメータをいじらなければ見た目を変えることができないのですが、もちろん外側から普通にやっていじることはできません。そこで再帰関数を用いて中のUIKitのパーツまでいきいじれるようにするのがSwiftUI-Introspectです。ライブラリを導入して、.introspectTableView { tableView in }というModifierで中のUITableViewのあれこれをいじることができます。 iOS14は少し厄介です。iOS15のように設定できるものがないのですが、レンダリングはSwiftUI独自のものになっているため、Introspectでいじっても見た目には反映されません。 大人しくLazyVStackを使えという話ではあるのですが、iOS13対応をしていると単純にそうもいかないので、Listのまま解決する方法がAppleのフォーラムに上がっています。 具体的には以下のようなViewModifierを用意することになります。 struct HideRowSeparatorModifier: ViewModifier { static let defaultListRowHeight: CGFloat = 44 var insets: EdgeInsets var background: Color init(insets: EdgeInsets, background: Color) { self.insets = insets var alpha: CGFloat = 0 if #available(iOS 14, *) { UIColor(background).getWhite(nil, alpha: &alpha) assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.") } self.background = background } func body(content: Content) -> some View { content .padding(insets) .frame(minWidth: 0, maxWidth: .infinity, minHeight: Self.defaultListRowHeight, alignment: .leading) .listRowInsets(EdgeInsets()) .background(background) } } これは何をやっているのかを図解してみたものがこちらです。 SwiftUIのListのレンダリングの要素としては、中のコンテンツ、そしてListRowというものがあると考えられます(完全に推測です) さまざまなModifierはこのListRowに対しての設定がされていると見ていいでしょう。セパレーターも同様です。そして全般的に重なり順としてはコンテンツが上に来るようになっています。 そこで.listRowInsetsを0にしてあげるとコンテンツが目一杯に広がり、透過されていない限りセパレーターを隠してくれます。 唯一解決できないのは全体の一番上についているセパレーターです。下に引っ張らないと出てこない部分ではあるのですが、こちらは色々調整してみたものの消すことはできませんでした。 この工夫により無事セパレーターを全バージョンListのまま消すことが叶ったのですが、いかんせんこのModifierの中で余白などを指定することになるのでレイアウトの調整が煩雑になります。iOS13を切れるならiOS13を切って、LazyVStackを使う方が幸せになれると思います。 スクロール量を検知する 続いてはスクロール量を検知する際の話です。基本的にSwiftUIでこのような座標などをとる操作をするときにはGeometryReaderというものを使うのですが、これに関してiOS13とiOS14以降で少し挙動が変わるため注意が必要でしたという話です。 いくつかの記事ではスクロール量をとる際に一つのGeometryReaderを使ってスクロール量を直接検知しようとするコードがあるのですが、ここに少し罠があります。 何かというと、以下のようにスクロール量として取れるoffsetの基準がiOSバージョンによって変わってしまうのです。 特にLarge Titleがある画面だと注意が必要になります。そこでこちらの記事のように二個のGeometryReaderを使って差分を取ることでこのバージョン差を無くすという解決法に至りました。 struct TrackableScrollView<Content: View>: View { private let axes: Axis.Set private let showIndicators: Bool private let content: Content private let onChangeOffset: (CGFloat) -> Void init( _ axes: Axis.Set = .vertical, showIndicators: Bool = true, onChangeOffset: @escaping (CGFloat) -> Void, @ViewBuilder _ content: () -> Content ) { self.axes = axes self.showIndicators = showIndicators self.onChangeOffset = onChangeOffset self.content = content() } var body: some View { GeometryReader { outsideProxy in ScrollView(axes, showsIndicators: showIndicators) { content .background(GeometryReader { insideProxy in Color.clear.preference( key: ScrollViewOffsetKey.self, value: calculateContentOffset(from: outsideProxy, insideProxy: insideProxy) ) }) .onPreferenceChange(ScrollViewOffsetKey.self) { onChangeOffset($0) } } } } private func calculateContentOffset(from outsideProxy: GeometryProxy, insideProxy: GeometryProxy) -> CGFloat { if axes == .vertical { return outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY } else { return outsideProxy.frame(in: .global).minX - insideProxy.frame(in: .global).minX } } } private struct ScrollViewOffsetKey: PreferenceKey { typealias Value = CGFloat static var defaultValue: Value = CGFloat.zero static func reduce(value: inout Value, nextValue: () -> Value) { value += nextValue() } } ただしこれをListにも応用したいというときは注意が必要です。これはあくまでスクロールする中のコンテンツに透明な背景色をつけ、その要素の座標をとっているだけです。そのため、Listのようなものの場合一番上にくる要素にこの座標取得用の背景をつけるようにしないと正しい座標を取れなくなってしまいます。コンポーネントとして切り出すのはやや難しい対応になってしまうので、その場合は個別実装することになるでしょう。 NavigationBarのあれこれ SwiftUIをUIHostingControllerで使う際、NavigationBarの扱いは少しややこしくなります。 UIKitかSwiftUIのどちらかによっていれば起きにくいことではあるのですが、以下の問題がありました。 NavigationBarを隠す設定がUIHostingControllerのデフォルト挙動で上書きされてしまう問題 ScrollView/Listが厳密に最背面にいないとLarge Titleなどスクロールによって挙動が変わる機能が正常に働かない問題 一個ずつ見ていきます。 NavigationBarを隠す設定がUIHostingControllerのデフォルト挙動で上書きされてしまう問題 何らかの理由でNavigationBarを隠したい時、SwiftUI + UIHostingControllerだとUIKit側のライフサイクルで設定しても反映されないという現象があります。iOS14まではこのワークアラウンドとして、SwiftUI側で.navigationBarHidden(true)をするという解決法がよく言われていましたがこれがiOS15から効かなくなりました。 これに関してよくよく探っていくと、どうやらUIHostingControllerのライフサイクルにNavigationBarを表示するような動作が入っており、その実行が親のUIViewControllerのライフサイクルの後になってしまうためにうまくいってないようでした。 逆に言えば、UIHostingControllerのライフサイクルのタイミングで諸々の設定ができれば良さそうです。そのためにこのようなクラスを用意することで解決することができました。 class CustomHostingController<Content>: UIHostingController<AnyView> where Content: View { private var onViewWillAppear: (() -> Void)? private var onViewWillDisapper: (() -> Void)? public init(onViewWillAppear: (() -> Void)?, onViewWillDisapper: (() -> Void)?, rootView: Content) { self.onViewWillAppear = onViewWillAppear self.onViewWillDisapper = onViewWillDisapper super.init(rootView: AnyView(rootView)) } @available(*, unavailable) @MainActor @objc dynamic required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) onViewWillAppear?() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) onViewWillDisapper?() } } このCustomHostingControllerはviewWillAppearとviewWillDisappearで実行してほしい内容を渡すことができるため上書きされずに対処が可能です。 もちろん他のライフサイクルでも何か実行したいことがあれば拡張することもできるでしょう。中でどのようなデフォルト動作が組まれているかは謎ですが、もし何かうまく設定できないということがあればこちらを試してみるのもおすすめです。 ScrollView/Listが厳密に最背面にいないとLarge Titleなどスクロールによって挙動が変わる機能が正常に働かない問題 こちらの問題は実はUIKitにも存在しているものです。スクロールする要素が最背面かつセーフエリアなど画面一番上まで領域が広がっていないと、Large Titleがスクロールに合わせてしまわれなかったり、iOS15だとスクロールしてもNavigationBarの背景色がつかずに透明なままになったりします。 UIKit + SwiftUIではこのためにいくつかチェックする必要のある項目がありました。 SwiftUIをアタッチするViewの後ろに別のViewがないか ScrollView/Listが画面上部まで広がっているか?またZStackなどで後ろに他の要素がある状態になっていないか ScrollView/Listにbackgroundがついていないか 特に最後のポイントが少し厄介です。この問題でいう重ね順にはbackgroundでつけた要素も別個のものとしてカウントされてしまいます。そのためもし背景色をつけたい場合は、UIHostingController().view.backgroundColorでUIKit側から設定するようにしなければなりません。 実は存在した、iPhone世代間の挙動の差異 他にもさまざまあるのですがあまりにも長くなり過ぎてしまうのでこれで最後のトピックにしようと思います。最後に取り上げるのはiPhoneの世代間の差異です。iOSのバージョン違いやiPhoneのサイズの違い、あるいはセーフエリアの有無とかいう話ではありません。 一番顕著だったところで言うと、iPhone11までとiPhone12以降でいくつかの挙動が変わっていたのでそれを最後に紹介したいと思います。 今回発見しているのは以下の二点です。ただ、これがある以上注意深く検証すれば他にもあるかもしれません。 Pickerの標準の大きさが違う Textのトランケーションされる基準が違う この二つはほとんど同じ原因の問題とも考えられます。実はiPhone12以降、若干標準コンポーネントの大きさが大きくなっている場合があるのです。(少なくともそう考えるしかないようなバグがちらほらありました) そのため、Pickerに関してはframeで大きさを想定通りのものに調整できるようにし、Textは発見するたびにfixedSize(horizontal: false, vertical: true)をつけていく対応が発生しました。 Dynamic Typeを扱ったりもしていたので完全に推測通りの原因とは言い切れませんが、少し検証時に注意が必要なのかもしれません。 まとめ 以上、SwiftUIを実際に実プロダクトに使ったときに見つかった少し変わった注意点をいくつかご紹介してみました。 個人的な肌感としては、サポートはなるべくiOS15以上にできるアプリで、なおかつSwiftUIをメインに、一部UIViewRepresentableで対応するというのが実プロダクトでSwiftUIをストレスなく使う条件になってくると思いました。ただまだまだ足りない機能やonAppearの挙動が不安定などの問題もあるのでうまく検証しながら向き合っていきたいですね。 VIPERアーキテクチャのRouterで使う、Wireframeプロトコルをプロトコルエクステンションで記述したものです。extensionの中でStoryboardからの初期化やさまざまな設定を実装することで、各画面からは関数を呼び出すだけで遷移できるようになり、個別の実装を行う必要がなくなります。 ↩
- 投稿日:2021-12-03T12:27:36+09:00
Interface Builder 上で maskedCorners を設定
背景 iOS11 より、 View の四隅を丸める際に maskedCorners を指定することで、一部だけに cornerRadius を設定することが可能となりました。 コード上での設定するとなると view.layer.cornerRadius = 16 view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXminYCorner] となるのですが、レイアウトを Interface Builder で組んでいる場合、このためだけにコード側で処理するのは面倒なため、 Interface Builder 上で maskedCorners の設定が完結することを目指します。 設定方法 Interface Builder 上の User Defined Runtime Attributes にて、 layer.maskedCorners の Key Path に対して、 15 までの Number を設定することで対応が可能となります。 IB 上の設定 表示 maskedCorners の型である CACornerMask は protocol OptionSet に準拠することによって、複数のフラグ設定を UInt のビットフラグ表現で保持できるような実装となっています。 ビットフラグとしては、 layerMinXMinYCorner (左上) layerMaxXMinYCorner (右上) layerMinXMaxYCorner (左下) layerMaxXMaxYCorner (右下) の4つを取るため、 $0 〜 (2^4 - 1)$ の値によって、 maskedCorners: CACornerMask を表現できます。 早見表 以下に maskedCorners: CACornerMask に数値を設定する場合の対応をまとめました。 値 右下MaxXMaxY 左下MinXMaxY 右上MaxXMinY 左上MinXMinY 表示 0 − − − − 1 − − − 2 − − − 3 − − 4 − − − 5 − − 6 − − 7 − 8 − − − 9 − − 10 − − 11 − 12 − − 13 − 14 − 15
- 投稿日:2021-12-03T08:05:28+09:00
ARをさわる【コンピュータ・ビジョン*AR】
ARオブジェクトにさわれたらいいと思いませんか? 【ARをさわる】 VisionとRealityKitで指が触れたARオブジェクトを反応させられます。 まずはVisionで指先の位置を取得します。 import Vision import RealityKit ... var handPoseRequest = VNDetectHumanHandPoseRequest(completionHandler: handDetectionCompletionHandler) // 手のポイントを取得してくれるリクエスト // CompletionHandler内で結果を処理します ... func session(_ session: ARSession, didUpdate frame: ARFrame) { let pixelBuffer = frame.capturedImage // ARSessionで取得したフレームでVisionリクエストを実行 DispatchQueue.global(qos: .userInitiated).async { [weak self] in let handler = VNImageRequestHandler(cvPixelBuffer:pixelBuffer, orientation: .up, options: [:]) do { try handler.perform([(self?.request)!]) } catch let error { print(error) } } } 取得した指先の位置でヒットテストをし、ARオブジェクトを検出します。 ARViewはView上のポイントの延長線上にあるエンティティを検出できます。 func handDetectionCompletionHandler(request: VNRequest?, error: Error?) { // リクエストの結果から、人差し指の先の位置を取得する guard let observation = request?.results?.first as? VNHumanHandPoseObservation else { return } guard let indexFingerTip = try? observation.recognizedPoints(.all)[.indexTip], indexFingerTip.confidence > 0.3 else {return} // Visionの結果は0~1に正規化されているので、ARViewの座標に変換する let normalizedIndexPoint = VNImagePointForNormalizedPoint(CGPoint(x: indexFingerTip.location.y, y: indexFingerTip.location.x), viewWidth, viewHeight) // 取得した指先の座標でヒットテストを実施 if let entity = arView.entity(at: normalizedIndexPoint) as? ModelEntity { // 見つけたボックス・オブジェクトに物理的な力を加える entity.addForce([0,40,0], relativeTo: nil) // addForceするには、対象のエンティティにPhysicsBodyComponentを与えてください } } ヒットテストで検出するエンティティにはコリジョン(衝突)シェイプが必要です。 let box = ModelEntity(mesh: .generateBox(size: 0.05), materials: [SimpleMaterial(color: .white, isMetallic: true)]) box.generateCollisionShapes(recursive: false) GitHubのサンプルコード:RealityKit-Sampler 手のポイントの取得について:VisionでHand Pose Detection 手のトラッキング: MLBoyだいすけ ARオブジェクトの作り方など、RealityKitの取り扱いについて:RealityKitの参考書
- 投稿日:2021-12-03T04:06:16+09:00
未経験から月収100万円のフリーランスエンジニアになった経緯とコツ
未経験からフリーランスとして稼げるようになったポイントは3つあると思っています。 1、時間をつくる 2、全体像をつかむ 3、発信する です。それぞれ解説していきます。 【著者近影】 僕がニートからエンジニアになった経緯は、 Youtubeチャンネル「エンジニアと人生」でも取材してもらいました。 そちらもあわせて見てもらえると嬉しいです。 1、時間をつくる 雪がたっぷり積もったら、外に出ていろいろ遊んでみようと思いますよね。雪だるまを作ったり、かまくらを建てたり、雪合戦をしたり。 それと同じで、時間的なスペースがあると、いろいろ工夫してみようという気持ちが生まれます。 一個失敗しても、「じゃあこうしてみようかな」みたいな試行錯誤ができる。 子どもが雪の中で転げ回るみたいに。失敗ができる。 無駄って大事なんです。 なんならこたつで丸くなってもいい笑。 僕は以前の仕事の合間にも、プログラミングをかじっていたのですが、形にならないままでした。 本業の隙間時間で一個失敗してしまうと、次がないんですよ。 で、ニートになった。 時間ができたから、すぐプログラミングに邁進するかというと、そんなこともない笑 しばらくの間はネットフリックスを見たり散歩したりしていました。 そういうのもだんだん飽きてきて、「なんかやることないかなあ」とやってみたプログラミングが楽しくなってきた。 それぐらいのペースだと思うんですよ。自発性が生まれるタイミングって。 そうはいっても大体の人はまとまった時間を作るのは難しいと思うから、1日1時間、時間的なスペースを作ってみる、というルールでもいいと思います。 やり始めると、習慣的な勢いが出てきます。 2、全体像をつかむ ものごとがうまくいかないときって、全体像が見えていないときだと思うんですよ。 逆上がりがうまくできない子って、最初のキックはやってみるんだけど、そのキックが以降の動作とどう結びついて、全体の中でどう位置づけられるかイメージができていない。それで、ただただ高くキックしようとするのを繰り返していると思うんです。 つまり、初心者コースから抜け出せない人は、一つのエレメントには取り組めるけど、それが全体のどこに位置づけられるのかわからない。せっかく学んでも結び付けられないと意味を成さない。 何かがうまくできるようになるには、ぼんやりとでもいいから、全体像を感じることができることが大事だと思うんです。 女の子を口説くときと一緒ですね。ゴールまでイメージを。。。いや、このたとえはやめよう。ポリティカリー・インコレクトな気がする。 全体像があって、目の前のエレメントを有機的に結び付けられる。 プログラミングを勉強し始めて、写経してみたものの、それをどう使えばいいかわからない。初学者あるあるだと思うんですよ。 「1+1はこう書くとプリントされる」「でもこっからどうすればアプリが作れるんだ??」みたいな。 僕もそうでした。ネットの講座を受けてみたり、オライリー本を買ったり。 で、そこからどう脱却したかというと、 スタンフォード大学の講座を受けたんですよ。 コンピュータサイエンスiOS講座(CS193)です。 この講座を受けて僕はアプリが作れるようになりました。 ここの授業は、アプリを作るのに必要な知識を網羅していて。「本当に必要なことは全部わかる」という感じです。世界最高峰の大学だけあって、世界最高峰にわかりやすい。アプリを作るの必要なランドマークとその結び付け方が、手を動かしながら実感できる仕組みになっています。 実際に作れるようになりました。 全体像をワンセットで見せてくれてつかませてくれるものを見つけるって大事だと思います。 大工の徒弟制度とかあるじゃないですか。最初はひたすらカンナクズがきれいになるまでカンナがけするとかいうけど、カンナがけ自体は実は重要じゃなくて、それをしながら親方のそばにいて1から10まで親方が仕事をするのを見ているってことが大事なんだと思うんです。 ひとの真似をするって、全体像をつかむうえで重要だと思います。 アプリを作るということも、一度全体像がわかってしまえば、作りたいものがあったときに、どの知識が不足しているのかがわかります。あとは必要な知識をウェブで拾って全体像の中に落とし込んでいけばいいだけです。 講座の最終課題で「自分のアプリを作ってみる」というのがありました。 授業で習ったことにプラスアルファして、何か最新のテクノロジーをくっつけたアプリを作ってみようと思いました。ネットで面白そうなフレームワークを探したりして、機械学習を使ったアプリを作ってみようとおもいました。 けっきょく、機械学習だろうがiOSだろうがなんだろうが、ネットを検索したら、ちゃんとドキュメントなり文章なりコードなりで情報が載っていて、それをちゃんと読めば(全体像さえあれば)理解できるようになっています。全体像と情報のありかがわかっていれば、できることが格段に増えます。 CS193に話を戻すと、これはYoutubeで無料で全授業が見られます。 英語は、Youtubeの英語字幕でなんとかなりました。あと、アメリカドラマ「ブレイキング・バッド」を一通り英語で見たのも役に立ったかな笑 英語がわかると、ドキュメントなどが原語で読めますし何かと便利です。 まあGoogle翻訳もかなり優秀なので、英語は大した問題ではなくなっている。 3、発信する 箱の中の猫と同じで、見られるまでは他の人にとって自分は存在しないのと同じなんです。 実力があるだけでは仕事はこない。 箱をぶちやぶって顔を出して、にゃーと鳴かないといけない。 自分はここにいるよ!と叫ぶ効率的な方法が「ネット上に情報を発信する」ということだと思います。 技術記事などを書いて、自分の実力という情報をネット上に公開していく。その記事がクライアントの目に届けば、仕事がくる。 お金を出してくれる人と自分を繋げないといけないんですよね。 そうすることで、プログラミングが一気にお金につながりました。 僕はQiitaで技術記事を発信して、Mediumという英語のサービスでも同じ記事を書いています。GitHubにコードもアップしています。 それを見た国内外の企業から開発の依頼が来て、こなしていくうちに3ヶ月で月収100万円を超えました。 それまで1円も稼げない時期が半年ぐらいありました。 個人開発アプリをAppStoreにアップロードして、広告収入で当てようと、10個ぐらいリリースしました。 顔の似ている度を計算するアプリとか、ARでバーチャル背景をつけるアプリとか、機械学習で写真の加工をはがすアプリとか、写真をアニメ風にするアプリとか。 箱の中でキメラ的な化け猫として特異進化を遂げていたんです。怖いですよね。 でも、肝心のお金を出してくれる人の元には、僕の情報は届かなかった。 僕の場合はお金を出してくれるのはコンシューマーではなかったということでしょうか。 お金を払える人にきちんとリーチするマーケティングが大切だと思います。 発信を始めたのは「エンジニアと人生」というコミュニティに入ったのが大きかったです。 有名iOSエンジニアの方が始められたコミュニティです。 このコミュニティの中で「発信講座」があります。 この講座の先生がすごくたくさん技術記事や技術本を書いておられて、それらが仕事につながっていく全体感をわかりやすく教えてくれました。さっきの大工の徒弟制度の話じゃないけど、仕事につなげるまでの全体感を見せてもらえたのが大きいと思います。 個人相談にのってくださるイベントも開催されていて、某社の採用担当の方とお話しする機会を設けてもらったり、フリーランスになることの背中を押してもらったり、技術記事の添削をしてくださったりして、それがすごく財産になりました。依頼主との交渉の仕方も教えてもらいました。 ちょうど僕も暇だったので、コミュニティの方の名言集をまとめたり、Youtubeチャンネルの編集を手伝ってみたりしていて、そういうのもあって可愛がってもらったと思います。これも時間があったからできたことですね。 1、たっぷりと時間つくる 2、全体像をつかむ 3、発信する これらは、「プログラミングで稼ぐ」以外にも使えそうな方法な気がします。 この方法で女の子にモテた人は詳細を教えてください。 ? この記事は、#エンジニアと人生 #2 Advent Calendar 2021の3日目の記事です。 フリーランスエンジニアです。 お仕事のご相談はこちらまでお気軽に? rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 GitHub Twitter Medium