- 投稿日:2022-01-19T23:57:57+09:00
SwiftUI で 親 View に ScrollView、子 View に GeometryReader の構造にしたら、高さのレイアウトがおかしくなった
はじめに SwiftUI で GeometryReader を ScrollView の入れ子にする構造にしたところ、高さのレイアウトがおかしくなったので、解決する方法を検討しました。 環境 macOS 12.1 Xcode 13.2.1 作りたかったもの 正方形のセルが 4 × 5 の Grid 状に並んでいて、縦スクロールできる画面です。 親 View に ScrollView、子 View に GeometryReader の構造にした例 タイトルに書いたレイアウトが崩れてしまったパターンです。 この構造にすると GeometryReader の高さがうまく計算されずに 10px となるため、ChildView が重なってしまいました。 import SwiftUI struct ContentView: View { let rows = 12 var body: some View { ScrollView { ForEach((0..<rows)) { _ in ChildView() } } } } struct ChildView: View { let columns = 4 var body: some View { GeometryReader { geometry in let cellWidth = geometry.size.width / CGFloat(columns) LazyVGrid( columns: Array<GridItem>( repeating: .init(.flexible(minimum: 1, maximum: cellWidth)), count: columns)) { ForEach((0..<20)) { index in Text("\(index)") .frame(width: cellWidth, height: cellWidth) } } } } } 試したパターン 親 View で GeometryReader の中に ScrollViewを入れ子にして子 View に width を渡し、セルサイズを計算させる ScrollView の中に GeometryReader をいれるとレイアウトが崩れるので、親 View で GeometryReader を使って width を取得し、子 View に渡してみました。 うまく計算されていますが、padding とか inset の計算を考えるとちょっと面倒くさいですね。 import SwiftUI struct ContentView: View { let rows = 12 var body: some View { GeometryReader { geometry in ScrollView { ForEach((0..<rows)) { _ in ChildView( containerWidth: geometry.size.width ) } } } } } struct ChildView: View { let columns = 4 let containerWidth: CGFloat var body: some View { let cellWidth = containerWidth / CGFloat(columns) LazyVGrid( columns: Array<GridItem>( repeating: .init(.flexible(minimum: 1, maximum: cellWidth)), count: columns)) { ForEach((0..<20)) { index in Text("\(index)") .frame(width: cellWidth, height: cellWidth) } } } } 子 View で UIScreen から width を取得してセルサイズを計算させる GeometryReader を使わずに UIScreen を使う方法です。 こちらも padding や inset があると計算が面倒ですが、親 View からサイズを渡されることもなく子 View で完結できるので、多少マシかも。 struct ContentView: View { let rows = 12 var body: some View { ScrollView { ForEach((0..<rows)) { _ in ChildView() } } } } struct ChildView: View { let columns = 4 var body: some View { let cellWidth = UIScreen.main.bounds.width / CGFloat(columns) LazyVGrid( columns: Array<GridItem>( repeating: .init(.flexible(minimum: 1, maximum: cellWidth)), count: columns)) { ForEach((0..<20)) { index in Text("\(index)") .frame(width: cellWidth, height: cellWidth) } } } } GeometryReader を使わず、Spacer と aspectRatio を活用してレイアウトする Twitter で d_date さんに、GeometryReader を使わず、Spacer と aspectRatio を活用してレイアウトする方法を教えていただきました。 という感じなので、StackViewを組む要領でこんなふうに書いたりしますね pic.twitter.com/bCrVEgW25t— Date (@d_date) January 19, 2022 こちらのコードを手元で動かしたときの画像です。 import SwiftUI struct ContentView: View { let rows = 12 var body: some View { ScrollView { ForEach((0..<rows)) { _ in ChildView() } } } } struct ChildView: View { let columns = 4 var body: some View { LazyVGrid( columns: Array<GridItem>( repeating: .init(.flexible(minimum: 60, maximum: .infinity)), count: columns)) { ForEach((0..<20)) { index in VStack { Spacer() HStack(alignment: .center) { Spacer() Text("\(index)") Spacer() } Spacer() } .aspectRatio(1, contentMode: .fit) } } } } 記事公開後に追記した方法 GeometryReader を使わず、maxWidth と maxHeight を .inifinity にし、aspectRatio を指定してレイアウトする 記事を読んだ会社の同僚から GeometryReader を使わず、Spacer と aspectRatio を活用してレイアウトする をベースにした改良版を教えていただいたので、追記しました。 ChildView の ForEach の内容がシンプルになりました。 Text の frame を .frame(maxWidth: .infinity, maxHeight: .infinity) とすることで、いい感じにサイズを調整しています。 import SwiftUI struct ContentView: View { let rows = 12 var body: some View { ScrollView { ForEach((0..<rows)) { _ in ChildView() } } } } struct ChildView: View { let columns = 4 var body: some View { LazyVGrid( columns: Array<GridItem>( repeating: .init(.flexible(minimum: 60, maximum: .infinity)), count: columns)) { ForEach((0..<20)) { index in Text("\(index)") .frame(maxWidth: .infinity, maxHeight: .infinity) .aspectRatio(1, contentMode: .fit) } } } } 結論 GeometryReader を使わず、Spacer と aspectRatio を活用してレイアウトする か、GeometryReader を使わず、maxWidth と maxHeight を .inifinity にし、aspectRatio を指定してレイアウトする の方法が変にハマることもなさそうですね。 最初私も Spacer でレイアウトを調整していたんですが、なんか計算でうまいことできんの?って思って GeometryReader を使った結果、どはまりしました。 GeometryReader を正しく使えたら UI 実装の幅も広がりそうではありますが、現状どうにもとっつきにくい印象なので、極力使わない方向でいこうと思います。 これぞ!という使い道を知っている方は教えていただけるとうれしいです。
- 投稿日:2022-01-19T22:49:01+09:00
【Swift】CoreBluetoothの使い方 その2:セントラルの実装例
はじめに Corebluetoothのセントラル側の実装例について記載していきます。 ①その1:BLEにおける役割 ②その2:セントラルの実装 ← 今回はここ 処理の順番 ・必要な機能の初期化 ・スキャン処理 ・接続処理 ・ペリフェラルの情報を取得して通信を行う。 ・切断 上記順番に処理を行いますので、順番に記載していきます。 必要な機能の初期化 ・BLE関連の設定の追記 info.plistに「Privacy - Bluetooth Always Usage Description」を追加 試すだけであれば上記だけで問題ありませんが、実際にアプリに組み込み、Appstoreconnectに上げる場合はペリフェラル側の「Privacy - Bluetooth Peripheral Usage Description」も追記が必要です。一見アップロードがうまく言ったように振る舞いますが、メールで怒られます。 ・フレームワークのimport import CoreBluetooth ・必要な変数の宣言 //セントラルの振る舞いは全てcentralManagerで操作します private var centralManager:CBCentralManager! //接続したペリフェラルを保持するために使います private var cbPeripheral:CBPeripheral? = nil //以下は今回のサンプルで便宜上使うものです。信号の送信・読み出しに使います。 //必要に応じて追加・変更が必要です。 private var writeCharacteristic: CBCharacteristic? = nil private var readCharacteristic: CBCharacteristic? = nil ・centralManagerの初期化 //①セントラルマネージャーの初期化 private func bleInit() { //セントラルマネージャーを初期化:初期化した時点でPermissionの許諾のpopupが出て、Bluetoothの電源がONになる。 centralManager = CBCentralManager(delegate: self, queue: nil) } 初期化を実施するとBluetooth機能がONになり、 CBCentralManagerDelegateの下記Delegateメソッドが呼ばれます。 ・centralManagerDidUpdateState //BLEの状態が変化する毎に呼ばれるメソッド func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { //②:セントラル側BLEの電源ONを待つ //BLEが使用可能な状態:電源がONになっている case CBManagerState.poweredOn: print("Bluetooth PowerON") break //BLEが使用出来ない状態:電源がONになっていない case CBManagerState.poweredOff: print("Bluetooth PoweredOff") break //BLEが使用出来ない状態:リセット中 case CBManagerState.resetting: print("Bluetooth resetting") break //BLEが使用出来ない状態:Permissionの許諾が得られていない case CBManagerState.unauthorized: print("Bluetooth unauthorized") break //BLEが使用出来ない状態:不明な場外 case CBManagerState.unknown: print("Bluetooth unknown") break //BLEが使用出来ない状態:BLEをサポートしていない case CBManagerState.unsupported: print("Bluetooth unsupported") break } } 「CBManagerState.poweredOn」を待ってください。 これで、BLEの機能が使用可能になります。 スキャン処理 ・スキャンの実行 //③Peripheralのスキャン private func scan(){ //Bluetoothの状態がONになっていることを確認 if centralManager.state == .poweredOn{ //Service指定せず周囲の全てのPeripheralをスキャンする centralManager.scanForPeripherals(withServices: nil, options: nil) //Serviceを指定してスキャンをする //デリゲートメソッドが指定のサービス以外で呼ばれなく、効率的であるため、こちらがベストプラクティス //let services: [CBUUID] = [CBUUID(string: "サービスのUUID")] //centralManager.scanForPeripherals(withServices: services, options: nil) //タイマーなど設けずスキャンをしているが、この場合は所望のペリフェラルが見つかるまでスキャンし続けてしまうので、 //実際に使う場合はタイマーでスキャンを停止する、スキャン停止ボタンを設けるなどの配慮が必要 // scanTimer = Timer.scheduledTimer(timeInterval: TimeInterval(10), // target: self, // selector: #selector(self.timeOutScanning), // userInfo: nil, // repeats: false) // ///スキャンタイムアウト // @objc func timeOutScanning() { // centralManager.stopScan() // } } } ●注意点 ・コード内にも記載していますが、serviceを指定せずにスキャンを行うと周囲の全てのBLE機器をスキャンしてしまい、そのたびに以降記載するDelegateメソッド(didDiscover peripheral)が呼ばれてしまいます。 なので、serviceを指定してスキャンを実施するのがベストプラクティスです。 ・スキャン処理はcentralManagerのstopScan()を実行するまでずっと継続されます。そのため、タイマーなどで止められる手段を設けておく方がよいです。 接続処理 ・アドバタイズ信号の受信 ※実際はアドバタイズを拾ってスキャンリクエストを実施してスキャンレスポンスが返ってきたら呼ばれます。ここはOSが勝手に行います スキャン処理を行うと、BLE機器が見つかる毎にCBCentralManagerDelegateの下記Delegateメソッドが呼ばれます。 ※service指定した場合は指定したserviceをアドバタイズしている機器が見つかった時だけ呼ばれます。 //④スキャンでPeripheralが見つかる毎に呼ばれるメソッド func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { //peripheralのローカルネーム print("name:\(peripheral.name)") //advertiseの中身 print("advertisementData:\(advertisementData)") //advertiseに入っているServiceUUID print("advertisementServiceUUID:\(advertisementData["kCBAdvDataServiceUUIDs"])") //advertiseの電波強度(RSSI) print("rssi:\(RSSI.stringValue)") //名称フィルターして接続する場合 if peripheral.name == "接続したいperipheralの名称"{ //見つけたペリフェラルを保持 self.cbPeripheral = peripheral central.connect(peripheral, options: nil) //スキャン停止 centralManager.stopScan() } //アドバタイズに入っているService UUIDでフィルターして接続する場合 //※実シーンではscanの時点でadvertisementDataを指定することでフィルターをかけるので使用シーンはほとんど無いと思われる // let SERVICE_UUID:CBUUID = CBUUID(string: "接続したい機器がアドバタイズに乗っけているServiceUUID") // if advertisementData["kCBAdvDataServiceUUIDs"] != nil { // let UUID:[CBUUID] = advertisementData["kCBAdvDataServiceUUIDs"] as! [CBUUID] // //アドバタイズに入っているUUIDは一つだけのため // if UUID.first == SERVICE_UUID{ // central.connect(peripheral, options: nil) // } // //スキャン停止 // centralManager.stopScan() // } } 上記Delegateメソッドの中で接続したい機器を見つけて接続を行います。 ※機器一覧を取得して画面に表示させ、後でユーザーの操作で接続を行いたければ、上記メソッドの中で取得した「peripheral」を配列に保存してtableViewなどで表示すればよいです。 BLE的にはこの時点で接続されています。 ・接続完了後以下Delegateメソッドが呼ばれます。 //⑤接続が成功したときに呼ばれるデリゲートメソッド func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { //ペリフェラルのデリゲートをセット peripheral.delegate = self // 指定のサービスを探す:機器は複数のサービスを持っているので使いたいサービスのみ探すべきである // let services: [CBUUID] = [CBUUID(string: "サービスのUUID")] // peripheral.discoverServices(services) //サービスを指定せずにペリフェラルの全てのサービスを探す peripheral.discoverServices(nil) //サービスが見つかるとCBPeripheralのデリゲートメソッドが呼ばれる。 } //⑤'接続が失敗したときに呼ばれるデリゲートメソッド func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { print("接続失敗") } ペリフェラルの情報を取得して通信を行う。 接続完了時のコードに記載している以下のコードでまずServiceを探します。 //サービスを指定せずにペリフェラルの全てのサービスを探す peripheral.discoverServices(nil) また、接続が完了した時点でperipheralのdelegateをセットしています。 peripheral.delegate = self 以降はCBPeripheralDelegateのDelegateメソッドが呼ばれます。 ・サービスが見つかった時に呼ばれdelegateメソッド //⑥ ⑤で探したサービスが見つかった時に呼ばれるデリゲートメソッド func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { //キャラクタリスティック検索開始 指定して検索。以下は一つしか指定していないが配列なので複数指定も可能 //機器は複数の機能を持っていることがほとんどなので欲しいキャラクタリスティックのみを検索するのが効率的 //let charcteristicUUID:CBUUID = CBUUID(string: "検索したいUUID") //peripheral.discoverCharacteristics([charcteristicUUID], // for: (peripheral.services?.first)!) //全てのサービスのキャラクタリスティックの検索 for service in peripheral.services! { peripheral.discoverCharacteristics(nil, for: service) } } この中でCharacteristicの検索を行います。 peripheral.discoverCharacteristics(nil, for: service) ・キャラクタリスティックが見つかった時に呼ばれるdelegateメソッド //⑦ ⑥で探したキャラクタリスティックが見つかった時に呼ばれるデリゲートメソッド func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { for characteristic in service.characteristics!{ if characteristic.uuid.uuidString == "属性がNotify or indicateのキャラクタリスティックのUUID" { //Notificationを受け取るハンドラ peripheral.setNotifyValue(true, for: characteristic) } if characteristic.uuid.uuidString == "属性がWriteのキャラクタリスティックのUUID" { writeCharacteristic = characteristic } if characteristic.uuid.uuidString == "属性がreadのキャラクタリスティックのUUID"{ readCharacteristic = characteristic } //なおcharacteristicの属性は以下で取得可能 //characteristic.propertie //「.indicate .notify .read .write .writeWithoutResponse」で属性の判別が可能 print("発見したキャラクタリスティック",characteristic.uuid.uuidString) } } 本メソッド内で見つけたキャラクタリスティックは信号の送信・読み出しで使うので保持しておきます。 Notifyとindicateのキャラクタリスティックに関してはsetNotifyValueを行うことで、ペリフェラルのディスクリプタに書き込みを行い、以降、ペリフェラルからの信号送信を受けられるように設定しておきます。 ここまでで接続後の準備は完了です。 以降は信号の送受信になります。 ・信号の送信 信号を送信したければ、保持したキャラクタリスティックに対して、centralManagerを通して信号の送信を行います。 //データの送信 private func sendData(_ data:Data){ //データの書き込み:属性がwrite with responseの場合 if let peripheral = self.cbPeripheral,let writeCharacteristic = self.writeCharacteristic{ peripheral.writeValue(data, for: writeCharacteristic, type: CBCharacteristicWriteType.withResponse) } //データの書き込み:属性がwrite without responseの場合 // if let peripheral = self.cbPeripheral,let writeCharacteristic = self.writeCharacteristic{ // peripheral.writeValue(data, for: writeCharacteristic, type: CBCharacteristicWriteType.withoutResponse) // } } 送信が完了すると以下のCBPeripheralDelegateのDelegateメソッドが呼ばれます。 //write実行時に呼ばれる func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("書き込みエラー:",error.localizedDescription) return }else{ print("書き込み成功:",characteristic.uuid) } } ・信号の受信 以下のように読み出しを行う //データの読み出し private func readData(){ if let peripheral = self.cbPeripheral,let readCharacteristic = self.readCharacteristic{ peripheral.readValue(for: readCharacteristic) } } 上記で読み出しを行う、もしくはペリフェラルからnotify or indicateで信号が送信された場合、以下のメソッドが呼ばれます。 //Notify or indicate or Read時に呼ばれる func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { print("送信元のCharacteristic:",characteristic.uuid.uuidString) if let error = error { print("情報受信失敗...error:",error.localizedDescription) } else { print("受信成功") let receivedData = String(bytes: characteristic.value!, encoding: String.Encoding.ascii) print("受信データ",receivedData) } } 切断を行う cancelPeripheralConnectionで切断を行う。 //切断処理 private func disconnectPeripheral(){ if let peripheral = cbPeripheral { centralManager.cancelPeripheralConnection(peripheral) } } さいごに 以上で一連のBLEのセントラル側の実装は終了です。 対抗機が無いとなかなか実感しにくいので、次はペリフェラル側の実装を行い、実際に通信させます。 qiita初心者ですがqiitaの投稿って結構大変ですね。。。 至らない点、間違っている点などあれば、是非ご指摘頂ければ幸いです。
- 投稿日:2022-01-19T22:43:50+09:00
【Swift】CoreBluetoothの使い方 その1:BLEにおける役割
はじめに 2022年はOutPutしていくぞ!!ということで、 初めてQiitaに投稿しますが、iOSでのBLEの使い方、CoreBluetooth周りの投稿をしていきます。 ①その1:BLEにおける役割 ← 今回はここ ②その2:セントラルの実装例 CoreBluetoothとは? Core Bluetoothフレームワークは、アプリがBluetooth搭載の低エネルギー(LE)および基本レート/拡張データレート(BR/EDR)ワイヤレステクノロジーと通信するために必要なクラスを提供します。 Apple Developerより https://developer.apple.com/documentation/corebluetooth iOSでBLE(Bluetooth Low Energy)を使う時に使用するフレームワークです。 iOSでBLEを使う時は基本的にCoreBluetoothを使うことになります。 BLEにおける役割とは? CoreBluetoothの前に、 BLEを使う上で知っておくべき前提条件として、2つの役割があります。 Central(セントラル) マスターとも呼ばれ、機能を使う側に当たります。クライアントサイドとなります。スマホアプリなどではスマホがセントラルになることが多いです。機器の機能を使う側ですので。 Peripheral(ペリフェラル) スレーブとも呼ばれ、機能を提供する側に当たります。サーバーサイドとなります。 BLE機器は一般的にはこちらになります。イヤホンや温湿度などの環境センサーなど、機能を提供する機器側ですね。スマホをペリフェラルにすることも当然可能です。 BLE通信の一連の流れ ペリフェラルが電波を発信する(アドバタイズ)。 セントラルは電波をスキャンしアドバタイズ信号を受信する。 アドバタイズ信号を受け取ったセントラルはペリフェラルにどのような機能があるのか更に知りたいのでスキャンリクエストを投げる。※アドバタイズに乗せられる情報は限られているので。 ペリフェラルはスキャンリクエストに応じてスキャンレスポンスを投げる セントラルはスキャンレスポンスの中身を確認し、所望の機器であるならば、セントラルはペリフェラルに接続要求を投げ、接続を行う。 接続後、セントラルはペリフェラルの機能の走査を行い機能の全てを把握し、ペリフェラルを使用する。 使用が終わったら切断を行う。 ペアリングなどのセキュリティ関連は一旦置いておき、一連の流れは上記となります。 次は? 次は上記一連の流れにおけるセントラル側の実装に関して記述していきます。 ②その2:セントラルの実装例
- 投稿日:2022-01-19T19:44:59+09:00
iOS & iPadOS 15 リリースノート日本語訳
iOS & iPadOS 15 リリースノート 新機能を利用し、APIの変更をテストするためにアプリをアップデートしてください。 概要 iOS & iPadOS 15 SDKは、iOS & iPadOS 15を実行するiPhone、iPad、及びiPod touchデバイス用のアプリを開発するためのサポートを提供します。 SDKはMac AppStoreから入手できるXcode 13にバンドルされています。 Xcode 13の互換性要件についてはXcode 13リリースノートを参照して下さい。 App Store 新機能 StoreKit 2はSwiftの同時実行性などの新しい言語機能を利用する最新のSwiftベースのAPIを導入しています。このAPIを使用して、製品情報の読み込み、ストアでのアプリ内購入の表示、顧客による購入の許可、コンテンツとサブスクリプションへのアクセスの管理、およびAppStoreによって署名されたトランザクション情報をJSONWeb署名(JWS)形式で受信します。(66587964) Productのrequest(with:)メソッドはproducts(for:)メソッドへと名称変更されました。 Product.SubscriptionInfo.StatusとStorefrontのTransactionリスナータイプのプロパティが、それぞれupdatesとupdatesになりました。 ネストされたAsyncSequence準拠構造は、Transaction.Transactions、Product.SubscriptionInfo.Status.Statuses、およびStorefront.Storefrontsになりました。 TransactionSequenceとTransactionListenerの両方がTransaction.Transactionsになりました。 (79034347) StoreKitError.userDidNotAuthenticateは使用できなくなりました; 代わりにStoreKitError.userCancelledを使用してください。(78270199) カスタムデコード用の生のJSONデータにアクセスできるようになりました。 複数のProduct.PurchaseOptionメソッドが許可されるようになりました。.custom(_ :)は、custom(key:stringValue:)、custom(key:numberValue:)、custom(key:boolValue:)、及びcustom(key:dataValue:)といういくつかの新しいタイプのメソッドに置き換えられました。 Product、Transaction、及びrenewalInfoの添字演算子は、ネストされたキー列挙とともに削除されました。 BackingValue及びBackingValueから初期化子を追加する拡張機能は削除されました。(79101606) 新しいタイプのプロパティunfinishedがTransactionで利用できるようになりました。これは、アプリが引き続きコンテンツをユーザーに配信する必要があるトランザクションの署名付き情報を返します。 (79620896) 新しいonStorefrontChange(shouldContinuePurchase:)がStoreKit 2で利用可能になりました。この購入オプションを使用して、トランザクション中にAppStoreストアフロントが変更された場合にトランザクションを続行するかどうかを決定できます。 このオプションが追加されていない場合、デフォルトはtrueです。 (70757789) VerificationResult.unverified(SignedType)はVerificationResult.unverified(SignedType、VerificationError)になり、未検証の符号付き値の理由を提供します。 jsonRepresentationはTransactionで利用でき、jsonRepresentationはrenewalInfoで利用できます。 どちらのプロパティも、ペイロードJSONをデータとして提供します。 符号付きの値にアクセスするのに便利なように、payloadValueプロパティとunsafePayloadValueプロパティをVerificationResultで使用できます。 (80701792) 解決された問題 サンドボックス環境で実行された購入がVerificationResult.unverified(_ :_ :)を返す問題を修正しました。 (71949674) 既知の問題 unfinishedプロパティが、すでに終了したトランザクションに対してVerificationResult<Transaction>を返す場合があります。 (81346114) Audio Units 新機能 Audio Unitsは、Audio UnitホストがiOSで表示できるカスタムビューを提供するようになりました。 AUAudioUnitにユーザーインターフェイスがあるかどうかを判断するには、providesUserInterfaceプロパティを使用します。 requestViewController(completionHandler:)メソッドを使用して、viewのAUViewControllerを取得してください。 カスタムビューは、tintColorプロパティを介したビューの色合いの設定をサポートしています。 これを使用して、ビューの色をトラックごとに異なる色に設定したり、アプリの外観に一致させたりすることができます。 (74183251) AVFoundation 新機能 iPadOSアプリは、複数のウィンドウを表示し、画面上の唯一のアプリケーションである間も、引き続きカメラを使用できるようになりました。 非推奨 supportPhotoPixelFormatTypes(for:)およびsupportedRawPhotoPixelFormatTypes(for:)は、Swiftで[NSNumber]ではなく[OSType]を返すようになりました。 (64822071) RecommendedVideoSettings(forVideoCodecType:assetWriterOutputFileType:)は、Objective-Cではnullable NSDictionary *の代わりにnullable NSDictionaryを、Swiftでは[AnyHashable:Any]?の代わりに[String: Any]?を返すようになりました。(33784279) cgImageRepresentation()およびpreviewCGImageRepresentation()はUnmanaged<CGImage>?の代わりにCGImage?を返すようになりました。(44734827) recommendedAudioSettingsForAssetWriter(writingTo:)は、Objective-Cではnullable NSDictionary *の代わりにnullable NSDictionaryを、Swiftでは [Any Hashable: Any]?の代わりに[String: Any]?が返すようになりました。(50450334) Core Haptics 新機能 CHHapticEventTypeAudioContinuous、CHHapticEventTypeHapticContinuous、およびCHHapticEventTypeAudioCustomのtypeのイベントは、一時停止したCHHapticAdvancedPatternPlayerが再開した場合、イベントの途中で再生を再開するようになりました。 seek(toOffset:)が特定の時間オフセットでプレーヤーを開始した場合、これらのイベントはイベントの途中で開始されません。 (29274583) タイプリソースにボリュームエンベロープを適用するかどうかを制御できるようになりました。デフォルトでは、これらのリソースは、クリックを回避するために、最初に信号をランプインし最後にランプアウトするビルトインボリュームエンベロープで再生されます。 (75491090)ボリュームエンベロープは、次のいずれかの方法で適用できます。 カスタムオーディオアセットのオーディオリソースIDを登録してインポートする場合は、システムがregisterAudioResource(_:options :)に渡す新しいキー値引数CHHapticAudioResourceKeyUseVolumeEnvelopeを介してこの動作を指定できます。 AHAPファイルまたはCHHapticPatternのinitWithDictionary:error:を使用してオーディオアセットを参照している場合は、CHHapticPatternKeyEventWaveformUseVolumeEnvelopeパターンキーを使用してこの動作を制御できます。 Core ML 既知の問題 automatic reference counting(ARC)モードでは、.dataPointerプロパティを使用すると、コンパイラがMLMultiArrayのライフタイムを予想よりも長くする場合があります。 これにより、メモリ使用量が増える可能性があります。 (80895213)回避策:.dataPointerアクセスを@autoreleasepool {...}ブロックで囲みます。 Create ML 新機能 Create MLフレームワークがiOS & iPadOS15で利用できるようになり、デバイス上のMLを活用するダイナミックなアプリ体験を構築するための新しい機会が得られるようになりました。 画像分類、音声分類、テキスト分類、手の形と手の動きの分類のためのタスクフォーカス型のAPIが、古典的な表形式の分類と回帰のためのAPIとともに利用できます。(37087332) Audio Feature Print-based MLSoundClassifierアルゴリズムは、サウンド分類モデルをより高速に、より精密に、より低いレイテンシーで、より小さなモデルサイズでトレーニングします。 このアルゴリズムは、Create MLのMLSoundClassifierのデフォルトオプションになりました。(70106630) Debugging 既知の問題 macOS 11を実行しているAppleシリコンを搭載したMacのデバイスシミュレータで実行されているiOSアプリでディスパッチセマフォを使用すると、アプリがクラッシュします。(81783378)回避策:Xcodeで、[製品]> [スキーム]> [スキームの編集]を選択し、[実行]> [オプション]> [キューのデバッグ]> [バックトレース記録を有効にする]の選択を解除します。 Find My 既知の問題 iOSデバイスを充電する必要がある場合、Find Myネットワークがアクティブであることを示すテキストは、デバイスの言語が英語に設定されている場合にのみ表示されます。(78547946) Guided Access 既知の問題 VoiceOverでガイドアクセスを使用する場合、ガイドアクセスを終了するためのガイドアクセスパスコードを入力できない場合があります。(79370792)回避策:デバイスのパスコードが設定されている場合は、デバイスを強制的に再起動してガイド付きアクセスを終了します。 Home 既知の問題 Threadを使用するMatterアクセサリとペアリングすることはできません。(80991829) アクセサリがすでに別のアプリとペアリングされている場合、アプリのペアリングフローを通じてサードパーティのアプリをMatterアクセサリとペアリングすることはできません。(80059432)回避策:他のアプリからアクセサリのペアリングを削除してから、サードパーティのアプリをペアリングします。 Apple Homeを作成していない場合、Matterアクセサリを使用してサードパーティアプリにフローを追加することはできません。(80058744)回避策:フローを追加する前に、Homeアプリを起動してHomeを作成します。 Apple TVがWi-Fi経由で接続されている間は、Matterアクセサリにアクセスできません。(79582629)回避策:イーサネット経由でAppleTVを接続します。 Matterアクセサリは、ペアリング後に無応答状態になる場合があります。(76019163)回避策:アクセサリをHomeから削除し、アクセサリをリセットして、Homeに追加し直します。問題が解決しない場合は、HomeハブをHomeから削除して再度追加します。それでも問題が解決しない場合は、Homeを削除して新しいHomeを作成します。 Matterアクセサリとの最初のペアリングの試行には、予想外に長い時間がかかり、最終的に失敗する可能性があります。(77967587)回避策:アクセサリのペアリングを再試行してください。 1つのHomeでペアリングできるMatterアクセサリは最大5つまでです。(77967671) Matterアクセサリをペアリングできるのは、招待されたユーザーではなく、Homeの所有者だけです。(76012945) Home Screen 既知の問題 ウィジェットギャラリーでの検索をキャンセルした後、キャンセルボタンは表示されたままになり、ウィジェットギャラリーが空白になる場合があります。(78572049)回避策:ウィジェットギャラリーを閉じて再度開きます。 iCoud 新機能 iCloud Private Relayは、追加のフィードバックを収集し、Webサイトの互換性を向上させるために、パブリックベータとしてリリースされます。(82150385) 既知の問題 Legacy ContactsはiOSおよびiPadOS15 beta 5から削除され、将来のリリースで復活する予定です。 (81292885) 「+」や「-」などの区切り文字を含むカスタムメールドメインアドレスは設定できません。 (82425376) 別のiTunesアカウントに関連付けられているカスタムメールドメインアドレスは設定できません。 (82358431) 一部のアカウントは、カスタムメールドメインの対象とならない場合があります。 (82421769) Foundation 新機能 Foundationには、自動文法一致エンジンが含まれるようになりました。 これにより、ローカライズされた文字列が自動的に活用されて、複数形、文法的な性別の一致、およびユーザーの住所の用語との一致が考慮されるため、コードが簡素化され、提供するローカライズされた文字列の数が減ります。 英語とスペイン語で利用できます。 (70210115) フォーマットAPIが利用可能になりました。これは、フォーマットに重点を置き、フォーマッターインスタンスを作成、構成、およびキャッシュする必要をなくします。 各Formatterタイプには、フォーマットされた関数があります。 これらの関数には、スタイルの設定とカスタマイズを可能にする引数があります。 (70220307) JSONSerializationとJSONDecoderは、JSON5からのデコードをサポートするようになりました。 (73954652) SortDescriptor、KeyPathComparator、およびSortComparator APIは、値を並べ替えるためのアーカイブ可能なルールを表現するためのSwiftインターフェイスを提供します。 (74264359) Logging 新機能 Swiftのos_signpost(_:dso:log:name:signpostID:)は、すべてのプラットフォームのフレームワークOSの一部です: サブシステムとカテゴリ、既存のOSLogオブジェクト、または既存のLoggerオブジェクトを使用してOSSignposterをインスタンス化します。 OSSignposter APIは、Signpostsを出力するためのメソッドを提供します。 beginInterval(_ )は開始Signpostsを出力し、endInterval(_ :_ :)は終了Signpostsを出力し、emitEvent(_)はイベントSignpostsを出力します。 これらは、Stringとvarargsに基づく既存のos_signpost呼び出しを置き換えます。 APIは、メタデータパラメーターの文字列補間をサポートします。 文字列補間は、Logger APIsで受け入れられるものと同じです。 OSSignposter APIは、すべてのフォーマットおよびプライバシーオプション(以前はos_signpost関数によって提供されていた)をサポートし、Logger APIsと同じ構文に従います。 APIは、従来のAPIよりもパフォーマンスが向上しています。 OSSignposterタイプは、コードのブロックを開始および終了のSignposts、withIntervalSignpost(_ _:around:)で囲むための新しいスコープAPIを提供します。 注: これらのAPIは、iOS14およびiPadOS14以前では使用できません。 ただし、既存のos_signpostAPIは引き続き使用できます。 (54756831) Maps 既知の問題 丸みを帯びた建物の角が消える場合があります。 (80468151) 非推奨 MKPinAnnotationViewとMapPinは、このベータ版では非推奨としてマークされています。 (78536295) Networking 新機能 URLSessionが送信するデフォルトのAccept-Languageヘッダーの形式が更新され、複数のロケールの値が修正されています。 優先言語に加えて、ヘッダーには、優先言語と異なる場合のフォールバックとして現在のシステム言語も含まれます。 この動作は、macOS 12、iOS 15、tvOS 15、およびwatchOS 8SDKに対してリンクするアプリに影響します。 (38772422) URLSessionに非同期関数が含まれるようになりました。 (68890254) For example, a one-shot fetch: let (data, response) = try await URLSession.shared.data(from: URL(string: "https://www.apple.com")!) if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { // Use data. } And support for an AsyncSequence stream of bytes: let (bytes, response) = try await URLSession.shared.bytes(with: URL(string: "https://www.apple.com")!) if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { for try await line in bytes.lines() { // Parse line. } } 非推奨 プロキシ自動設定(PAC)のクリアテキストHTTP URLスキームのサポートは非推奨になりました。 PACにはHTTPS URLスキームのみを使用してください。 これは、設定、システム環境設定、プロファイル、およびconnectionProxyDictionaryやCFNetworkExecuteProxyAutoConfigurationURL(_ :_ :_ :_ :)のようなURLSession APIなどを介して設定されたすべてのPAC構成に影響します。 クリアテキストのHTTP PAC URLを構成すると、PACファイルのロード中にシステムがHTTPSにアップグレードする場合があります。 DNSを介したWebProxy Auto-Discovery(WPAD)プロトコルは影響を受けません。 Dynamic Host Configuration Protocol(DHCP)Option 252 WPADは、PACファイルのロード中にクリアテキストHTTP URLをHTTPSにアップグレードしようとする場合があります。 (61981845) Privacy 新機能 App Privacy Reportのアプリコンテンツを表示するファイルをダウンロードするには、[設定]> [プライバシー]> [アプリのアクティビティを記録]を選択します。(77758720) Reality Composer 既知の問題 Reality Composerで新しいプロジェクトを作成できない場合があります。 (79418400)回避策:macOSのReality Composerで新しいプロジェクトを作成し、AirDropまたはMailを介して.rcprojectファイルをデバイスに転送します。 Safari 新機能 下部のタブバーは、ページコンテンツの下に表示されるように再設計されています。 上部にアドレスバーを表示するオプションも利用できます。 (81118141) 既知の問題 iPhoneのSafari Web Extensionポップオーバーの入力フィールドをタップすると、拡張機能UIが上に移動してキーボード用のスペースが確保されない場合があります。 (81676564) SharePlay 非推奨 beta 7および今後のbetaリリースでのSharePlay開発には、更新されたSharePlay Development Profileのインストールが必要です。 このプロファイルにより、iOS 15、iPadOS 15、tvOS 15 beta 7、およびmacOS Monterey beta 6のGroup Activities APIを介してGroupSessionsを正常に作成および受信できます。(81816137) ShazamKit 既知の問題 SHMediaLibraryのデフォルトインスタンスに追加されたメディアアイテムは、Shazamに表示されません。 (77785557)回避策:音楽認識コントロールセンターモジュールを長押しして、SHMediaLibraryのコンテンツを表示します。 Siri 既知の問題 VoiceOverおよびSpoken Contentのユーザーは、最初は利用可能なすべての音声オプションを表示できない場合があります。 音声オプションは、しばらくすると入力されます。 (79463000) デバイス上の音声認識は、中国語(マンダリン - 中国本土)、英語(オーストラリア)、英語(カナダ)、英語(英国)、および英語(米国)のみをサポートします。 (78483609) SKAdNetwork 新機能 開発者が獲得ポストバックを受け取ることを選択した場合、デバイスは、宣伝されたアプリの開発者に獲得ポストバックのコピーを送信できるようになりました。 (75054513) Swift New Features 新しいSwiftの値タイプAttributedStringが、Swift文字列と同じ文字カウント動作で使用できるようになりました。 完全にローカライズ可能で、Markdown、Codable、強く型付けされた属性などのサポートも含まれています。 (27227292) NotificationCenterは、async/awaitを使用して通知を受信するための新しいAsyncSequence APIを含んでいます。 (74401384) for await note in NotificationCenter.default.notifications(named: .MyNote) { // Use note. } 既知の問題 Combineに依存するSwiftライブラリは、armv7およびi386アーキテクチャを含むターゲット用にビルドできない場合があります。 (82183186、82189214)回避策:影響を受けない更新バージョンのライブラリを使用するか(利用可能な場合)、armv7およびi386のサポートを削除します(たとえば、ライブラリの展開ターゲットをiOS 11以降に増やします)。 iOS 15またはmacOS 12 SDKを使用してRealityKitにリンクしているアプリケーションは、以前のOSでは起動できません。 (79584511)回避策:Xcodeプロジェクト設定にOTHER_LD_FLAGS = -weak_framework RealityFoundationを追加して、古いOSでRealityKitアプリを実行できるようにします。 Settings 既知の問題 Sound Actions機能がSwitch Controlの一部として機能している間、設定アプリのPracticeとマークされた領域でサウンドが検出されません。 (82411537) SwiftUI 新機能 LocalizedStringKeyにMarkdown構文を含めることができるようになりました。 LocalizedStringKeyからTextビューを作成すると、文字列リテラルで作成されたTextビューを含め、マークダウン文字列が解析されます。 システムは、Markdown構成に従ってTextのスタイルを設定します。 (74515884) AttributedString構造からTextを作成できます。 Textは、SwiftUI属性スコープ内の属性を介して提供するスタイルを尊重します。 これらのスタイルは、ビュー修飾子を介して提供するスタイルよりも優先されます。 (74841755) 特定の種類のアニメーションがメインスレッドから実行されるようになったため、新しいスレッドセーフ要件があります。 (70524799)次の関数とタイプがスレッドセーフであることを確認してください: AlignmentID、Animatable、EnvironmentKey、EnvironmentValues、Equatable、GeometryEffect、Hashable、Identizable、PreferenceKey、Shape、VectorArithmeticのプロトコルに準拠するタイプのすべてのメソッドとアクセッサー。 以下のタイプと関数に渡すクロージャ。(ただし、それらを作成したビューにObservableObjectタイプへの参照がない場合に限る):ForEach、GeometryReader、backgroundPreferenceValue(_ :_ :)、overlayPreferenceValue(_ :_ :)、transformPreference(_ :_ :)、anchorPreference(key:value:transform:)、transformAnchorPreference(key:value:transform:)、transformEnvironment(_ :transform:)、transaction(_ :) NSFormatterが提供するTextFieldは、ユーザーが入力するとバインディングを更新するようになりました。 NSFormatterは、ユーザーがフィールドを送信したとき、またはフォーカスがフィールドから離れたときに、フィールドのテキストをフォーマットします。 (67899823) DisclosureGroupは、行をタップしたときに展開を切り替えるようになりました。 (62208702) デフォルトのListStyleがinsetGroupedになりました。 (75072988) TextFieldラベルは、フォームのフィールドの横には表示されません。 プロンプトパラメータを使用して、フィールドの明示的なプレースホルダーを指定します。 (61260160) FormatStyleを使用してTextを初期化できるようになりました。 (72159423) 検索中に、searchCompletion(_ :)修飾子を使用する提案をタップすると、選択した単一の提案が表示されるのではなく、提案リストが非表示になります。 (76965399) 以前のtitleパラメーターの代わりにpromptパラメーターを使用して、検索可能な修飾子が構成する検索フィールドのプロンプトをカスタマイズできるようになりました。 (77988967) SwiftUIはtextSelection修飾子をサポートするようになりました。 (77827592) 境界線の付いたボタンの形状を制御するために使用できるbuttonBorderShapeが追加されました。 (79456465) 新しいAttributedString属性underlineStyleとstrikethroughStyleをAttributeScopes.SwiftUIAttributesに追加しました。 (78437803) Animatableプロトコルに準拠し、さらにViewまたはViewModifierプロトコルのいずれかに準拠するタイプは、値が変更されたときにアニメーションを適用するようになりました。 その結果、AnimatableModifierプロトコルはソフト非推奨になりました。 最新のOSバージョンをターゲットにする場合は、Animableを直接使用します:例えば、struct CustomModifier:AnimatableModifierではなく、struct CustomModifier:ViewModifer, Animatableを使用します。 (76971100) contentShape(_ :eoFill:)修飾子により、さまざまな種類のシェイプをきめ細かく制御できるようになりました。 ドラッグプレビュー、ホバーエフェクト、およびコンテキストメニューの場合、iOS 15.0以降でリンクされたときにプレビューの形状に影響を与えるには、一致するContentShapeKindsが必要です。 デフォルトの動作は、interactionの種類を設定することです。 (60792377) openURLの環境値は、LinkビューでのURL処理やTextビューに埋め込まれたリンクなど、ビュー階層でのURL処理をカスタマイズするために設定し使用できるようになりました。 (78551237) Taskは、新しいタスクを生成するとき、使用される優先度を渡すことができるようになりました。 (80599258) 過剰な行の高さの文字を含むTextビューでは、サイズの大きい文字のクリッピングやオーバーラップを回避するために、デフォルトサイズが大きくなりました。 (80665315) isDetailLink(false)を使用するiPadのサイドバーのNavigationLinkは、詳細エリアではなくサイドバーに正しくプッシュするようになりました。 (80919171) 既知の問題 OutlineGroupへのバインディングを提供するには、init(_ :children:content:)のキーパスパラメーターにwrappedValueを含める必要がある場合があり、iOS & iPadOS 14以前では使用できません。 (77890799) FocusStateを使用して新しく追加されたList行にビューをフォーカスするには、メインの実行ループが次に実行されるときにフォーカス状態プロパティの更新を延期する必要があります。 (78607356) Listは、SwiftUIのセーフエリアインセットを尊重しなくなりました。 (82295913) 非推奨 controlProminenceは非推奨になりました。 代わりに、新しい.borderedProminent ButtonStyleを使用してください。 (78908460) 関数(Fn)ショートカット修飾子は非推奨になり、システムで使用するために予約されています。 (78627099) TabularData 新機能 TabularDataは、表形式データの分析と操作に使用する新しいSwiftフレームワークです。 DataFrameを使用して、CSVファイルとJSONファイルを読み取ったり、データを結合、グループ化、集計したりできます。 (69982458) UIKit 新機能 iOS 15 beta SDKに対してコンパイルされたアプリの場合、テキストビューとテキストフィールドに入力するときに、キーコマンドがテキスト入力とテキスト編集コマンドをインターセプトすることはなくなりました。 たとえば、Deleteキーを押すと常に文字が削除され、Deleteキーコマンドが存在する場合もトリガーされません。 キーコマンドがテキスト入力をインターセプトするようにするには、キーコマンドでwantsPriorityOverSystemBehaviorプロパティをtrueに設定します。 これは、矢印やタブキーの押下などのフォーカスキーボードナビゲーションコマンドよりもキーコマンドを優先させるためにも必要です。 (55118263) iOS 14およびiPadOS 14以前では、autocorrectionTypeがUITextAutocorrectionTypeNoに設定されている場合、QuickTypeバーは無効になります。 iOS 15およびiPadOS 15以降に対してリンクされたアプリの場合、QuickTypeバーが有効になり、スペルチェックの候補が表示されます。 新しい動作がユースケースにとって望ましくない場合は、spellCheckingTypeをUITextSpellCheckingTypeNoに設定して、QuickTypeバーを非表示にします。 (68874861) iOS 15 beta SDKを使用してコンパイルすると、いくつかのキーウィンドウ関連のプロパティ、メソッド、および通知の動作が変わります: isKeyWindowは、ウィンドウがアプリではなくシーンのキーである場合にtrueを返します。 becomeKeyWindowは、シーンの中でウィンドウがアプリからキーになったときに呼び出されます。 didBecomeKeyNotificationは、ウィンドウがアプリではなくシーンのキーになったときに通知します。 resignKeyWindowは、ウィンドウがアプリからシーン内のキーウィンドウステータスでなくなったときに呼び出されます。 didResignKeyNotificationは、ウィンドウがアプリではなくシーン内のキーウィンドウステータスでなくなったときに通知します。 (72873846) Xcode 既知の問題 音楽リクエストを含むコンテンツの読み込みなどのMusicKit機能は、シミュレートされたデバイスでは機能しません。 (78559381)
- 投稿日:2022-01-19T18:55:09+09:00
【RxSwift】Rx の Extension を作成するパターン色々
実現したいこと Binder や Observable を作成してコードの整理を行いたい 環境 ReactiveCocoa/ReactiveSwift 6.7.0 ReactiveX/RxSwift 6.2.0 Xcode Version 13.1 (13A1030d) 基本形: .rx. の形で呼び出せるようにする 元を辿ると .rx. でアクセスしているのは Reactive<Base> 定義上 .rx. でアクセス可能な Reactive<Base> の Base は NSObject になる 拡張したい class を Base として持つ Reactive<Base> を拡張することで .rx. で呼び出せるようにする SampleClass+Rx.swift // MARK: - SampleClass extension Reactive where Base: SampleClass { // ここに何か .rx.◯◯ とアクセスしたいプロパティを定義する. } パターン1: Binder で受け口を作る 通常のプロパティに関しては RxSwift ライブラリ側で自動で Binder が作られる 例: isEnabled, isHidden など 内部的には KeyPath Member Lookup を利用してプロパティごとに Binder を作成している ユースケースとして基礎的なクラス(UIButton など)に対して、元の値を触らず実効的には didSet を行うような挙動が可能 UILabel+Rx.swift // MARK: - UILabel extension Reactive where Base: UILabel { /// 赤字による文字更新. var textWithRed: Binder<String?> { return Binder<String?>(self.base) { (label: UILabel, value: String?) in label.text = value label.textColor = .red } } } パターン2: イベントが呼び出されたタイミングを知りたい @objc となっているイベントは呼び出されたタイミングを取得可能 ユースケースとしては画面のライフサイクルに関連するイベントが呼び出された際にそれに応じた処理を行うなど UIViewController+Rx.swift // MARK: - UIViewController extension Reactive Base: UIViewController { /// view の表示直前を表す Observable var viewWillAppear: Observable<Void> { return self.sentMessage(#selector(base.viewWillAppear(_:))) .map { _ -> Void in return () } .share(replay: 1) } } Reactive.sentMessage(#selector) の返却値は Observable<[Any]> 今回はタイミングが知りたいだけなので map で Void に変換している .share(replay: 1) については こちらの記事 が詳しいです ざっくり言うと複数購読されても余計なストリームを流さないようにしている パターン3: クロージャによる非同期処理を Rx に変換する 以下のようなクロージャによる非同期処理を持つクラスがあった場合に、処理を Rx 化したい SampleClass.swift // MARK: - SampleClass class SampleClass: NSObject { func fetchData(url: URL, completion: @escaping ((Result<Data, Error>) -> Void)) { // テストのため3秒後に .success を返却する. DispatchQueue.main.asyncAfter(deadline: .now() + 3) { completion(.success(Data())) } } } SampleClass+Rx.swift // MARK: - SampleClass extension Reactive where Base: SampleClass { func fetchData(url: URL) -> Observable<Result<Data, Error>> { return Observable<Result<Data, Error>>.create { [weak base] (observer: AnyObserver<Result<Data, Error>>) in base?.fetchData(url: url, completion: { (result: Result<Data, Error>) in observer.onNext(result) observer.onCompleted() }) return Disposables.create() } } } パターン4: デリゲートメソッドが発火したタイミングが知りたい 以下のようなデリゲートを持つクラスがあった場合に、デリゲートのメソッドが呼ばれたタイミングを知りたい SampleClass.swift // MARK: - SampleClass open class SampleClass: NSObject { weak var delegate: SampleClassDelegate? public func processA() { self.delegate?.processA?() } public func processB(by value: String) -> String? { return self.delegate?.processB(by: value) } } // MARK: - SampleClassDelegate @objc public protocol SampleClassDelegate: AnyObject { @objc optional func processA() func processB(by: String) -> String? } 手順1: Rx**DelegateProxy を作成 DelegateProxyType と SampleClassDelegate を準拠させる SampleClassDelegate で optional として定義されているかどうかで扱いが異なるため注意 同様に返却値が Void かどうかでも扱いが異なるため注意 RxSampleClassDelegateProxy.swift // MARK: - RxSampleClassDelegateProxy public class RxSampleClassDelegateProxy: DelegateProxy<SampleClass, SampleClassDelegate> { // processB のイベント発火検知用 Subject internal lazy var processBDidInvokedSubject = PublishSubject<String?>() // ParentObject および Delegate は DelegateProxy で定義されている. // 今回は SampleClass が ParentObject 、 SampleClassDelegate が Delegate にあたる. public init(by value: ParentObject) { super.init(parentObject: value, delegateProxy: RxSampleClassDelegateProxy.self) } // Proxy が破棄されるタイミングで complete をイベントとして流す. deinit { self.processBDidInvokedSubject.onCompleted() } // DelegateProxy の Subclass である RxSampleClassDelegateProxy を factory に登録する.(実装必須) public static func registerKnownImplementations() { self.register { RxSampleClassDelegateProxy(by: $0) } } } // MARK: - DelegateProxyType extension RxSampleClassDelegateProxy: DelegateProxyType { // ParentObject が持つ delegate を返却する. public static func currentDelegate(for object: ParentObject) -> Delegate? { return object.delegate } // ParentObject が持つべき delegate を設定する. public static func setCurrentDelegate(_ delegate: Delegate?, to object: ParentObject) { object.delegate = delegate } } // MARK: - SampleClassDelegate extension RxSampleClassDelegateProxy: SampleClassDelegate { // delegate の定義上で必須となっているメソッドの定義方法. // Proxy を通じて本来の delegate 実装先を発火させる. // ここで定義したメソッドは sentMessage / methodInvoked ではイベント検出できない. // また、返却値のあるメソッドは sentMessage / methodInvoked が呼ばれないためここでイベントを流す必要がある. public func processB(by value: String) -> String? { let result: String? = _forwardToDelegate?.processB(by: value) self.processBDidInvokedSubject.onNext(result) return result } } 手順2: Reactive の extension を作成 以下の2パターン sentMessage / methodInvoked を利用する Rx**DelegateProxy で定義しておいた PublishSubject を利用して Observable を作成 SampleClass+Rx.swift // MARK: - Reactive extension Reactive where Base: SampleClass { public var delegateProxy: RxSampleClassDelegateProxy { RxSampleClassDelegateProxy.proxy(for: self.base) } // void を返却する optional のメソッドは sentMessage / methodInvoked が利用可能. public var processAWillCalled: Observable<Void> { return self.delegateProxy .sentMessage(#selector(SampleClassDelegate.processA)) .map { _ -> Void in return () } .share(replay: 1) } public var processADidCalled: Observable<Void> { return self.delegateProxy .methodInvoked(#selector(SampleClassDelegate.processA)) .map { _ -> Void in return () } .share(replay: 1) } public var processBDidCalled: Observable<String?> { return self.delegateProxy.processBDidInvokedSubject.asObservable() } // Rx**DelegateProxy で定義した delegate のメソッドは sentMessage / methodInvoked が呼ばれない. // 以下の実装は呼ばれない. /* public var processBWillCalled: Observable<Void> { return self.delegateProxy .sentMessage(#selector(SampleClassDelegate.processB(by:))) .map { _ -> Void in return () } .share(replay: 1) } public var processBDidCalled: Observable<Void> { return self.delegateProxy .methodInvoked(#selector(SampleClassDelegate.processB(by:))) .map { _ -> Void in return () } .share(replay: 1) } */ } 使用例 // MARK: - TestViewController class TestViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.setupRx() self.test() } // MARK: - Private private let disposeBag = DisposeBag() private let sampleClass = SampleClass() private func setupRx() { self.sampleClass.rx.processAWillCalled .subscribe(onNext: { print("Process A Will Called") }) .disposed(by: self.disposeBag) self.sampleClass.rx.processADidCalled .subscribe(onNext: { print("Process A Did Called") }) .disposed(by: self.disposeBag) self.sampleClass.rx.processBDidCalled .subscribe(onNext: { print("Process B Did Called") }) .disposed(by: self.disposeBag) } private func test() { // SampleClassDelegate を実装しなくても rx にストリームを流せる. self.sampleClass().processA() _ = self.sampleClass().processB() } } 注意点 SampleClassDelegate メソッドを実装しなくてもイベントは検出可能になるが、あくまで「串(Proxy)」を刺してイベントの発火を検出しているだけなので扱いには注意 不明点 DelegateProxy.swift の sentMessage の定義箇所に以下のコメントがある Only methods that have void return value can be observed using this method because those methods are used as a notification mechanism. It doesn't matter if they are optional or not. メソッドの返却値が void であれば optional であっても sentMessage が使えるような書き振りだが、 実際 optional ではないメソッドは Rx**DelegateProxy に定義する必要があり、 そのため sentMessage でイベントを取得できなかった なにか別の実装方法があるのかもしれませんが、ちょっと思いつきませんでした 参考 GeolocationSampleから学ぶdelegateのRx対応 [Swift]Dynamic Member LookupからSwift5.1で追加されたKeyPath Member Lookupまで [RxSwift] shareReplayをちゃんと書いてお行儀良くストリームを購読しよう
- 投稿日:2022-01-19T10:54:29+09:00
ARの使い道が思いつかない人へ ARとはセンシングであるという考え方と、具体例 ARで紙を切り取る
ARの使い道の考え方と、その具体例です。 具体例では、画像を登録して、現実の紙からその部分を切り取ります。 ARってなかなか使い道がわからない ARって楽しそうなものの、イマイチどういうアプリケーションに使えばいいかわからない。 現実のものとどうインタラクトするかが鍵 リアリティを拡張するもの、ということは、ベースとなるリアルの理解が鍵となります。 ARはセンシングである 現実を拡張するためには、現実を理解する必要があります。 ARとは、コンテンツを表示するもの、というのに加えて、 「センシングである」 というところから考えてみます。 ARコンテンツの配置は、あくまで現実を拡張する手段の一つであって、 コンテンツを現実のどこに配置するか、それによって何の現実を拡張しているか、がARの目的を形作るのではないでしょうか。 となると、まずは現実をどのように認識するのか、というセンシングの部分が重要になります。 ARKitはワールドトラッキングやフェイストラッキングから始まり、最近ではLidarによるオブジェクト理解など、 現実をセンサーによって把握する部分が鍵となっています。 現実から考えよう バーチャルコンテンツではなく、まずは理解すべき現実から考えてみましょう。 あなたの周りにはどんな現実のものがありますか? それは、コンピュータによってどのようにセンシングして理解することが可能でしょうか? 最後に、その現実をどのように拡張すれば、面白い効果、便利な効果が得られるでしょうか? 例 画像を登録して、現実の紙からその部分を切り取ります。 私たちの周りには、様々な画像があります。 雑誌の表紙、ポスター、広告。 そこには、多くの人の顔が写っています。 幸い、機械学習のおかげで、画像を認識し、顔を検出することができます。 ここがセンシングの部分です。 顔という現実をARに与え、拡張してみましょう。 拡張は、何も加えることだけではありません。 それを無くす(減らす)ことも拡張と言えます。 把握した顔を、切り取って無くしてみましょう。 まずは顔の把握フェーズです。 Visionで、カメラフレームから顔を検出して、顔だけの画像を作成します。 let request = VNDetectFaceRectanglesRequest() let pixelBuffer = frame.capturedImage let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer) do { try handler.perform([self.qualityRequest]) guard let result = request?.results?.first as? VNFaceObservation else { return } let boundingBox = result.boundingBox let ciImage = CIImage(cvImageBuffer: pixelBuffer) let faceRect = VNImageRectForNormalizedRect((boundingBox),Int(ciImage.extent.size.width), Int(ciImage.extent.size.height)) let croppedImage = ciImage.cropped(to: faceRect) } catch let error { print(error) } 作成した顔の画像をARReferenceImageとして登録します。 これによって、ARKitは顔の画像を認識し、画像アンカーを作成してコンテンツを配置できるようになります。 guard let cgImage = context.createCGImage(croppedImage, from: croppedImage.extent), let imageData = UIImage(cgImage: cgImage).pngData(), let url = try? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("temp.png"), ((try? imageData.write(to: url)) != nil) else {isRequesting = false; return} let referenceImage = ARReferenceImage(cgImage, orientation: .up, physicalWidth: 0.1) let config = ARImageTrackingConfiguration() config.trackingImages = [referenceImage] arView.session.run(config, options: [.removeExistingAnchors]) ARKitで登録した顔を認識し、そこに顔画像を貼り付けたボックスを置いて、移動させることで切り抜きに見せます。 func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { for anchor in anchors { guard let imageAnchor = anchor as? ARImageAnchor else {continue} Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { timer in let anchorEntity = AnchorEntity(anchor: imageAnchor) var material = UnlitMaterial(color: .white) let texture = try? TextureResource.load(contentsOf: self.url!) material.baseColor = MaterialColorParameter.texture(texture!) let faceBox = ModelEntity(mesh: .generateBox(size: [0.1,0.02,Float(imageAnchor.referenceImage.physicalSize.height)]), materials: [material]) let croppedBox = ModelEntity(mesh: .generateBox(size: [0.1,0.02,Float(imageAnchor.referenceImage.physicalSize.height)]), materials: [SimpleMaterial(color: .black, isMetallic: true)]) croppedBox.position = [0,-0.01,0] anchorEntity.addChild(croppedBox) anchorEntity.addChild(faceBox) self.arView.scene.addAnchor(anchorEntity) faceBox.move(to: Transform(translation:[0,0,0.3]), relativeTo: faceBox, duration: 3, timingFunction: .easeInOut) } } } 身の回りからはじめよう あなたの周りにあるもので、コンピュータによって把握できるものはなんですか? コンピュータはどのようにそれらのものを認識しますか? そして、あなたはそれをどのように拡張できますか? ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium