- 投稿日:2022-03-03T22:53:30+09:00
ARKitのフレームを解析するときの注意点
ARKitのCapturedFrameをVisionなどで解析してARインタラクションを作る場合があるが、 この際ARKitSessionDelegateで取得できるフレームとディスプレイに表示されているフレームの領域が違うことがある。(キャプチャされた一部分を表示していることがある) 例えば、iPhone11ではディスプレイが 414x896 だが、キャプチャしたフレームサイズは 1440x1920 である。 ディスプレイ内にフレーム縦幅は全て収まっているが、横は中心部しか収まっていない。 ディスプレイに表示されている領域でフレームをクロップするコード。 func session(_ session: ARSession, didUpdate frame: ARFrame) { let pixelBuffer = frame.capturedImage let ciImage = CIImage(cvPixelBuffer: pixelBuffer, options: [:]).oriented(.right) let aspect = arView.bounds.width / arView.bounds.height let widthForDisplayAspect = ciImage.extent.height * aspect let cropped = ciImage.cropped(to: CGRect(x: ciImage.extent.width/2-widthForDisplayAspect/2, y: 0, width: widthForDisplayAspect, height: ciImage.extent.height)) } これを解析すれば、ディスプレイ表示されたフレームを基準に解析できる。 ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium GitHub
- 投稿日:2022-03-03T20:19:46+09:00
SwiftUI Tutorial の応用
内容 SwiftUI Tutorial の応用 開発環境 ハードウエア 項目 PC MacBook Air(M1,2020) メモリ:16GB ストレージ:1TB 実機 iPhoneSE(2nd Generation 128GB iOS 15.3.1) PCと実機を接続する USB-C Digital AV Multiportアダプタ ソフトウエア 項目 言語 Swift 5.5.2 IDE Xcode Ver 13.2.1 その他 Visual Studio Code Ver 1.64.2 バージョン管理 GitHub - 実際にコードを確認できます アプリの内容 画面 駅一覧画面 駅詳細画面 機能 駅一覧画面 路線選択タブで路線を切替 お気に入りのみ表示 駅詳細画面 お気に入り機能 隣の駅を表示 ただし大阪梅田駅は、次の駅のみ(宝塚・神戸三宮・京都河原町方向)を表示 ただし宝塚駅、神戸三宮駅および京都河原町駅は、前の駅のみ(大阪梅田方向)を表示 応用 画面上部にタブを追加 LNE / MoneryForward ME 等に実装されている 駅一覧画面 import SwiftUI struct StationListView: View { @EnvironmentObject var modelData: ModelData @State private var showFavoriteOnly = false @State private var selection: Line = .Takarazuka var filteredStations: [Station] { modelData.stations.filter { station in (!showFavoriteOnly || station.isFavorite) && station.line == selection } } var body: some View { // GeometryReader を使うことがポイント GeometryReader { geometry in NavigationView { VStack(spacing: .zero) { // LineTabView() で画面上部のタブを実装 LineTabView(selection: $selection, geometrySize: geometry.size) List{ Toggle(isOn: $showFavoriteOnly){ Text("お気に入りのみ表示") } ForEach(filteredStations) { station in NavigationLink { StationDetail(station: station) } label: { StationRow(station: station) } } } Spacer() } .navigationBarTitle("駅一覧",displayMode: .inline) } .navigationViewStyle(.stack) // iPad のサイドビューでは画面上部タブのレイアウトが崩れるため、.navigationViewStyle を .stack に指定している } } } struct StationListView_Previews: PreviewProvider { static var previews: some View { StationListView() .environmentObject(ModelData()) } } 画面上部のタブ import SwiftUI import MapKit struct LineTabView: View { @Binding var selection: Line var geometrySize: CGSize var body: some View { HStack(spacing: .zero) { ForEach(Line.allCases,id: \.self){ line in VStack { Button { self.selection = line print("\(line.getLineName())を選択しました") } label: { Text("\(line.getLineName())") .font(.body) .fontWeight(.heavy) .foregroundColor(self.selection == line ? line.getLineColor() : .gray) } .frame(width: geometrySize.width / CGFloat(Line.allCases.count), height: 25) Rectangle() .fill(self.selection == line ? line.getLineColor() : .gray) .frame(width: geometrySize.width / CGFloat(Line.allCases.count), height: 2) } } } } } struct LineTabView_Previews: PreviewProvider { static var geometry: CGSize = CGSize(width: 200, height: 44) static var previews: some View { LineTabView(selection: .constant(.Takarazuka), geometrySize: geometry) } } その他 隣の駅を表示する処理のテストコードを書く & XCode でテストを実行 import XCTest @testable import Sample class SampleTests: XCTestCase { // 省略 func testGetNextStation() throws { var count = 0 let stations = ModelData().stations stations.map { station in print("\(count) : \(station)") count += 1 } let takarazukaOsakaUmeda = stations[0] let takarazukaNakatsu = takarazukaOsakaUmeda.getNextStation() // 期待する結果とコードの処理結果が等しければ、SUCCESS XCTAssertEqual(takarazukaNakatsu?.id, "takarazuka02") let takarazukaTakarazuka = stations[18] XCTAssertEqual(takarazukaTakarazuka.getNextStation()?.id, nil) let kobeOsakaUmeda = stations[19] let kobeNakatsu = kobeOsakaUmeda.getNextStation() XCTAssertEqual(kobeNakatsu?.id, "kobe02") let kobeKobeSannomiya = stations[34] XCTAssertEqual(kobeKobeSannomiya.getNextStation()?.id, nil) let kyotoOsakaUmeda = stations[35] let kyotoJuso = kyotoOsakaUmeda.getNextStation() XCTAssertEqual(kyotoJuso?.id, "kyoto03") let kyotoKyotoKawaramachi = stations[62] XCTAssertEqual(kyotoKyotoKawaramachi.getNextStation()?.id, nil) } func testGetPreviousStation() throws { let stations = ModelData().stations let takarazukaOsakaUmeda = stations[0] XCTAssertEqual(takarazukaOsakaUmeda.getPreviousStation()?.id, nil) let takarazukaNakatsu = stations[1] XCTAssertEqual(takarazukaNakatsu.getPreviousStation()?.id, takarazukaOsakaUmeda.id) let takarazukaTakarazuka = stations[18] XCTAssertEqual(takarazukaTakarazuka.getPreviousStation()?.id, "takarazuka55") } // 省略 } テスト実行結果 Test Successed 今後 おでかけスポット画面を追加 GitHub Actions を試してみる 感想 慣れたら UIKit よりもラク ダークモードが自動で反映されるのでラク Enum を上手く使うと改修がラク 参考資料 SwiftUI Tutorial 画面上部のタブを実装する際に参考にした記事
- 投稿日:2022-03-03T17:09:02+09:00
【iOS】Storyboardを使わずUIを作成するために必要なXCodeの初期設定
目的 Storyboardを使わずにUIを作成する場合、必要となるXCodeの初期設定について説明 流れ ①: プロジェクトを作成し、実行できることを確認(Storyboardあり) ②: Main.storyboard を削除 ③: SceneDelegate.swift のコードを全てコメントアウト ④: MainInterface の設定を変更 ⑤: info.plist の Storyboard Name を削除 ⑥: ViewController のプログラム変更 ⑦: 実行結果 ①: プロジェクトを作成し、実行できることを確認(Storyboardあり) 新規でプロジェクトを作成し、「▶︎」ボタンを押して実行可能なことを確認する ②: Main.storyboard を削除 Main.storyboard上で右クリック → 「Delete」 → 「Move To Trash」 ③: SceneDelegate.swift のコードを全てコメントアウト SceneDelegate.swift のコードを全て選択して、「Command + /」 でコメントアウト AppDelegate.swift のコードを変更 AppDelegate.swift import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // アプリ起動時に表示する View を格納する変数 // 起動時に実行 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.makeKeyAndVisible() window?.rootViewController = ViewController() // アプリ起動時に表示するViewを指定 return true } // MARK: UISceneSession Lifecycle // // func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // // Called when a new scene session is being created. // // Use this method to select a configuration to create the new scene with. // return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) // } // // func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // // Called when the user discards a scene session. // // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // // Use this method to release any resources that were specific to the discarded scenes, as they will not return. // } } ④: MainInterface の設定を変更 画面左上にある「"アプリ名"」 → 「Target」 → 「General」 → Deployment Info の 「Main Interface」 → Main を削除(空欄にする) ⑤: info.plist の編集 「info.plist」 → 「Application Scene Manifest」 → 「Application Session Role」 → 「item 0(Default Configuration)」 → 「-」(項目にカーソルを合わせると出てくる) ※ SceneDelegate で画面を呼び出す時は、「item 0(Default Configuration)」を消すのではなく、「item 0(Default Configuration)」でいらないものを消す。いらないもの については今回省略します! ⑥: ViewController のプログラム変更 ViewController.swift import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green } } ⑦: 実行結果 見事、StoryboardなしでUI作成を行えました! 参考サイト
- 投稿日:2022-03-03T12:50:31+09:00
【Flutter】IOSのモーダルアラートを日本語にLocalizeする方法
はじめに Flutterで開発したIOSアプリを審査に提出した際、 モーダルアラートが日本語になっていないとのことでリジェクトされました。 当方、IOSアプリの経験がなく結構ハマったため 同じようにハマられている方向けに解決策を記載します。 リジェクト内容の詳細 要約すると 日本語設定なのにフォトライブラリのモーダルアラートが英語になっています。 フォトライブラリのモーダルアラートとはこのことです。 赤枠の部分を日本語にしなければいけないみたいですね。 原文 Guideline 4.0 - Design We noticed an issue in your app that contributes to a lower-quality user experience than Apple users expect: Your app's photos modal alerts are written in English while the app is set to the Japanese localization. To help users understand why your app is requesting access to a specific feature, your app's modal alerts should be in the same language as your app's current localization. Next Steps To resolve this issue, please revise your app to address all instances of this type of issue. 日本語にする方法 プロジェクトに日本語を追加する Xcodeで対象のFlutterプロジェクトを開きます 左のサイドメニューのRunnerを選択 選択すると真ん中の画面の左側にPROJECTとTARGETSが表示されるはずです。 PROJECT配下のRunnerを選択 真ん中画面下部のLocalizationsの+ボタンをクリックしJapaneseを追加 Finishをクリック アラートのテキストを日本語にする アラートのテキストはRunner > Runner > Info.plistで設定できますが、 これを日本語にローカライズするには新しくファイルを作り、日本語のテキストを記載します。 1. ファイルを作成する 左のサイドメニューのRunner > Runnerを選択して File > New > File...を選択 Strings.Fileを選択してNext ファイル名はInfoPlist.stringsにします。 保存場所はXcodeで見るとRunner > Runner Vscodeで見ると Flutter project > ios > Runner 2. InfoPlist.stringsをローカライズする InfoPlist.stringsを選択したら右側にLocalize...ボタンが表示されるのでクリックします。 クリックするとDo you want to localize this file?と聞かれるので Japaneseを選択してLocalizeをクリックします。 3. 対象のKeyを探す 最初にInfo.plistを開いて該当箇所を探します。 今回はフォトライブラリのアラートなのでKeyはNSPhotoLibraryUsageDescriptionになります。 Info.plist <key>NSPhotoLibraryUsageDescription</key> <string>{アラートに表示する説明文(英語)}</string> ※{アラートに表示する説明文(英語)}を変更するとアラートの説明文も変更されます。 4. 日本語対応させる 先ほど作成したInfoPlist.stringsを開き NSPhotoLibraryUsageDescriptionに対応する値を記載します。 NSPhotoLibraryUsageDescription = "アプリは選択した画像をアプリに投稿するためフォトライブラリへのアクセスが必要です"; 再度ビルドしてアラート内の説明文が変わっていたら成功です!