20220112のSwiftに関する記事は8件です。

DispatchGroupを使っていたら、「Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)」が発生

SwiftのDispatchなんちゃらで毎回詰まっていますが、今回もまた詰まったので 記録に残しておきます。 多分記事にしておくくらいの苦労をしないと、また数ヶ月後にまた同じことで詰まる気がとてもしているからです! (って自信持っていうことじゃないが笑) 原因は単純ミスです。 発生していたこと DispatchGroupを使用していたところ、 Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) というエラーが発生・・・ 実際にプロジェクトで詰まったコードに似たサンプルを作りました。 Playgroundで実行できます。 Thread.sleep(forTimeInterval: 0.5)の部分は実際はAPIがリクエストされている箇所でしたが、 今回はsleepに差し替えています。 import UIKit func getItems( itemIds: [String], completionHandler: @escaping ([String]) -> Void ) { var items = [String]() let group = DispatchGroup() for id in itemIds { group.enter() getItemDetail(id) { result in defer { group.leave() } guard let result = result else { return } items.append(result) print("ループの中") } } group.notify(queue: .main) { print("ループ全部終わった\n-------------") completionHandler(items) } } func getItemDetail( _ id: String, completionHandler: @escaping (String?) -> Void ) { Thread.sleep(forTimeInterval: 0.5) print("0.5秒待機中") calcPrice(id) { result in completionHandler(id) } } func calcPrice( _ id: String, completionHandler: @escaping (String?) -> Void ) { if !id.isEmpty { Thread.sleep(forTimeInterval: 0.5) completionHandler(id) } completionHandler(nil) } // ----------------------------------- let itemIds = ["1", "2", "3", "", ""] getItems(itemIds: itemIds) { result in print(result) } どう解決したか、原因は? エラーメッセージからは何が原因かわかりませんでしたが、参考に載せたstackoverflowで、この答えを発見。 You can check number of count entered in group before leaving from any group by below Patch Work また、コメントを見ていたところ、こんな記述も発見。 My API call was sending 2 completion handlers ということではい、私もドンピシャで同じことをしておりました。 修正したのはcalcPriceメソッドです。 returnを忘れた結果、completionHandlerが2回呼ばれてしまっていました。 func calcPrice( _ id: String, completionHandler: @escaping (String?) -> Void ) { if !id.isEmpty { Thread.sleep(forTimeInterval: 0.5) completionHandler(id) return // 追加したコード } completionHandler(nil) } 公式ドキュメントにも似たようなことが書いてありました。 A call to this function must balance a call to enter(). It is invalid to call it more times than enter(), which would result in a negative count. つまりenter()とleave()の呼び出し回数が釣り合ってなかったわけです。 itemIdsの要素が空文字の場合、enter()が1回呼ばれているのに対し、leave()が2回呼ばれていました。 それが原因で例のエラーが起きていました。 コード全体はこちらに上がっています。 おわりに returnを忘れるという単純ミスでしたが、なかなか気づかなかったので 記事として記録しておきました。 Swift自体も勉強中ですが、中でも非同期処理が苦手です。 もっと良いやり方あるよ、ここ違くないなどなどありましたが、ご指摘いただけますと幸いです。 参考 Apple Developer Document leave()→公式ドキュメント Call DispatchGroup() in the right place→stackoverflow Leaving DispatchGroup causes my code to crash→stackoverflow
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode + Swift でiPhone用 地図アプリ。(21日目) 

