20211130のiOSに関する記事は6件です。

SceneKitでサイコロを振る(Swift)

はじめに iOS アプリをつくっているとだれしも一度はサイコロを振りたい!と思ったことがあるはずです今回は SceneKit を使ってサイコロを振ってみます。 完成形はこんな感じです。 SwiftUIでもサイコロ振りたかったけどDelegateまわりがややこしくて断念した? pic.twitter.com/qsHSjEeqRl— am10 (@am103141592) November 30, 2021 ソース ソース全体はこんな感じです。 ソース全体 import UIKit import SceneKit final class ViewController: UIViewController { @IBOutlet private weak var scnView: SCNView! @IBOutlet private weak var resultLabel: UILabel! private var boxNode: SCNNode! private var timer: Timer? private let diceLength: CGFloat = 1.5 private let diceNumbers = ["1", "3", "6", "4", "5", "2"] override func viewDidLoad() { super.viewDidLoad() resultLabel.text = nil scnView.allowsCameraControl = true scnView.scene = SCNScene() // カメラをシーンに追加する let cameraNode: SCNNode = { let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 10, z: 10) cameraNode.rotation = .init(x: 1, y: 0, z: 0, w: -Float.pi/4) return cameraNode }() scnView.scene?.rootNode.addChildNode(cameraNode) // 床をシーンに追加する let floorNode: SCNNode = { let floor = SCNFloor() floor.reflectivity = 0.1 let floorNode = SCNNode(geometry: floor) floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: .init(geometry: floor)) floorNode.physicsBody?.contactTestBitMask = 1 return floorNode }() scnView.scene?.rootNode.addChildNode(floorNode) // サイコロをシーンに追加する boxNode = { let box = SCNBox(width: diceLength, height: diceLength, length: diceLength, chamferRadius: 0) box.materials = diceNumbers.map { let material = SCNMaterial() material.diffuse.contents = UIImage(named: $0) return material } let boxNode = SCNNode(geometry: box) boxNode.name = "dice" boxNode.position = SCNVector3(x: 0, y: 0, z: 0) boxNode.physicsBody = .init(type: .dynamic, shape: .init(geometry: box)) boxNode.physicsBody?.categoryBitMask = 1 return boxNode }() scnView.scene?.rootNode.addChildNode(boxNode) scnView.scene?.physicsWorld.contactDelegate = self } @IBAction private func rollDice() { resultLabel.text = nil timer?.invalidate() timer = nil boxNode.physicsBody?.angularVelocity = .init(x: Float.random(in: 0..<10), y: Float.random(in: 0..<10), z: Float.random(in: 0..<10), w: 1) boxNode.physicsBody?.velocity = .init(-3, 0, 0) boxNode.position = SCNVector3(x: 7, y: 10, z: 0) } } extension ViewController: SCNPhysicsContactDelegate { func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) { if contact.nodeA.name == boxNode.name { timer?.invalidate() timer = nil timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false, block: { [weak self] _ in DispatchQueue.main.async { self?.diceRollDidEnd() } }) } } private func diceRollDidEnd() { var worldPosition = boxNode.presentation.worldPosition worldPosition.y += Float(diceLength)/2 let projectPoint = scnView.projectPoint(worldPosition) let point = CGPoint(x: CGFloat(projectPoint.x), y: CGFloat(projectPoint.y)) if let result = scnView.hitTest(point).first, result.node.name == boxNode.name { resultLabel.text = diceNumbers[result.geometryIndex] } } } サイコロを表示する 各処理の説明です。まずサイコロを表示します。SCNBox を使ってキューブを表示し、materials プロパティに各面の画像を設定します(面の順番はよくわからなかったのでいろいろ試して並べました)。 画像はこちら(1 という名前で Xcassets に追加してます)。色を変えたりご自由にお使いください サイコロ サイコロ サイコロ ソースはこんな感じです。 @IBOutlet private weak var scnView: SCNView! private var boxNode: SCNNode! private let diceLength: CGFloat = 1.5 private let diceNumbers = ["1", "3", "6", "4", "5", "2"] boxNode = { let box = SCNBox(width: diceLength, height: diceLength, length: diceLength, chamferRadius: 0) box.materials = diceNumbers.map { let material = SCNMaterial() material.diffuse.contents = UIImage(named: $0) return material } let boxNode = SCNNode(geometry: box) boxNode.name = "dice" boxNode.position = SCNVector3(x: 0, y: 0, z: 0) // 床とぶつけるために設定 boxNode.physicsBody = .init(type: .dynamic, shape: .init(geometry: box)) boxNode.physicsBody?.categoryBitMask = 1 return boxNode }() scnView.scene?.rootNode.addChildNode(boxNode) サイコロを振る サイコロを振る処理は単純でサイコロにランダムに angularVelocity を設定し velocity と position の設定で画面の右側から落とすようにしています。timer はサイコロが転がり終わったかどうかの判定用です。 @IBOutlet private weak var resultLabel: UILabel! private var boxNode: SCNNode! private var timer: Timer? @IBAction private func rollDice() { // サイコロ振り終わったとき用 resultLabel.text = nil timer?.invalidate() timer = nil // サイコロを画面右側から落とす boxNode.physicsBody?.angularVelocity = .init(x: Float.random(in: 0..<10), y: Float.random(in: 0..<10), z: Float.random(in: 0..<10), w: 1) boxNode.physicsBody?.velocity = .init(-3, 0, 0) boxNode.position = SCNVector3(x: 7, y: 10, z: 0) } サイコロの出た目を取得する ここが一番苦労した部分です(たぶんもっといい方法があると思います)。サイコロが転がり終わったのを判定するために床とサイコロが衝突するたびにタイマーを設定しています。衝突した 1.5 秒後に出た目の計算処理をおこないます(1.5 秒以内に再び衝突するとまたタイマーがはじまる)。 出た目の計算処理ではサイコロの上の面の中心座標を取得して hitTest で materials のどのインデックスなのかを取得しています。 参考:Swiftで始まったタッチでヒットしたキューブの面を特定する-SceneKit extension ViewController: SCNPhysicsContactDelegate { func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) { if contact.nodeA.name == boxNode.name { // 床とサイコロがぶつかるたびにタイマーを設定する timer?.invalidate() timer = nil timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false, block: { [weak self] _ in DispatchQueue.main.async { self?.diceRollDidEnd() } }) } } private func diceRollDidEnd() { // ワールド座標系での現在のサイコロの上の面の中心座標を取得する var worldPosition = boxNode.presentation.worldPosition worldPosition.y += Float(diceLength)/2 // 2Dの座標に変換する let projectPoint = scnView.projectPoint(worldPosition) let point = CGPoint(x: CGFloat(projectPoint.x), y: CGFloat(projectPoint.y)) // 上の面がどこなのか取得する if let result = scnView.hitTest(point).first, result.node.name == boxNode.name { resultLabel.text = diceNumbers[result.geometryIndex] } } } おわりに サイコロを振るのに 3D 表示する必要があるので今まではむずかしくてなんども挫折していたのですがついにサイコロを振ることができました SceneView というのがあるようなので SwiftUI でもサイコロを振れそうです SceneView:ドキュメント 参考 SceneKit:ドキュメント SCNBox:ドキュメント SCNGeometrys:ドキュメント Swiftで始まったタッチでヒットしたキューブの面を特定する-SceneKit SceneKitで重力などの物理シュミレーションを設定する ARKitのための3D数学
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ内バナーの実装・運用をできるだけ楽にする(Swift × microCMS)

はじめに アプリの規模が大きくなってくると、販促やお知らせ、チュートリアル導線などのためにアプリ内にバナーを表示したいケースが出てきます。 (↑こういう画像バナーのイメージです) バナー表示に必要なのは「画像」「タップ時の遷移先URL」とシンプルですが、それなりの頻度でバナーを更新することを考えると運用ができるだけ楽になるようにホスティングしたい気持ちになってきます。 私がこれまで作っていた個人アプリでは、 JSONファイルを適当なサーバーに置く Firebaseなどを利用してそれ専用のAPIを作る などとしていましたが、更新が大変だったり、APIの管理が面倒だったりとモヤる点も多く、もっと良いソリューションがあるのではないかと感じていました。 この記事ではmicroCMSというサービスを利用して、バナー表示をできるだけ簡単に実装・運用する方法について書いてみます。 バナーを表示する まずはアプリのバナー表示を実装します。 管理画面から情報を入稿 バナーの「画像」と「遷移先URL」を管理画面から入稿します。 上記のように入稿してみました。 画像もmicroCMSでホスティングできるので、別のファイルストレージに画像をアップロードする必要はありません。 APIプレビューで確認 入稿した情報はアプリ側からAPI経由で取得することになります。 管理画面の「APIプレビュー」機能からリクエストを試せます。 ここでレスポンスを確認し、問題なければ上部に表示されているiOSのコードサンプルを例に実装します。 アプリに組み込み 今回はサンプルとして、写真ギャラリーアプリのヘッダー部分にバナーを表示していきます。 struct ContentView: View { let client = MicrocmsClient( serviceDomain: "<YOUR_SERVICE_DOMAIN>", apiKey: "<YOUR-API-KEY>") @State var banner: Banner? var body: some View { VStack(alignment: .leading) { if let banner = banner { ImageView(url: banner.image.url) .onTapGesture { UIApplication.shared.open(banner.url, options: [:], completionHandler: nil) } } } .onAppear { fetch() } } private func fetch() { client.get( endpoint: "banners") { result in switch result { case .success(let object): self.banner = Banner(dict: object as! [String: Any]) case .failure(let error): print("[ERROR] ", error) } } } } メインの実装部分だけピックして記載していますが、起動時にバナーをmicroCMSから取得して表示する実装になっています。 実行するとこのような感じ。バナー画像をタップすると入稿している「遷移先URL」を開きます。これでアプリの画面にバナーを実装することができました。 バナーを更新する(運用) 内容を変更したい場合はmicroCMSの管理画面から情報を更新すればOKです。 今回は簡単のため画像+遷移先URLだけにしていますが、必要なフィールドを追加することで、iOS/Androidで配信するバナーを分ける、アプリのバージョンによって出し分けるなどのケースにも柔軟に対応できます。 運用を助けるmicroCMSの機能 実際にバナー運用をしていて便利だと思った機能をいくつか紹介します。 画像API microCMSは画像向けCDNであるimgixと提携しており、柔軟なリクエストで画像を取得できます。(公式ドキュメント) 例えば以下のように画像URLの末尾に?w=200とつけると画像のwidthをリサイズして取得できます。 モバイルは様々な画面サイズが存在し、最も大きなものに合わせて画像を用意するとiPhone SEなどの小さな端末では過剰な画像サイズになります。 画像APIを使って端末ごとの表示領域に合わせたサイズの画像を取得することで、データ通信量を抑え、表示パフォーマンスを向上させることができます。 入稿ワークフロー microCMSにはレビュー機能というものがあります。 ( microCMS公式ブログより引用) GitHubのPull Requestライクな機能でコンテンツ更新をレビューできる機能です。 例えばバナーの入稿は誰でもできて、オーナー権限を持った人だけが承認&公開できるようなワークフローを組むことができます。(個人アプリは一人で運用してるので使っていませんが) 柔軟な入力フィールド ヘッドレスCMSはスキーマの型を柔軟に決められます。例えばバナーの「公開開始日」が追加で必要になった場合、日時フィールドを使うことになるでしょう。 日時フィールドだとカレンダーのUIが表示されています。フィールドに応じたUIで入力できるため入稿が簡単です。 JSONファイルを手動で管理していた時は日付の文字列を間違えないように入力していたので気持ち的に楽になり助かっています。 まとめ microCMSを使うとアプリ内バナーの運用が楽になった話を書きました。 これからバナー実装を考えている方の参考になれば幸いです。 サンプルコード あまり大した実装はしていませんが、こちらに置いています。 https://github.com/himaratsu/BannerSample
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSのアップデートをブロックするには

iOSのOTAアップデートをブロックする方法 ①サーバーの通信をブロックする 以下のドメインをルーターなどのブラックリストに登録して通信出来ないようにしてください。 Appleのサイトに記載されているドメインです。 appldnld.apple.com gg.apple.com mesu.apple.com updates-http.cdn-apple.com updates.cdn-apple.com xp.apple.com •メリット ①通信をブロックする事で強制的に解決することができる。 •デメリット ①Appleの一部のサービス(iCloud等)で不具合が起こる可能性がある。 ② TVOSのプロファイルを使用する(現在は不可) これが一番簡単で確実な方法です。 以下のサイトからプロファイルをダウンロードしてください。 Download Beta Profiles •メリット ①操作が比較的少ない。 ②iCloudが使えない等の不具合が発生しない。 •デメリット ①プロファイルの期限が切れると自動で削除されてしまう。 ②配布されるのが不定期でいつになるのかが分からない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのクラッシュを減らしアプリを安定運用(したい)[Carefully Unwrapping]

はじめに 本記事は with Advent Calendar 2021 2日目の記事です。 こんにちは。withでアプリ開発をしている @zrn-ns です。 withアドベントカレンダーの2日目を担当させていただきます。 注意 この記事で取り上げた内容はまだ検証中です。 最後の方に課題も記載していますので、もし採用する際は十分にご検討いただくようお願いします。 Optional型のアンラップ、どうしてますか? 早速ですが、Optional型をアンラップするとき、どのような方針をとっていますか? もちろん場合によって様々だと思いますが、たまにどうアンラップするか悩むことってないですか。 たとえば下記のような、ほぼ確実にアンラップに成功する気がするけど、どういうパターンでアンラップに失敗するのか分からない...っていうパターンです。 // stringにどんな値を入れればnilになる...? 強制アンラップして大丈夫...? let url: URL = .init(string: "https://google.com")! // じゃあOptional Bindingを使う...? guard let url: URL = .init(string: "https://google.com") else { return } 上記の例では固定の文字列を使っているので、一度成功すればその後もほぼ確実にアンラップに成功するはずなので、Forced Unwrappingを使用することが多いと思います。 しかし、ライブラリ側の更新やロジックの変更などにより、突然nilになる可能性もあります。 ではOptional Binding(やnil結合演算子)を使用すべきなのでしょうか... ? Forced UnwrappingとOptional Bindingの比較 ここでForced UnwrappingとOptional Bindingを比較してみます。 メリット デメリット Forced Unwrapping - 記述が簡略- 万が一nilになった場合、クラッシュが発生するので、Crashlytics等でロジックエラーの発生を検知できる- 意図しない状態でプログラムが進行するのを防ぐことができる - 万が一nilになった場合、ユーザ環境でクラッシュしてしまう Optional Unwrapping - 万が一nilになった場合に、ユーザ環境でのクラッシュを防げる - 基本的に発生しないパターンに対する記述が冗長になる- 万が一nilになった場合、ロジックエラーに気づけない(回避コードの内容による)- 意図しない状態でプログラムが進行してしまう 表を見てみると分かるように、それぞれメリット・デメリットがあります。 Forced Unwrappingは記述も容易ですし、Crashlyticsを使用していればアプリのクラッシュをCrashlyticsで検知可能になるので、基本的にはForce Unwrappingを選択すべきだと思います。 しかしこの場合、ユーザ環境でクラッシュする可能性を残してしまうことになります。 AppStoreで配信しているアプリでクラッシュが起きた場合、修正から審査を経てストアに公開されるまで、少なくとも丸一日はかかります。 この際の緊急対応にかかるコストやユーザへの補償を考えると、頭が痛くなりますね。 一方、Optional Binding(やnil結合演算子)を使えばクラッシュを防ぐことはできますが、想定されないパターンに関する回避コードを追加しないといけなくなりますし、ユーザ環境でロジックエラーが発生していることに気付きにくくなります。 そこで今回、Forced Unwrapping、Optional Bindingに次ぐ第3の選択肢として Carefully Unwrapping を提案します。 Carefully Unwrapping 何を実現したいのか 今回Carefully Unwrappingを作成して実現したいのは、 記述が楽 アンラップに失敗した場合にクラッシュさせない アンラップに失敗したことを開発者側が検知できるようにする の3点です。 Forced Unwrappingのメリットである「記述が楽」「アンラップの失敗を検知できる」、Optional Bindingのメリットである「アンラップ失敗時にクラッシュしない」の良いとこ取りになっています。 ソースコード Carefully Unwrappingのソースコードは下記のような感じです。 // エラー情報をCrashlyticsに送信したいため必要 import FirebaseCrashlytics import Foundation public extension Optional { /// デフォルト値つきでアンラップする /// /// - 値が存在する場合はそれを返す /// - 値がnilの場合はデフォルト値を返し、Crashlyticsにエラー情報を送信する func carefullyUnwrapped(defaultValue: Wrapped, originFilePath: String = #file, originFileLine: Int = #line, originMethodName: String = #function) -> Wrapped { guard let _self = self else { // エラー情報をCrashlyticsに送信する let error = CarefullyUnwrapFailure(originFilePath: originFilePath, originFileLine: originFileLine, originMethodName: originMethodName) Crashlytics.crashlytics().record(error: error) return defaultValue } return _self } } public extension Optional where Wrapped: DefaultValuePresentable { /// デフォルト値つきでアンラップする /// /// - 値が存在する場合はそれを返す /// - 値がnilの場合はデフォルト値を返し、Crashlyticsにエラー情報を送信する /// - またデバッグ環境の場合は、気づきやすいようクラッシュさせる func carefullyUnwrapped(originFilePath: String = #file, originFileLine: Int = #line, originMethodName: String = #function) -> Wrapped { carefullyUnwrapped(defaultValue: Wrapped.defaultValue(), originFilePath: originFilePath, originFileLine: originFileLine, originMethodName: originMethodName) } } public protocol DefaultValuePresentable { static func defaultValue() -> Self } /// Optional.carefullyUnwrapped(defaultValue:) に失敗した private struct CarefullyUnwrapFailure: Error { /// originMethodName: 呼び出し元メソッド名 init(originFilePath: String, originFileLine: Int, originMethodName: String) { let originFileName: String = originFilePath.components(separatedBy: "/").last ?? originFilePath _domain = "\(String(describing: Self.self))(\(originFileName):\(originFileLine) >>> \(originMethodName))" } /// エラードメイン設定(_domain:String と _code:Int の組み合わせでCrashlytics上のエラーが分類される) public let _domain: String } // MARK: - 型ごとのデフォルト値設定(ここに設定を追加すれば、carefullyUnwrappedのdefaultValue引数を省略できる) extension Date: DefaultValuePresentable { public static func defaultValue() -> Date { .init() } } extension URL: DefaultValuePresentable { public static func defaultValue() -> URL { // NOTE: 存在しないURLではあるが、クラッシュ回避用としては十分 return .init(string: "https://example.com/image.jpg")! } } extension String: DefaultValuePresentable { public static func defaultValue() -> String { "" } } extension Int: DefaultValuePresentable { public static func defaultValue() -> Int { 0 } } extension Double: DefaultValuePresentable { public static func defaultValue() -> Double { 0 } } extension Data: DefaultValuePresentable { public static func defaultValue() -> Data { .init() } } Optional型に対して、carefullyUnwrapped(defaultValue:) を呼べるようになります。 その際、引数のdefaultValueは、Optional.Wrappedの型がDefaultValuePresentableに適合していれば省略可能です。 またアンラップに失敗してデフォルト値が返ったとき、↓のようにエラーの詳細がCrashlyticsに記録されます。 使い方 Carefully UnwrappingでOptional型をアンラップするコードを見てみましょう。 // (比較用) Forced Unwrapping let url: URL = .init(string: "https://google.com")! // (New!) Carefully Unwrapping let url: URL = .init(string: "https://google.com").carefullyUnwrapped() シンプルな記述で、Forced Unwrappingで発生する可能性のあるクラッシュの発生を抑制することができ、かつエラー情報はFirebaseに通知されるため、ロジックエラーの発生を検知する事ができます。 課題 Carefully Unwrappingを先程の表に追加してみます。 メリット デメリット Forced Unwrapping - 記述が簡略- 万が一nilになった場合、クラッシュが発生するので、Crashlytics等でロジックエラーの発生を検知できる- 意図しない状態でプログラムが進行するのを防ぐことができる - 万が一nilになった場合、ユーザ環境でクラッシュしてしまう Optional Unwrapping - 万が一nilになった場合に、ユーザ環境でのクラッシュを防げる - 基本的に発生しないパターンに対する記述が冗長になる- 万が一nilになった場合、ロジックエラーに気づけない(回避コードの内容による)- 意図しない状態でプログラムが進行してしまう Carefully Unwrapping - 記述が簡略- アンラップに失敗した場合でもクラッシュせず、Crashlytics等でロジックエラーの発生を検知できる - 意図しない状態でプログラムが進行してしまう Forced Unwrappingの問題であったクラッシュの発生を抑制でき、かつシンプルな記述にすることができたので、めでたしめでたし...のように思えますが、Carefully Wrappingにはいくつか課題があります。 課題1. デフォルト値が返された場合に、後続の処理が正常に実行される保証はない アンラップに失敗したとき、Forced Unwrappingのように即時でクラッシュすることは回避できましたが、代わりにデフォルト値が返され、その値で後続の処理が実行されてしまいます。 デフォルト値でちゃんと動作する保障が無い以上、結局後続の処理がうまく動かずクラッシュしてしまう可能性があります。 課題2. protocol DefaultValuePresentable をfinalでない型に対して適合するのが難しい defaultValue引数を省略するためには、型をDefaultValuePresentableに適合させる必要があるのですが、そのためには static func defaultValue() -> Self {} の実装が必要です。 finalでない型でこのメソッドを実装しようとした場合、対象の型のdesignated initializerを使って初期化する必要があります(convenience initializerを使用することはできません)。 型によっては簡単に使えるようなdesignated initializerが提供されていない場合もあるので(CGImage等)、その場合は苦労することになります。 課題3. Carefully Unwrappingを使いたい場面はそもそもそんなに多くない どのようなパターンでnilになるかわからない場合は便利に使えますが、そういう箇所についてはしっかりテストを書くべきだと思います。 またそのようなパターンは実際そこまで多くないような気がしているので、Carefully Unwrappingを定義するよりはOptional Bindingを使用してCrashlyticsへのエラー送信はその場その場で実装したほうがよいような気もしています。 さいごに というわけで結論としては、残念ながらちょっと微妙かも、ということになります。 もしCarefully Unwrappingを採用する場合は、上記の課題を知った上でご利用いただくことをおすすめします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSで位置情報やモーションを使うのときの権限要求あれこれ

仕事でiOSアプリを作っていて、妙なところでハマってしまったので備忘録までに。 位置情報やモーション フィットネスを使うための事前設定 任意のタイミングで許諾を求める処理 アプリを開いたときの権限不足チェック 位置情報やモーション フィットネスを使うための事前設定 アプリを作成したら、info.plistに位置情報とモーションの項目を追加します Targets > Signing&Capabilitiesの左上の「+Capability」からBackgroundModesを選択すると項目が追加されます バックグラウンドでアクセスする処理を追加しておきます これにてアプリ側の初期設定は終了かなと思います 任意のタイミングで許諾を求める処理 例えばウォークスルーの途中など、ユーザとコミュニケーションを撮っている流れの中で許諾を取りたい場面もあるかと思います SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate, CLLocationManagerDelegate { // CLLocationManagerDelegate private let locationManager = CLLocationManager() private let motionManager = CMMotionActivityManager() var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } self.locationManager.requestAlwaysAuthorization() self.locationManager.delegate = self self.motionManager.startActivityUpdates( to: OperationQueue.current!) { (data: CMMotionActivity?) in /* do nothing */ } self.motionManager.stopActivityUpdates() } 〜 略 〜 } ハマったポイントとしては、 位置情報のアクセスを求めるときは、delegateをしないといけない モーションには位置情報のように権限リクエストするメソッドが見当たらず、一瞬start/stopして動かしている アプリを起動すると、位置情報アクセスとモーションフィットネスへのアクセスを要求するダイアログが出るようになりました アプリを開いたときの権限チェック アプリを使っている途中やウォークスルーで許可しそびれたときに、アプリ側から権限チェックをする機能です。アプリを起動して最前面にもっていくごとにチェックするため、SceneDelegateからダイアログを表示しています SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate, CLLocationManagerDelegate { private let locationManager = CLLocationManager() private let motionManager = CMMotionActivityManager() 〜 略 〜 func sceneDidBecomeActive(_ scene: UIScene) { if #available(iOS 14.0, *) { if locationManager.authorizationStatus != .authorizedAlways { showPermitDialog(title: "権限が不足しています", body: "位置情報アクセスを「常に許可」にしてください") } if locationManager.accuracyAuthorization != .fullAccuracy { showPermitDialog(title: "権限が不足しています", body: "正確な位置情報を許可にしてください") } } else { if CLLocationManager.authorizationStatus() != .authorizedAlways { showPermitDialog(title: "権限が不足しています", body: "位置情報アクセスを「常に許可」にしてください") } } if CMMotionActivityManager.authorizationStatus() != .authorized { showPermitDialog(title: "権限が不足しています", body: "モーション&フィットネスを許可にしてください") } if ProcessInfo.processInfo.isLowPowerModeEnabled { showPermitDialog(title: "権限が不足しています", body: "低電力モードを無効にしてください") } } private func showPermitDialog(title: String, body: String) { let dialog = UIAlertController(title: title, message: body, preferredStyle: .alert) dialog.addAction(UIAlertAction(title: "設定を開く", style: .default, handler: { (action: UIAlertAction!) -> Void in if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } })) dialog.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)) self.window?.rootViewController?.present(dialog, animated: true, completion: nil) } 〜 略 〜 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】ファイル操作で保存されたファイルやパスを色んな形式で確認してみる

はじめに ※この記事はLIFULL Advent Calendar 2021の2日目の記事となります。 個人でiOSアプリ開発をしているときに、アプリ内でファイル操作をしたいときがあって、ほしい値を見たい時にどれを参照するのかがごっちゃになるのでまとめました。 前提 今回はDocumentディレクトリに日付のディレクトリを作成し、その中に動画ファイルがあるという状態で考えました。 ※↓の階層はDocumentから始まっていますが、実際にはもっと階層は上にもあります。 Document/ ├── 20211124/ │ ├── hogehoge.mov │ └── fugafuga.mov └── 20211126/ ├── hogehoge.mov └── hugahuga.mov 準備 ディレクトリ作成 今回はDocumentsディレクトリに作成します。 guard let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } // ひとまず日付を直撃ちしてディレクトリ名を作成する // ex.20211124とか let directory = directory.appendingPathComponent("[ディレクトリ名]", isDirectory: true) do { try fileManager.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) } catch { print("失敗した") } ファイル作成 作成したディレクトリにファイルを追加します。 ハードコーディング気味です。 // ディレクトリ名を直撃ちしています let directoryPath = documentDirectoryURL.absoluteString + "20211124" guard let targetDirectory = URL(string: directoryPath) else { return } // ディレクトリのURLにファイル名を追加する // これで ~~~~~/Document/20211124/hogehogeみたいなURLのファイルを作成できる let fileName = "hogehoge" let fileURL = targetDirectory.appendingPathComponent(fileName) // 省略 fileURLを用いてrecordする パスやファイル名の取得 let fileManager = FileManager.default let documentDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! do { // Documentから動画ファイルのURLを取得 // 今回はURLの一つに対しての値をいくつか見る guard let contentUrl = try fileManager.contentsOfDirectory(at: documentDirectoryURL, includingPropertiesForKeys: nil).first else { return } print("contentUrl: \(contentUrl)") print("contentUrlPath: \(contentUrl.path)") print("contentUrlLastPathComponent: \(contentUrl.lastPathComponent)") print("FileName: \(try FileManager.default.contentsOfDirectory(atPath: contentUrl.path))") } catch { print("フォルダが空です。") } これを実行するとコンソールにprint文の結果が以下のように表示されます。 contentUrl: file:///private/var/mobile/Containers/Data/Application/FBAC66B0-F8EF-4689-8379-EE961C62E00B/Documents/20211124/ contentUrlPath: /private/var/mobile/Containers/Data/Application/FBAC66B0-F8EF-4689-8379-EE961C62E00B/Documents/20211124 contentUrlLastPathComponent: 20211124 FileName: ["hogehoge.mov"] 動画の再生をするにはファイルのURLが必要で、最初contentUrl.pathで取れるかと思っていました。 しかし、今回のcontentUrlはDocumentディレクトリの一個下にある階層のコンテンツになるので、20211124ディレクトリまでの情報は取得できますが、そのさらに一個下の階層にあるhogehoge.movの情報はこれだけだと取得できませんでした。 これによって動画の再生ができなくて詰まっていました。 今回は前提としてDocument > [日付のディレクトリ] > [なんらかの動画ファイル]という構成になることは決まっていたので、contentUrlPathとFileNameを取得し、それらを文字列として連結させ、URLに変えてあげる。。をすれば最低限動画の再生には持って行けました。 これは若干力技なので、Documentディレクトリから再帰的にディレクトリじゃなくなるまでファイルを検索しにいくようにするとファイルの構成が異なったりしていても柔軟に動画ファイルのURLを取得できてスマートかなと思ったりしました。 また、地味にcontentUrlとcontentUrl.pathの違いでも詰まりました。 .pathはあくまでファイルパスの文字列を返すだけなので、これを使えば動画再生ができると思いましたが、URLではないので動画をそのまま使いたい場合は間違えずにURL型のcontentUrlの方を使ってあげましょう。 終わりに 今回はSwiftでファイルの作成や取得をする際に、必要な情報をどう取得するべきかを、簡単にコーディングをしながらまとめてみました。 今回得た知見を活かしてファイルの取り扱いをうまくやっていきながらアプリ開発していけるようにしたいですね。 また、ここら辺参考になるよとか、言ってること違うよとかがあればコメントいただけると幸いです。 僕の書いた本文と、コメント欄で一つの作品とさせていただければと思います。 参照 いまさらだけどiOSのファイル操作まとめ(Swift) - Qiita Documentフォルダにファイルを保存する – 野生のプログラマZ Swiftでパスからファイル名や拡張子を取得する方法 ファイルPathとファイルURL - アプリ開発ブログ(仮)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む