はじめに この記事はSwiftのObjectIdentifierについての記事です ObjectIdentifierとは ObjectIdentifierとはクラスまたはメタタイプの一意のidを表す型です。 structやenum, 関数、タプルに対してはObjectIdentifierを生成できなく、classやactorに対してObjectIdentifierを生成できます。 // --- Structに対してはObjectIdentifierを生成できない --- struct S {} let s = S() let num: Int = 0 ObjectIdentifier(s) // Error: Argument type 'S' expected to be an instance of a class or class-constrained type ObjectIdentifier(num) // Error: Argument type 'Int' expected to be an instance of a class or class-constrained type // --- Classに対してはObjectIdentifierを生成できる --- class B { var value: Int init(value: Int) { self.value = value } } actor A {} let b = B(value: 0) let a = A() ObjectIdentifier(b) ObjectIdentifier(a) structに対してObjectIdentifierを生成しようとするとエラーになる ObjectIdentifierによる比較は参照が同じという意味になるので===と同じ意味になる認識です。...
GeoHashを可視化できるアプリを作ってみた
はじめに こんにちは、Swift Advent Calendar 2024の19日目を担当しますfummicc1です。 この記事では、ジオハッシュ(GeoHash)を可視化できるアプリについての話を書きました。 Swift Advent CalendarなのでSwiftに関連する話も書いたつもりですが、Swift以外の話も含まれているのであらかじめご了承いただけると嬉しいです。 この記事に書いてあること ジオハッシュについて ジオハッシュを可視化するアプリを作った話 ジオハッシュを確認できるアプリを作った際のSwiftに関連する話 ジオハッシュとは ジオハッシュは地図上の特定の区域を表現した文字列のことやそのアルゴリズムのことを指します。世界地図に対して「経度を2分割→緯度を2分割」という処理を繰り返すことで生成されるビット列をハッシュ化することで文字列を生成します。 ジオハッシュの例: 区域ごとに一意の文字列が割り当てられる ジオハッシュの生成では、境界を区切って0,1を割り当てる作業を緯度・経度ごとに行います。経度だけに絞ってみると下記のように分割されていきます。 経度を1bitで分割 経度を2bitで分割 経度を3bitで分割 どこまで分割を行うかでビット列の長さが変わり、長ければ長いほど狭い区域を表現するジオハッシュ文字列となります。 ジオハッシュのビット列は、経度と緯度それぞれについてのビット列を交互に並べたものです。 例えば、経度のビット列が「11100」で,緯度のビット列が「10001」の場合、ジオハッシュは「1110100001」となります。 ビット列は5bitごとに32種類の英数字にマッピングされます。数字10個 + アルファベット22個からa,i,l,oを除いたものの合計32種類から構成されます。 ジオハッシュ生成の流れ このジオハッシュから、地点の緯度・経度の区域を特定することができます。 例えば、日本はおおよそ1桁のジオハッシュでは「w」もしくは「x」で表現されます。ジオハッシュの桁が長ければ長いほど、その文字列が示す区域が狭くなるので1桁のジオハッシュだと結構な領域を示しています。詳しい制度についてはこちらに記述がありました。 日本はおおよそwかxに属する 西日本付近でxからwに変わる ジオハッシュの特性を応用することで、「ある地点の近くにある場所」の探索を文字列の比較で実現ができることができます。(ただし、多少の精度の誤差があることに注意が必要です。) ジオハッシュをSwiftで実装したい 勉強も兼ねてGeoHashSwiftというリポジトリに実装しました。ですが、既にGeoHashというSwiftで実装されたライブラリがあったのでそちらも参考に実装をしました。 ジオハッシュを可視化するアプリが欲しかった ジオハッシュのライブラリを作った後に実装が正しいことを確認する必要があったのですが、文字列を見ても正しいジオハッシュかどうかが直感的に分からなかったので入力した緯度経度から生成されるジオハッシュを簡単に確認できるアプリを作りました。 検索機能もつけてみました。(手元で動かすととても重いです) ビット列の長さをスライダーで変えられることができて、マップの中心のジオハッシュと周囲の区域のジオハッシュが表示されるアプリとなっています。 周囲のジオハッシュは幅優先探索で5ステップ先の周囲までを取得しています。全てのジオハッシュを取得すると2 ** ビット列の長さのジオハッシュを取得することになるので、計算が重くなったので断念しました。 5ステップ先のジオハッシュを取得するだけでも、Map上で動作確認をすると処理が重いと感じたので改善に取り組んだことをメモします。 Viewに記述された処理をメインスレッド外で実行する SwiftUIのViewはMainActorであるため、Viewに直接計算メソッドを書いている場合、処理がメインスレッド上で実行されてしまいます。今回はGlobalActorを定義してメソッドをactorで隔離させることでメインスレッドで処理が走らないようにしました。 @globalActor struct ComputationActor { actor ActorType {} static let shared = ActorType() } struct ContentData: Identifiable { var bound: [CLLocationCoordinate2D] = [] var geohash: GeoHash var id: GeoHash { geohash } } struct ContentView: View { // ....
SwiftUIのEmptyViewのonAppearは呼ばれることがある
SwiftUIのEmptyViewはレイアウトに影響しないことで知られています。 EmptyViewはViewが存在しないことを意味するのでonAppearメソッドも呼ばれないと想定されます。 ただ、RootViewにEmptyViewのみを配置した場合はonAppearメソッドは呼ばれるので注意が必要です。 @main struct SampleApp: App { var body: some Scene { WindowGroup { ContentView() // ContentViewだけを表示するとonAppearは呼ばれる // ContentView2() // ContentView2だけを表示するとonAppearは呼ばれない } } } struct ContentView: View { var body: some View { EmptyView() .onAppear { print("onAppear is called") } } } struct ContentView2: View { var body: some View { EmptyView() EmptyView() .onAppear { print("onAppear is not called") } } } ContentViewではView HierarchyにEmptyViewがあって、ContentView2ではView HierarchyにEmptyViewがないのでonAppearは呼ばれないみたいです。 ContentView ContentView2 onAppear is called onAppear is not called おわりに 簡潔な記事ですが、EmptyViewのonAppearはRootViewに単体で配置すると呼ばれるという備忘録でした。
XcodeのRuntime Warningを表示する方法
はじめに pointfreeのswift-issue-reportingというライブラリを眺めていたらXcodeの紫色の警告を出す方法を知ったので記録も兼ねて記事にしました。(参考) Xcodeの紫色の警告は、Runtime Warningというもので、ビルドが成功してアプリが起動した後の実行時に警告が出るというものです。UIの状態をメインスレッドの外で更新した際に出るのはよく見かけると思います。 このRuntime Warningの表示方法は明確にドキュメント化されていないようですが、pointfreeのUnobtrusive Runtime Warnings for Librariesという記事を読んで、表示方法が分かったのでその方法を記事を元に記録します。 なお、記事にもあるようにこの方法はAppleが明示的に示した方法ではないので、AppStoreに提出するアプリでは使用しないようにした方が良いです。 何が嬉しいのか 開発者がRuntimeWarningを出す目的は、ランタイムでアプリの不具合に気付けるようにすることです。他の手法として、ブレークポイントや assertなどがあります。 ユースケースとして、ライブラリ開発者が正しくない使い方での利用が発生したことを利用側に伝えることを考えます。その手法として、ブレークポイントや assertの2つを考えます。 しかし、ブレークポイントはライブラリ提供側ではなく利用側で設定する必要があるというデメリットがあります。また、assertは実際にアプリの不具合に気づけるものの、アプリケーションもクラッシュしてしまうため、開発の妨げになるケースが考えられます。 RuntimeWarningを使うとライブラリ提供側でアプリの不具合を利用側に伝えることができ、アプリケーションもクラッシュしないため、上記のデメリットを解決しつつ、ライブラリの正しくない使い方に即座に気づくことが可能です。 RuntimeWarningの表示方法 結論 os_logを使い、subsystemにcom.apple.runtime-issuesを指定する dsoにはSwiftUIのフレームワークのベースアドレスを指定する 下記のようなクラスを作れば再利用して使えますが、もっと簡単にswift-issue-reportingのreportIssue関数を使うことで実現できます。 actor RuntimeWarning { var info = Dl_info() init() { dladdr( dlsym( dlopen( nil, RTLD_LAZY ), """ $sxSg7SwiftUI8CommandsA2bCRzlAbCP4body4BodyQzvgTW """ ), &info ) } func log(message: StaticString) { os_log( .fault, dso: info.dli_fbase, log: OSLog( subsystem: "com.apple.runtime-issues", category: "ReportRuntimeWarningSample" ), message ) } } 詳細 RuntimeWarningの表示方法は、os_logを使うことで実現できます。subsystemにcom.apple.runtime-issuesを指定することが一つのポイントです。 それに加えて、dso(dynamic shared object)というパラメータを指定することで、RuntimeWarningを表示することができます。 os_log( ....
[SwiftUI] AsyncImage that can perform downsampling
Prologue This article is originated from AsyncImage that can perform downsampling. AsyncImage is a SwiftUI component to show Image fetched from internet resource. It is available since iOS15 like the following. AsyncImage(url: url) { phase in switch phase { case .empty: ProgressView().progressViewStyle(.circular) case .failure: Text("Error") case .success(let image): image .resizable() .frame(width: 56, height: 56) @unknown default: fatalError() } } Such a simple way is beneficial to build a UI layout with ease....