昨日は、C# WinForms + GMap.NET で地図アプリを作ったので、今日は、swiftでiPhone用の地図アプリ作成に挑戦。 Qiitaに分かりやすい記事を書かれてる方がいたので参考にさせてもらいました。 参考サイト 環境 ・Macbook Air 2020 (Big Sur 11.5) ・Xcode 12.4-> 13.2.1 ・iPhone11 14.6 プロジェクト作成 MapKit.frameworkを追加 [MyGpsMap] ->[Build Phase] ->[Link Binary With Libraries] ->[MapKit.framework] [Map Kit View]をドラッグアンドドロップ [Main storyboard] ->選択 [View] ->[Show Library]   ->[検索窓]    ->[map] [Map Kit View]をドラッグアンドドロップして拡大 実行(エミュレーター) 実行(実機:iPhone11) xcodeで実機が認識しない(参考サイト) ERROR: xcode doesn't support iphone iOS 14.x 参考サイト: ERROR: Signing for "TrashAPP" requires a development team. Select a development team in the Signing & Capabilities editor. ERRORS were encountered while processing 参考サイト:  I know this is just echoing the other answers here, but at the very least, I would recommend just starting with a simple phone restart and seeing if that fixes your problem. Here is the specific screenshot that I encountered, in case yours is the same (pro tip: you can open up the "Devices and Simulators" window by going to Window -> Devices and Simulators shift-command-2). スマホ再起動しても解決せず。 xcodeをアップデート 半日ぐらい時間がかかりました・・・ ERRO:iphone is busy fetching debug symbols for ~ [XcodeのDevice and Simulator] ->[Unpair Device] ケーブルを抜いて再接続 信頼されていないデベロッパーダイアログ [iPhone] ->[設定] ->[一般] ->[デバイス管理] iPhone11で無事動作 ここまでやって無事起動。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] Realmでプロパティがnilのものだけを取得する方法

自分のアプリでプロパティがnilのものだけを取得する必要があったので書き残します。 let realm = try! Realm() let results = realm.objects(hogeModel.self).filter("スキーマ名 = nil") こんな感じにしてあげればnilのものだけ取得できます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラー Application tried to present modally a view controller that is already being presented を対処する。

環境 Xcode 13 Swift 5.5 エラーの原因 現在のコントローラAがコントローラBをポップアップさせたいのに、A.present(B)が呼ばれたときに、既にコントローラBがポップアップされていること。 解決方法 コントローラがポップアップされる前にチェックコードを追加することで解決。 guard self.presentedViewController == nil else { return } guard self.BViewController.isBeingPresented == false else { return } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データセット用の顔画像をすぐに用意したい iPhoneの写真から、顔だけを数秒で100枚切り出す

iPhoneのフォトライブラリから、顔だけを切り出す方法です データセットを用意するのは大変 たとえば機械学習などで、データセットとして顔写真や人体の写真が数千枚必要なケースがあります。 しかし、手動で顔写真を切り出すのは大変です。 手元のiPhoneの写真が使えたら便利 たいていの人のiPhoneには、人の写っている写真が数百〜数千枚はあるでしょう。 それらの写真の顔だけを抽出できれば、迅速なデータセット作成に活かせそうです。 SemanticImageでiPhoneの写真を高速処理できる SemanticImageというライブラリを使えば、高速で写真から顔だけを切り抜けます。 手順 SwiftPackageManagerでSemanticImageをインストール。 let semanticImage = SemanticImage() PhPickerでiPhoneのフォトライブラリから、複数写真を選択します。 Personで検索すると、人の画像だけ切り抜きにかけられて便利です。 var configuration = PHPickerConfiguration() configuration.selectionLimit = 0 // 選択する写真の数を無制限にする configuration.filter = .images let picker = PHPickerViewController(configuration: configuration) picker.delegate = self present(picker, animated: true) 取得した写真をSemanticImageの顔切り抜き機能にかけます。 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) for result in results { // フォトライブラリから取得した写真を順次処理 if result.itemProvider.canLoadObject(ofClass: UIImage.self) { result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in if let image = image as? UIImage, let safeSelf = self { DispatchQueue.main.async { if let faceImage = safeSelf.sematicImage.faceRectangle(uiImage: image) { // 顔を切り抜く UIImageWriteToSavedPhotosAlbum(maskImage, nil, nil, nil) // フォトライブラリに保存 } } } } } } } これで、切り抜かれた顔の画像がフォトライブラリに保存されます。 100枚程度だと、14秒で処理でき、メモリとCPU使用量も安全です(iPhone11)。 (数千枚一気に処理すると、画像データを保持しきれず、メモリがオーバーするので、数百枚ずつ処理するのが良さそうです) 添付画像は全部マスクしてデータとしては意味ない感じですが、著者は照れ屋なので、すみません。 全身データや、犬、猫のデータにも SemanticImageは、全身やペットを検出して切り抜く機能もあるので、さまざまなデータ作成に活かせます。 ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] Proof of CoW

