├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE.md ├── Package.swift ├── README.md └── Sources └── MapItemPicker ├── MapItemPickerSheet.swift ├── MapItemPickerViewController.swift ├── SearchCompletionsTableViewController.swift ├── SearchResponseTableViewController.swift └── View+mapItemPicker.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lorenzo Fiamingo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swiftui-map-item-picker", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .tvOS(.v13), 11 | .watchOS(.v6) 12 | ], 13 | products: [ 14 | .library( 15 | name: "MapItemPicker", 16 | targets: ["MapItemPicker"]) 17 | ], 18 | targets: [ 19 | .target(name: "MapItemPicker") 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI MapItemPicker 🗺️ 2 | 3 | `MapItemPicker` is a location picker sheet. Currently supports only iOS and Mac Catalyst. 4 | 5 | 6 | 7 | ## Usage 8 | 9 | `MapItemPicker` has similar same API and behavior as other [Presentation Modifiers](https://developer.apple.com/documentation/swiftui/view-presentation). 10 | ```swift 11 | import SwiftUI 12 | import MapItemPicker 13 | 14 | struct ContentView: View { 15 | 16 | @State private var showingPicker = false 17 | 18 | var body: some View { 19 | Button("Choose location") { 20 | showingPicker = true 21 | } 22 | .mapItemPicker(isPresented: $showingPicker) { item in 23 | if let name = item?.name { 24 | print("Selected \(name)") 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ## Installation 32 | 33 | 1. In Xcode, open your project and navigate to **File** → **Add Packages...** 34 | 2. Paste the repository URL (`https://github.com/lorenzofiamingo/swiftui-map-item-picker`) and click **Next**. 35 | 3. Click **Finish**. 36 | 4. Add the `NSLocationWhenInUseUsageDescription` key to your app's Info.plist 37 | 38 | ## Other projects 39 | 40 | [SwiftUI VariadicViews 🥞](https://github.com/lorenzofiamingo/swiftui-variadic-views) 41 | 42 | [SwiftUI AsyncButton 🖲️](https://github.com/lorenzofiamingo/swiftui-async-button) 43 | 44 | [SwiftUI PhotosPicker 🌇](https://github.com/lorenzofiamingo/swiftui-photos-picker) 45 | 46 | [SwiftUI CachedAsyncImage 🗃️](https://github.com/lorenzofiamingo/swiftui-cached-async-image) 47 | 48 | [SwiftUI VerticalTabView 🔝](https://github.com/lorenzofiamingo/swiftui-vertical-tab-view) 49 | 50 | [SwiftUI SharedObject 🍱](https://github.com/lorenzofiamingo/swiftui-shared-object) 51 | -------------------------------------------------------------------------------- /Sources/MapItemPicker/MapItemPickerSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapItemPickerSheet.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 22/02/22. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import SwiftUI 11 | import MapKit 12 | 13 | @available(iOS 15.0, *) 14 | struct MapItemPickerSheet: View { 15 | 16 | @Binding var isPresented: Bool 17 | 18 | @State private var pickerViewController: MapItemPickerViewController 19 | 20 | private var content: Content 21 | 22 | private var onDismiss: ((MKMapItem?) -> Void)? 23 | 24 | init(isPresented: Binding, onDismiss: ((MKMapItem?) -> Void)?, content: Content) { 25 | let pickerViewController = MapItemPickerViewController() 26 | self.onDismiss = onDismiss 27 | self._pickerViewController = State(wrappedValue: pickerViewController) 28 | self._isPresented = isPresented 29 | self.content = content 30 | setupPickerViewController() 31 | } 32 | 33 | private func setupPickerViewController() { 34 | pickerViewController.onDismiss = { mapItem in 35 | onDismiss?(mapItem) 36 | isPresented = false 37 | } 38 | } 39 | 40 | var body: some View { 41 | content 42 | .onChange(of: pickerViewController.isBeingPresented) { presenting in 43 | isPresented = presenting 44 | } 45 | .onChange(of: isPresented) { presenting in 46 | if presenting && !pickerViewController.isBeingPresented { 47 | #if targetEnvironment(macCatalyst) 48 | UIApplication.shared 49 | .topmostViewController? 50 | .present(pickerViewController.searchNavigationController, animated: true) 51 | #else 52 | UIApplication.shared 53 | .topmostViewController? 54 | .present(pickerViewController, animated: true) 55 | pickerViewController 56 | .present(pickerViewController.searchNavigationController, animated: true) 57 | #endif 58 | } else { 59 | #if targetEnvironment(macCatalyst) 60 | pickerViewController.searchNavigationController 61 | .presentingViewController? 62 | .dismiss(animated: true) 63 | #else 64 | pickerViewController 65 | .presentingViewController? 66 | .dismiss(animated: true) 67 | #endif 68 | pickerViewController = MapItemPickerViewController() 69 | setupPickerViewController() 70 | } 71 | } 72 | } 73 | } 74 | 75 | private extension UIApplication { 76 | var topmostViewController: UIViewController? { 77 | var viewController = connectedScenes 78 | .filter { $0.activationState == .foregroundActive } 79 | .compactMap({$0 as? UIWindowScene}) 80 | .first? 81 | .windows 82 | .filter { $0.isKeyWindow } 83 | .first? 84 | .rootViewController 85 | while let presentedViewController = viewController?.presentedViewController { 86 | viewController = presentedViewController 87 | } 88 | return viewController 89 | } 90 | } 91 | 92 | #endif 93 | 94 | -------------------------------------------------------------------------------- /Sources/MapItemPicker/MapItemPickerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapItemPickerViewController.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 16/02/22. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import SwiftUI 11 | import MapKit 12 | import CoreLocation 13 | 14 | @available(iOS 15.0, *) 15 | class MapItemPickerViewController: 16 | UIViewController, 17 | UISheetPresentationControllerDelegate, 18 | UIAdaptivePresentationControllerDelegate, 19 | MKMapViewDelegate, 20 | UISearchBarDelegate, 21 | CLLocationManagerDelegate 22 | { 23 | 24 | lazy private var locationManager: CLLocationManager = { 25 | let locationManger = CLLocationManager() 26 | locationManger.delegate = self 27 | return locationManger 28 | }() 29 | 30 | lazy var searchNavigationController: UINavigationController = { 31 | let searchNavigationController = UINavigationController(rootViewController: searchResponseTableViewController) 32 | searchNavigationController.modalPresentationStyle = .pageSheet 33 | searchNavigationController.presentationController?.delegate = self 34 | if let sheet = searchNavigationController.sheetPresentationController { 35 | #if !targetEnvironment(macCatalyst) 36 | sheet.prefersGrabberVisible = true 37 | #endif 38 | sheet.delegate = self 39 | sheet.detents = [.medium(), .large()] 40 | sheet.largestUndimmedDetentIdentifier = .medium 41 | sheet.prefersScrollingExpandsWhenScrolledToEdge = false 42 | } 43 | searchNavigationController.isModalInPresentation = true 44 | return searchNavigationController 45 | }() 46 | 47 | var onDismiss: ((MKMapItem?) -> Void)? 48 | 49 | private lazy var searchCompletionsTableViewController: SearchCompletionsTableViewController = { 50 | let tableViewController = SearchCompletionsTableViewController(style: .plain) 51 | tableViewController.searchRegion = searchRegion 52 | tableViewController.onCompletionSelection = { completion in 53 | self.searchResponseTableViewController.tableView.isHidden = false 54 | self.searchController.isActive = false 55 | self.searchController.searchBar.text = nil //[completion.title, completion.subtitle].joined(separator: ", ") 56 | 57 | if let popover = self.searchNavigationController.popoverPresentationController { 58 | let sheet = popover.adaptiveSheetPresentationController 59 | sheet.animateChanges { 60 | sheet.selectedDetentIdentifier = .medium 61 | } 62 | } 63 | 64 | let searchRequest = MKLocalSearch.Request(completion: completion) 65 | searchRequest.region = self.searchRegion 66 | let search = MKLocalSearch(request: searchRequest) 67 | search.start { (response, error) in 68 | self.searchResponse = response 69 | } 70 | } 71 | 72 | tableViewController.tableView.backgroundColor = .clear 73 | let blurEffect = UIBlurEffect(style: .systemThickMaterial) 74 | let blurEffectView = UIVisualEffectView(effect: blurEffect) 75 | tableViewController.tableView.backgroundView = blurEffectView 76 | tableViewController.tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect) 77 | 78 | return tableViewController 79 | }() 80 | 81 | private lazy var searchResponseTableViewController: SearchResponseTableViewController = { 82 | let tableViewController = SearchResponseTableViewController(style: .insetGrouped) 83 | tableViewController.onMapItemSelection = { mapItemIndex in 84 | self.selectedAnnotationIndex = mapItemIndex 85 | let annotation = self.annotations[mapItemIndex] 86 | if !self.mapView.annotations(in: self.mapView.visibleMapRect).contains(annotation as! AnyHashable) { 87 | self.mapView.showAnnotations([annotation], animated: true) 88 | } 89 | } 90 | 91 | tableViewController.navigationItem.searchController = searchController 92 | tableViewController.navigationItem.leftBarButtonItem = cancelButton 93 | tableViewController.navigationItem.rightBarButtonItem = selectionButton 94 | tableViewController.navigationItem.hidesSearchBarWhenScrolling = false 95 | 96 | tableViewController.tableView.backgroundColor = .clear 97 | let blurEffect = UIBlurEffect(style: .systemThickMaterial) 98 | let blurEffectView = UIVisualEffectView(effect: blurEffect) 99 | tableViewController.tableView.backgroundView = blurEffectView 100 | tableViewController.tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect) 101 | 102 | tableViewController.onViewWillLayoutSubviews = { 103 | self.mapView.frame = self.view.bounds 104 | let bottomMargin: CGFloat 105 | if UIDevice.current.userInterfaceIdiom == .pad { 106 | bottomMargin = 1.115*self.mapView.bounds.height/2 107 | } else { 108 | bottomMargin = 1.075*self.mapView.bounds.height/2 109 | } 110 | self.mapView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: bottomMargin, right: 0) 111 | self.view.layoutIfNeeded() 112 | } 113 | 114 | return tableViewController 115 | }() 116 | 117 | private lazy var cancelButton: UIBarButtonItem = { 118 | let cancelAction = UIAction { _ in 119 | self.onDismiss?(nil) 120 | } 121 | return UIBarButtonItem(systemItem: .cancel, primaryAction: cancelAction, menu: nil) 122 | }() 123 | 124 | private lazy var selectionButton: UIBarButtonItem = { 125 | let selectionAction = UIAction { _ in 126 | if 127 | let index = self.selectedAnnotationIndex, 128 | let response = self.searchResponse?.mapItems[index] 129 | { 130 | self.onDismiss?(response) 131 | } 132 | } 133 | let button = UIBarButtonItem(systemItem: .done, primaryAction: selectionAction, menu: nil) 134 | button.isEnabled = selectedAnnotationIndex != nil 135 | return button 136 | }() 137 | 138 | private var searchResponse: MKLocalSearch.Response? { 139 | didSet { 140 | searchResponseTableViewController.searchResponse = searchResponse 141 | annotations = searchResponse?.mapItems.map(\.placemark) ?? [] 142 | if annotations.count > 0 { 143 | selectedAnnotationIndex = 0 144 | } 145 | } 146 | } 147 | 148 | var searchRegion: MKCoordinateRegion = MKCoordinateRegion(.world) { 149 | didSet { 150 | searchCompletionsTableViewController.searchRegion = searchRegion 151 | } 152 | } 153 | 154 | private var annotations: [MKAnnotation] = [] { 155 | didSet { 156 | mapView.removeAnnotations(mapView.annotations) 157 | mapView.addAnnotations(annotations) 158 | mapView.showAnnotations(annotations, animated: true) 159 | } 160 | } 161 | 162 | private var selectedAnnotationIndex: Int? { 163 | didSet { 164 | mapView.deselectAnnotation(nil, animated: true) 165 | if let annotationIndex = selectedAnnotationIndex { 166 | mapView.selectAnnotation(annotations[annotationIndex], animated: true) 167 | selectionButton.isEnabled = true 168 | } else { 169 | selectionButton.isEnabled = false 170 | } 171 | } 172 | } 173 | 174 | private lazy var searchController: UISearchController = { 175 | let searchController = UISearchController(searchResultsController: searchCompletionsTableViewController) 176 | searchController.searchResultsUpdater = searchCompletionsTableViewController 177 | searchController.obscuresBackgroundDuringPresentation = false 178 | searchController.searchBar.delegate = self 179 | searchController.searchBar.placeholder = "Location" 180 | searchController.hidesNavigationBarDuringPresentation = false 181 | return searchController 182 | }() 183 | 184 | private var foregroundRestorationObserver: NSObjectProtocol? 185 | 186 | override func viewDidLoad() { 187 | super.viewDidLoad() 188 | view.backgroundColor = .systemGroupedBackground 189 | } 190 | 191 | override func viewDidAppear(_ animated: Bool) { 192 | super.viewDidAppear(animated) 193 | let name = UIApplication.willEnterForegroundNotification 194 | foregroundRestorationObserver = NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: { [unowned self] (_) in 195 | self.locationManager.requestWhenInUseAuthorization() 196 | }) 197 | locationManager.requestWhenInUseAuthorization() 198 | } 199 | 200 | private lazy var mapView: MKMapView = { 201 | let mapView = MKMapView() 202 | mapView.delegate = self 203 | mapView.showsUserLocation = true 204 | view.addSubview(mapView) 205 | return mapView 206 | }() 207 | 208 | // MARK: UISheetPresentationControllerDelegate 209 | 210 | func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { 211 | } 212 | 213 | // MARK: UIAdaptivePresentationControllerDelegate 214 | 215 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 216 | false 217 | } 218 | 219 | // MARK: UISearchBarDelegate 220 | 221 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 222 | searchResponseTableViewController.tableView.isHidden = false 223 | guard let text = searchBar.text else { return } 224 | self.searchController.isActive = false 225 | self.searchController.searchBar.text = text 226 | let searchRequest = MKLocalSearch.Request() 227 | searchRequest.region = searchRegion 228 | searchRequest.naturalLanguageQuery = text 229 | let search = MKLocalSearch(request: searchRequest) 230 | search.start { (response, error) in 231 | self.searchResponse = response 232 | } 233 | } 234 | 235 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 236 | searchResponseTableViewController.tableView.isHidden = false 237 | } 238 | 239 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 240 | if searchText.count > 0 { 241 | searchResponseTableViewController.tableView.isHidden = true 242 | } else { 243 | searchResponseTableViewController.tableView.isHidden = false 244 | } 245 | } 246 | 247 | // MARK: MKMapViewDelegate 248 | 249 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 250 | searchRegion = mapView.region 251 | } 252 | 253 | func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { 254 | let annotationIndex = annotations.firstIndex { annotation in 255 | annotation === view.annotation 256 | } 257 | if let annotationIndex = annotationIndex { 258 | searchResponseTableViewController.tableView.selectRow(at: IndexPath(row: annotationIndex, section: 0), animated: true, scrollPosition: .middle) 259 | } 260 | } 261 | 262 | func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { 263 | let annotationIndex = annotations.firstIndex { annotation in 264 | annotation === view.annotation 265 | } 266 | if let annotationIndex = annotationIndex { 267 | searchResponseTableViewController.tableView.deselectRow(at: IndexPath(row: annotationIndex, section: 0), animated: true) 268 | } 269 | } 270 | 271 | private var initalUserLocation: MKUserLocation? { 272 | didSet { 273 | if let coordinate = initalUserLocation?.coordinate { 274 | let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 12_000, longitudinalMeters: 12_000) 275 | mapView.setRegion(region, animated: true) 276 | } 277 | } 278 | } 279 | func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { 280 | if initalUserLocation == nil { 281 | initalUserLocation = userLocation 282 | } 283 | } 284 | 285 | // MARK: CLLocationManagerDelegate 286 | 287 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 288 | } 289 | 290 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 291 | if let error = error as? CLError, error.code == .denied { 292 | // Location updates are not authorized. 293 | manager.stopUpdatingLocation() 294 | return 295 | } 296 | } 297 | } 298 | 299 | #endif 300 | -------------------------------------------------------------------------------- /Sources/MapItemPicker/SearchCompletionsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCompletionsTableViewController.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 20/02/22. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | import MapKit 12 | 13 | class SearchCompletionsTableViewController: UITableViewController { 14 | 15 | static private let cellReuseID = "CellReuseID" 16 | 17 | private var searchCompleter: MKLocalSearchCompleter? 18 | var searchRegion: MKCoordinateRegion = MKCoordinateRegion(.world) { 19 | didSet { 20 | searchCompleter?.region = searchRegion 21 | } 22 | } 23 | 24 | var onCompletionSelection: ((MKLocalSearchCompletion) -> Void)? 25 | 26 | var onAppear: (() -> Void)? 27 | var onDisappear: (() -> Void)? 28 | 29 | var searchCompletions: [MKLocalSearchCompletion]? { 30 | didSet { 31 | tableView.reloadData() 32 | } 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: SearchCompletionsTableViewController.cellReuseID) 38 | } 39 | 40 | override func viewWillAppear(_ animated: Bool) { 41 | super.viewWillAppear(animated) 42 | onAppear?() 43 | startProvidingCompletions() 44 | } 45 | 46 | override func viewDidDisappear(_ animated: Bool) { 47 | super.viewDidDisappear(animated) 48 | onDisappear?() 49 | stopProvidingCompletions() 50 | } 51 | 52 | private func startProvidingCompletions() { 53 | searchCompleter = MKLocalSearchCompleter() 54 | searchCompleter?.delegate = self 55 | searchCompleter?.region = searchRegion 56 | } 57 | 58 | private func stopProvidingCompletions() { 59 | searchCompleter = nil 60 | } 61 | } 62 | 63 | extension SearchCompletionsTableViewController { 64 | 65 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | searchCompletions?.count ?? 0 67 | } 68 | 69 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 70 | "Suggestions" 71 | } 72 | 73 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | if #available(iOS 14.0, *) { 75 | let cell = tableView.dequeueReusableCell(withIdentifier: SearchCompletionsTableViewController.cellReuseID, for: indexPath) 76 | guard let completion = searchCompletions?[indexPath.item] else { return cell } 77 | var content = cell.defaultContentConfiguration() 78 | let text = NSMutableAttributedString(string: completion.title) 79 | for value in completion.titleHighlightRanges { 80 | text.setAttributes([.font: content.textProperties.font.bold()], range: value.rangeValue) 81 | } 82 | content.attributedText = text 83 | let secondaryText = NSMutableAttributedString(string: completion.subtitle) 84 | for value in completion.subtitleHighlightRanges { 85 | secondaryText.setAttributes([.font: content.secondaryTextProperties.font.bold()], range: value.rangeValue) 86 | } 87 | content.secondaryAttributedText = secondaryText 88 | cell.contentConfiguration = content 89 | cell.backgroundColor = .clear 90 | return cell 91 | } else { 92 | fatalError() 93 | } 94 | } 95 | 96 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 97 | guard let completion = searchCompletions?[indexPath.item] else { return } 98 | onCompletionSelection?(completion) 99 | } 100 | } 101 | 102 | extension SearchCompletionsTableViewController: MKLocalSearchCompleterDelegate { 103 | 104 | func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { 105 | searchCompletions = completer.results 106 | } 107 | 108 | func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { 109 | if let error = error as NSError? { 110 | print("MKLocalSearchCompleter encountered an error: \(error.localizedDescription). The query fragment is: \"\(completer.queryFragment)\"") 111 | } 112 | } 113 | } 114 | 115 | extension SearchCompletionsTableViewController: UISearchResultsUpdating { 116 | 117 | func updateSearchResults(for searchController: UISearchController) { 118 | if let text = searchController.searchBar.text, text.count > 0 { 119 | searchCompleter?.queryFragment = text 120 | } else { 121 | searchCompleter?.cancel() 122 | searchCompletions = nil 123 | } 124 | } 125 | } 126 | 127 | 128 | private extension UIFont { 129 | 130 | func withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont { 131 | 132 | guard let fd = fontDescriptor.withSymbolicTraits(traits) else { 133 | return self 134 | } 135 | 136 | return UIFont(descriptor: fd, size: pointSize) 137 | } 138 | 139 | func bold() -> UIFont { 140 | withTraits(.traitBold) 141 | } 142 | } 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /Sources/MapItemPicker/SearchResponseTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResponseTableViewController.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 21/02/22. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | import MapKit 12 | 13 | class SearchResponseTableViewController: UITableViewController { 14 | 15 | static private let cellReuseID = "CellReuseID" 16 | 17 | private var currentPlacemark: CLPlacemark? 18 | 19 | var onMapItemSelection: ((Int) -> Void)? 20 | 21 | var onViewWillLayoutSubviews: (() -> Void)? 22 | 23 | var searchResponse: MKLocalSearch.Response? { 24 | didSet { 25 | tableView.reloadData() 26 | } 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: SearchResponseTableViewController.cellReuseID) 32 | } 33 | 34 | override func viewWillLayoutSubviews() { 35 | super.viewWillLayoutSubviews() 36 | onViewWillLayoutSubviews?() 37 | } 38 | } 39 | 40 | extension SearchResponseTableViewController { 41 | 42 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 43 | return searchResponse?.mapItems.count ?? 0 44 | } 45 | 46 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 47 | if let city = currentPlacemark?.locality { 48 | return "Results near \(city)" 49 | } else { 50 | return "Results" 51 | } 52 | } 53 | 54 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 | if #available(iOS 14.0, *) { 56 | let cell = tableView.dequeueReusableCell(withIdentifier: SearchResponseTableViewController.cellReuseID, for: indexPath) 57 | if let response = searchResponse { 58 | let mapItem = response.mapItems[indexPath.item] 59 | var content = cell.defaultContentConfiguration() 60 | content.text = mapItem.placemark.name 61 | content.secondaryText = mapItem.placemark.title 62 | cell.contentConfiguration = content 63 | } 64 | return cell 65 | } else { 66 | fatalError() 67 | } 68 | } 69 | 70 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 71 | onMapItemSelection?(indexPath.item) 72 | } 73 | } 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /Sources/MapItemPicker/View+mapItemPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+mapItemPickerSheet.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 25/02/22. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import SwiftUI 11 | import MapKit 12 | 13 | @available(iOS 15.0, *) 14 | extension View { 15 | public func mapItemPicker( 16 | isPresented: Binding, 17 | onDismiss: ((MKMapItem?) -> Void)? = nil 18 | ) -> some View { 19 | MapItemPickerSheet(isPresented: isPresented, onDismiss: onDismiss, content: self) 20 | } 21 | } 22 | 23 | #endif 24 | --------------------------------------------------------------------------------