├── .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 |
--------------------------------------------------------------------------------