Swift has two kinds of type, Value types and Reference types. Value types will copy the data when assign to another variable. But for array it does not copy every time, it use Copy-on-Write. import Foundation var a = [1,2,3,4,5,6] var b = a func address(_ p: UnsafeRawPointer) { print(p) } address(a) address(b) a.append(6) address(a) address(b) 0x0000000110412210 0x0000000110412210 0x0000000110412980 0x0000000110412210 See the above example, a and b will point to same memory address, var b = a dose not have copy occurred. When you change the content of a, it will copy the data and assign the new data address to a.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]MKLocalSearchCompleterをPOIフィルタリングして改善してみる!

はじめに 唐突ですが、あなたはパン屋を検索できるアプリを作っているとします。 オートコンプリートで簡単に検索できる機能を実装しようとしますが、関係のないお店が出てきてしまうかもしれません。 今回はパン屋のみを検索できるように改善してみようと思います。 参考:WWDC19 What’s New in MapKit and MapKit JS https://developer.apple.com/videos/play/wwdc2019/236/ 開発環境 Xcode 13.0 Swift 5.5 オートコンプリートとは 文字を途中まで入力すると、候補を出してくれる機能のことです。 MapkitのMKLocalSearchCompleterで簡単に実装できます。 POIとは Point Of Interestの略で、学校や動物園などの場所のことを示します。 Mapkitの場合、MKPointOfInterestCategoryのようにカテゴリーが用意されていて、40種類以上あります。 基本の実装 まずは簡単に検索機能を実装してみます。 StoryBoard TextFieldとTableViewのシンプルなものにします。 TableViewCellのCellStyleをsubtitleにし、Identifierはcellとします。 オートコンプリートを実装 import UIKit import MapKit class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var tableView: UITableView! var searchCompleter = MKLocalSearchCompleter() override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self searchCompleter.delegate = self } @IBAction func textEditingChanged(_ sender: Any) { searchCompleter.queryFragment = textField.text ?? "" } } extension ViewController: MKLocalSearchCompleterDelegate { func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { tableView.reloadData() } func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { print("error: \(error)") } } extension ViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {![Something went wrong]() return searchCompleter.results.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { return UITableViewCell() } return cell } } とても簡単に実装できました。 これでTextFieldに入力するとパン屋が検索できるはずです! 試しに僕の好きな『パンのトラ』を検索してみます。 検索はできてますね。 でも、パン屋以外の検索結果も表示されてしまいます。 パン屋を検索するアプリとしてはダメです。 POIフィルタリングする ここからが本題です。 pointOfInterestFilterとResultTypeを使って改善していきます。 let pointOfInterestFilter = MKPointOfInterestFilter(including: [.bakery]) まずはMKPointOfInterestFilterのカテゴリーを設定します。今回はベーカリーです。 override func viewDidLoad() { super.viewDidLoad() searchCompleter.pointOfInterestFilter = pointOfInterestFilter searchCompleter.resultTypes = .pointOfInterest } viewDidLoad()にこの二行を追加します。 searchCompleter.pointOfInterestFilter = pointOfInterestFilter これでsearchComleterにpointOfInterestFilterを適用しています。 searchCompleter.resultTypes = .pointOfInterest こちらは、searchCompleterのresultTypesをPOIに制限しています。 ちなみに、POIに制限しないと... このように、地名なども検索に引っかかってしまいます。 では、実際に動かしてみましょう。 ご覧の通り、パン屋だけが検索結果として返ってきています。 うまくいきました。 これらの検索結果を利用してLocalSearchにかけてあげれば、緯度経度なども取得できるようになります。 また別でその記事を書いてみようかなと思います。 では、良きパンライフを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI ScrollView でスナップ動作を実装

背景と宣伝 諸般の事情でカルーセルやスライドショー的なパーツを SwiftUI で実装する必要があったのですが、思ったより手軽に実装できなかったので、対応内容をまとめてみました。 今回の対応をベースに ScrollView + スナップ動作を簡単に実現するためのライブラリも作成しているので、とりあえずお試しといった場合は、こちらをどうぞ。 ゴール のようなスナップ動作を SwiftUI の ScrollView で実現します。 動作環境については、 iOS 15.0 以上の想定で進めます。 前準備 スナップ動作を実装する前に、横方向にスクロール可能な View 実装を用意します。 前準備 struct CarouselContentView: View { var body: some View { LazyHStack { ForEach(Array(1...100), id: \.self) { index in ZStack { Color(hue: Double(index * 11 % 100) / 100, saturation: 0.5, brightness: 1.0) Text("\(index)") } .frame(width: 280, height: 200) } } } } struct ContentView: View { var body: some View { ScrollView(.horizontal, showsIndicators: false) { CarouselContentView() } } } 表示するアイテムは固定サイズで 100 個 並び順に依存する背景色とテキスト表示 HSL で背景色の色味だけ揃える といった実装で、動きとしては以下のような状態です。 前準備段階での動作 これをベースとしてスナップ動作の実装を進めます。 対応方針 スナップ動作を実現するにあたって、必要な要素を以下のように分割、それぞれ対応していくことにしました。 スナップ開始タイミングの検知 スナップ先座標の取得 スナップ先座標へのスクロール (スナップ動作) 要求されるスナップ動作によって、それぞれの指定が微妙に異なるのですが、今回は以下の条件で対応します。 (冒頭の gif 画像通りの動作です) 指を離した瞬間 (ドラッグ終了) のタイミングでスナップ開始 Center-to-Center で ScrollView の中央位置に最も近いアイテムをスナップ先に指定 対応内容 1. スナップ開始タイミングの検知 いきなり大問題なのですが、 現在 (Xcode 13 / iOS 15) の SwiftUI では UIScrollViewDelegate 相当の処理が I/F として提供されていない ScrollView + DragGesture の併用ができない といった状況のため、 指を離した瞬間 (ドラッグ終了) のタイミングでスナップ開始 の実現にハードルがあります。 非常に悩んだのですが、 Introspect for SwiftUI を導入し、 ScrollView が背後で利用している UIScrollView のインスタンスを強引に取得して、 UIScrollViewDelegate を利用してドラッグ終了を検知することとしました。 具体的な対応手順としては、 UIScrollViewDelegate に適合した NSObject な class を用意 SwiftUI View 側で 1. の class のインスタンスを保持 UIScrollViewDelegate を利用してドラッグ終了を検知した場合、クロージャ経由で View 側に伝達 といった流れで、対応コードと動作確認は以下のような感じとなります。 対応コード UIScrollViewDelegate の実装 class DragDetector: NSObject, UIScrollViewDelegate { var didEndScroll: (() -> Void)? func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { // decelerate の場合は、 `scrollViewDidEndDecelerating(_:)` に処理を任せる didEndScroll?() } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { didEndScroll?() } } 動作確認 struct ContentView: View { @State private var didEndScroll: Bool = false // 動作確認用 private let dragDetector = DragDetector() var body: some View { VStack { ScrollView(.horizontal, showsIndicators: false) { CarouselContentView() } // `Introspect for SwiftUI` で UIScrollView のインスタンスを取得 .introspectScrollView { scrollView in scrollView.delegate = dragDetector } // 動作確認用 if didEndScroll { Text("ScrollView didEndScroll") } else { Text(" ") } } .onAppear { dragDetector.didEndScroll = { self.didEndScroll = true } } } } 動作確認 2. スナップ先座標の取得 Center-to-Center で ScrollView の中央位置に最も近いアイテムをスナップ先に指定 を実現するために、 ScrollView 自体の x 軸中央座標と、 ScrollView (+ LazyHStack) 中で管理している子 View の x 軸中央座標を算出。 各子 View と ScrollView の間で距離を求めて、最も距離が短くなる子 View を取得します。 ScrollView の座標については、 GeometryReader を利用してそのまま算出できるのですが、子 View については、 LazyHStack で管理しており、 100 個すべてが必ず存在するわけでもないため、 PreferenceKey + anchorPreference の仕組みを利用して、座標を取得更新していきます。 参考: 対応手順としては、 子 View 識別用のインデックス値 (id) と座標を格納する PreferenceKey を定義 子 View に anchorPreference の ViewModifier を追加して、 親 View に PreferenceKey を伝達 親 View 側のプロパティで ScrollView と PreferenceKey 経由で取得した子 View の座標を保持 特定タイミングでプロパティで保持している座標を元に、スナップ先の子 View を算出 といった流れで、とりあえずは 4.特定タイミングでプロパティで保持している座標を元に、スナップ先の子 View を算出 を暫定的なボタンタップとすることで、動作確認を実施しました。 対応コードと動作確認結果は次のようになります。 対応コード PreferenceKey の用意 class SnapAnchorPreferenceKey: PreferenceKey { // Dictionary で子 View の インデックス値 (id) と 座標のペアを保持 static var defaultValue: [Int: Anchor<CGPoint>] = [:] static func reduce( value: inout [Int: Anchor<CGPoint>], nextValue: () -> [Int: Anchor<CGPoint>] ) { for (index, anchor) in nextValue() { value[index] = anchor } } } 子 View 側の対応 struct CarouselContentView: View { var body: some View { LazyHStack { ForEach(Array(1...100), id: \.self) { index in ZStack { Color(hue: Double(index * 11 % 100) / 100, saturation: 0.5, brightness: 1.0) Text("\(index)") } .frame(width: 280, height: 200) // 各表示アイテムの中心位置を PreferenceKey で親 View に伝達 .anchorPreference( key: SnapAnchorPreferenceKey.self, value: .center ) { [index: $0] } } } } } 動作確認 struct ContentView: View { @State private var scrollViewAnchor: CGFloat? @State private var anchors: [Int: CGFloat] = [:] @State private var snappingIndex: Int? var body: some View { VStack { GeometryReader { proxy in ScrollView(.horizontal, showsIndicators: false) { CarouselContentView() } // 子 View の座標変化に合わせてプロパティに座標を保存 .onPreferenceChange(SnapAnchorPreferenceKey.self) { preference in anchors = preference.mapValues { proxy[$0].x } } .onAppear { scrollViewAnchor = proxy.size.width / 2 } } // 動作確認用 HStack { Spacer() Button("Check") { guard let scrollViewAnchor = scrollViewAnchor else { return } // 最短距離の子 View を探索 snappingIndex = anchors.min { leftAnchor, rightAnchor in let leftDistance = abs(scrollViewAnchor - leftAnchor.value) let rightDistance = abs(scrollViewAnchor - rightAnchor.value) return leftDistance < rightDistance }?.key } if let snappingIndex = snappingIndex { Text("\(snappingIndex)") } else { Text(" ") } Spacer() } Spacer() } } } 動作確認 この対応において、 iOS 15.0 未満では Anchor<Value> が Equatable に準拠していない 問題から、 iOS 15.0 以上の Deployment Target が要求されてしまいます。 iOS 15.0 未満での動作も考慮する場合は、 Anchor<Value> ではなく直接 CGPoint を保持するような実装が必要となります。 3. スナップ先座標へのスクロール (スナップ動作) スクロール動作については、 iOS 14 から実装された ScrollViewReader と ScrollViewProxy によって、シンプルに実装可能です。 対応としては、 子 View ごとにユニークとなるような id(_:) を設定 ScrollViewReader + ScrollViewProxy で対象の子 View までスクロール という形となります。 対応コード 動作確認 struct CarouselContentView: View { var body: some View { LazyHStack { ForEach(Array(1...100), id: \.self) { index in ZStack { Color(hue: Double(index * 11 % 100) / 100, saturation: 0.5, brightness: 1.0) Text("\(index)") } .frame(width: 280, height: 200) // スナップ動作のスクロールで利用するためにユニークな id を付与 .id(index) } } } } struct ContentView: View { @State private var scrollTo: String = "" var body: some View { ScrollViewReader { proxy in VStack { ScrollView(.horizontal, showsIndicators: false) { CarouselContentView() } // 動作確認用 HStack { Spacer() TextField("Scroll to", text: $scrollTo) .frame(width: 100) Button("Scroll") { guard let index = Int(scrollTo) else { return } // アニメーション付きでスクロール withAnimation { proxy.scrollTo(index, anchor: .center) } } Spacer() } Spacer() } } } } 動作確認 まとめ これまでの 3 つの対応要素をまとめることによって、本記事冒頭の gif 画像動作が実装できます。 まとめた際の最終形のコードは以下の通りです。 最終形 // ドラッグ終了検知用の UIScrollViewDelegate に適合した NSObject class DragDetector: NSObject, UIScrollViewDelegate { var didEndScroll: (() -> Void)? func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { // decelerate の場合は、 `scrollViewDidEndDecelerating(_:)` に処理を任せる didEndScroll?() } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { didEndScroll?() } } // 子 View の座標伝達で利用する PreferenceKey class SnapAnchorPreferenceKey: PreferenceKey { // Dictionary で子 View の インデックス値 (id) と 座標のペアを保持 static var defaultValue: [Int: Anchor<CGPoint>] = [:] static func reduce( value: inout [Int: Anchor<CGPoint>], nextValue: () -> [Int: Anchor<CGPoint>] ) { for (index, anchor) in nextValue() { value[index] = anchor } } } struct CarouselContentView: View { var body: some View { LazyHStack { ForEach(Array(1...100), id: \.self) { index in ZStack { Color(hue: Double(index * 11 % 100) / 100, saturation: 0.5, brightness: 1.0) Text("\(index)") } .frame(width: 280, height: 200) // スナップ動作のスクロールで利用するためにユニークな id を付与 .id(index) // 各表示アイテムの中心位置を PreferenceKey で親 View に伝達 .anchorPreference( key: SnapAnchorPreferenceKey.self, value: .center ) { [index: $0] } } } } } struct ContentView: View { private let dragDetector = DragDetector() @State private var anchors: [Int: CGFloat] = [:] var body: some View { ScrollViewReader { scrollViewProxy in GeometryReader { geometryProxy in ScrollView(.horizontal, showsIndicators: false) { CarouselContentView() } // `Introspect for SwiftUI` で UIScrollView のインスタンスを取得 .introspectScrollView { scrollView in scrollView.delegate = dragDetector } // 子 View の座標変化に合わせてプロパティに座標を保存 .onPreferenceChange(SnapAnchorPreferenceKey.self) { preference in anchors = preference.mapValues { geometryProxy[$0].x } } .onAppear { dragDetector.didEndScroll = { let scrollViewAnchor = geometryProxy.size.width / 2 // 最短距離の子 View を探索 let snappingIndex = anchors.min { leftAnchor, rightAnchor in let leftDistance = abs(scrollViewAnchor - leftAnchor.value) let rightDistance = abs(scrollViewAnchor - rightAnchor.value) return leftDistance < rightDistance }?.key // アニメーション付きでスクロール withAnimation { scrollViewProxy.scrollTo(snappingIndex, anchor: .center) } } } } } } } その他対応方法との比較、参考記事 今回は ScrollView を生かしたままスナップ動作を実現しましたが、類似の実装を検索すると ScrollView を利用せずに、独自でスクロール + スナップ動作を実装 スナップ開始タイミングを ScrollView の offset 変化で検知 といったパターンでの実装もありました。 それぞれの対応でのメリット / デメリットは以下のようになると思われます。 対応方法 メリット デメリット 今回の対応(ドラッグ終了検知に UIScrollView を利用) ScrollView の動作を生かせる 背後に UIScrollView があるという暗黙の実装に依存 独自でスクロール + スナップ動作実装 DragGesture によって、 SwiftUI ベースでのドラッグ終了検知が可能 ScrollView と独自スクロールの間で動作差異が生まれる ScrollView の offset 変化検知 ScrollView の動作を生かしつつ、SwiftUI ベースでのドラッグ終了検知が可能 ドラッグしたまま静止された場合に留まれない 独自でスクロール + スナップ動作を実装 スナップ開始タイミングを ScrollView の offset 変化で検知
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む