Skip to content

Commit a65ffac

Browse files
authored
Merge pull request #7 from Quintschaf/add-recent-map-items
Add Recent MapItems to Search Sheet
2 parents 8aa5847 + f400dbe commit a65ffac

11 files changed

+164
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ MapItemPicker is a simple, yet highly customizable and powerful location picker
1010
<img src="https://user-images.githubusercontent.com/31473326/230954413-98d3428c-69d2-4273-9d49-d0e032fb7173.png" width="200" alt="Sheet for New York City" />
1111
<img src="https://user-images.githubusercontent.com/31473326/230954539-8d2efe0c-7762-4572-b805-24c383c57ab7.png" width="200" alt="Search for Airport in Germany" />
1212
<img src="https://user-images.githubusercontent.com/31473326/230954579-8c47e8ce-1d57-4623-a6de-c615a0dd5c82.png" width="200" alt="Sheet for Central Park" />
13-
<img src="https://user-images.githubusercontent.com/31473326/230954681-8a9bfc4e-957f-40b0-b345-7eb9f034e9f4.png" width="200" alt="Picker Inside a Full Screen Overlay" />
13+
<img src="https://user-images.githubusercontent.com/31473326/233777851-dc26c6eb-41b4-404e-a9b0-2cf41b967f35.png" width="200" alt="Picker Inside a Full Screen Overlay" />
1414
</p>
1515

1616
## Description

Sources/MapItemPicker/Controllers/MapItemSearchController.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,12 @@ extension MapItemSearchController {
242242
return
243243
}
244244

245-
self.completionItems = response.mapItems.compactMap(MapItemController.init(mapItem:))
245+
RunLoop.main.perform {
246+
self.completionItems = response.mapItems.compactMap(MapItemController.init(mapItem:))
247+
if let singularCompletionItem = self.singularCompletionItem {
248+
RecentMapItemsController.shared.addOrUpdate(mapItem: singularCompletionItem.item)
249+
}
250+
}
246251
})
247252
}
248253
}

Sources/MapItemPicker/Controllers/MapItemSearchViewCoordinator.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import MapKit
44

55
/// A class that coordinates a MapItemPicker. It can retrieve or set the region of the map view.
66
public class MapItemPickerController: NSObject, ObservableObject {
7-
@Published private(set) var selectedMapItem: MapItemController? = nil
7+
@Published private(set) var selectedMapItem: MapItemController? = nil {
8+
didSet {
9+
if oldValue != selectedMapItem {
10+
RecentMapItemsController.shared.mapItemWasSelected(selectedMapItem)
11+
}
12+
}
13+
}
814
@Published var selectedMapItemCluster: MKClusterAnnotation? = nil
915
@Published private(set) var shouldShowTopLeftButtons: Bool = true
1016
private var savedRectToSet: (rect: MKMapRect, animated: Bool)? = nil
@@ -43,7 +49,7 @@ public class MapItemPickerController: NSObject, ObservableObject {
4349
guard let mapView = currentMapView else { return }
4450

4551
if let selectedMapItem {
46-
let annotations = mapView.selectedAnnotations + mapView.annotations.filter({ !($0 is MKClusterAnnotation) })
52+
let annotations = mapView.selectedAnnotations + mapView.annotations//.filter({ !($0 is MKClusterAnnotation) })
4753
let annotation =
4854
annotations.first(where: {
4955
($0 as? MKClusterAnnotation)?.memberAnnotations.contains(annotation: selectedMapItem) ?? false
@@ -111,6 +117,9 @@ extension MapItemPickerController: MKMapViewDelegate {
111117
if let coordinator = annotation as? MapItemController {
112118
selectedMapItem = coordinator
113119
} else if let cluster = annotation as? MKClusterAnnotation, cluster.memberAnnotations.first is MapItemController {
120+
if let alreadySelected = self.selectedMapItem, cluster.memberAnnotations.contains(annotation: alreadySelected) {
121+
return
122+
}
114123
selectedMapItemCluster = cluster
115124
} else if #available(iOS 16, *), let item = annotation as? MKMapFeatureAnnotation {
116125
selectedMapItem = .init(mapFeatureAnnotation: item)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import Foundation
2+
import SwiftUI
3+
import Combine
4+
import SchafKit
5+
6+
/// A controller that holds the most recently selected map items.
7+
public class RecentMapItemsController: ObservableObject {
8+
/// The shared instance of `RecentMapItemsController`.
9+
public static let shared = RecentMapItemsController()
10+
11+
// MARK: - Constants
12+
13+
enum Constants {
14+
static let directory = SKDirectory.caches.directoryByAppending(path: MapItemPickerConstants.cacheDirectoryName, createIfNonexistant: true)
15+
16+
static let recentMapItemsFilename = "recentMapItems.json"
17+
static let maximumNumberOfRecentMapItems = 20
18+
}
19+
20+
// MARK: - Initializer
21+
22+
private init() {
23+
recentMapItems = Constants.directory.getData(at: Constants.recentMapItemsFilename).map { data in
24+
(try? JSONDecoder().decode([MapItem].self, from: data)) ?? []
25+
} ?? []
26+
}
27+
28+
// MARK: - Variables
29+
30+
var currentMapItemControllerObserver: AnyCancellable?
31+
/// The most recently selected map items.
32+
@Published public private(set) var recentMapItems: [MapItem] {
33+
didSet {
34+
saveRecentMapItems()
35+
}
36+
}
37+
38+
// MARK: - Public Functions
39+
40+
/// Adds or updates the given map item.
41+
///
42+
/// - note: If a similar map item is already in the stack, it will be removed and the given map item will be added to the top of the list.
43+
public func addOrUpdate(mapItem: MapItem) {
44+
var newMapItems = recentMapItems
45+
46+
newMapItems.removeAll(where: {
47+
mapItem.name == $0.name && mapItem.location == $0.location
48+
})
49+
newMapItems.insert(mapItem, at: 0)
50+
51+
while newMapItems.count > Constants.maximumNumberOfRecentMapItems {
52+
newMapItems.removeLast()
53+
}
54+
55+
recentMapItems = newMapItems
56+
}
57+
58+
// MARK: - Internal Functions
59+
60+
func mapItemWasSelected(_ mapItemController: MapItemController?) {
61+
guard let mapItemController else {
62+
currentMapItemControllerObserver = nil
63+
return
64+
}
65+
66+
addOrUpdate(mapItem: mapItemController.item)
67+
currentMapItemControllerObserver = mapItemController.objectWillChange.sink { _ in
68+
RunLoop.main.perform {
69+
self.addOrUpdate(mapItem: mapItemController.item)
70+
}
71+
}
72+
}
73+
74+
private func saveRecentMapItems() {
75+
if let data = try? JSONEncoder().encode(recentMapItems) {
76+
Constants.directory.save(data: data, at: Constants.recentMapItemsFilename)
77+
}
78+
}
79+
}

Sources/MapItemPicker/Resources/de.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ search.results = "Ergebnisse";
1212
search.moreSuggestions = "Weitere Vorschläge";
1313
search.loading = "Laden...";
1414
search.clearFilters = "Filter löschen";
15+
search.recentMapItems = "Zuletzt";
16+
search.recentMapItems.showMore = "Mehr zeigen";
17+
search.recentMapItems.showLess = "Weniger zeigen";
1518

1619
// MARK: Abstract Map Item Types
1720
mapItem.type.item = "Ort";

Sources/MapItemPicker/Resources/en.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ search.results = "Results";
1212
search.moreSuggestions = "More Suggestions";
1313
search.loading = "Loading...";
1414
search.clearFilters = "Clear Filters";
15+
search.recentMapItems = "Recents";
16+
search.recentMapItems.showMore = "Show More";
17+
search.recentMapItems.showLess = "Show Less";
1518

1619
// MARK: Abstract Map Item Types
1720
mapItem.type.item = "Location";

Sources/MapItemPicker/UI/Components/SearchCell.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ struct SearchCell: View {
77
let subtitle: String
88
let action: () -> Void
99

10+
init(systemImageName: String, color: Color, title: String, subtitle: String, action: @escaping () -> Void) {
11+
self.systemImageName = systemImageName
12+
self.color = color
13+
self.title = title
14+
self.subtitle = subtitle
15+
self.action = action
16+
}
17+
18+
init(mapItemController: MapItemController, coordinator: MapItemPickerController) {
19+
self.systemImageName = mapItemController.item.imageName
20+
self.color = mapItemController.item.color
21+
self.title = mapItemController.item.name
22+
self.subtitle = mapItemController.item.subtitle
23+
self.action = {
24+
coordinator.manuallySet(selectedMapItem: mapItemController)
25+
}
26+
}
27+
1028
var body: some View {
1129
Button(action: action) {
1230
HStack {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Foundation
2+
3+
public enum MapItemPickerConstants {
4+
/// The name of the cache directory MapItemPicker uses.
5+
public static let cacheDirectoryName = "MapItemPicker"
6+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
/// The section displaying the recently selected map items in the search view.
5+
public struct RecentMapItemsSection: View {
6+
@ObservedObject var controller: RecentMapItemsController = .shared
7+
@EnvironmentObject var coordinator: MapItemPickerController
8+
9+
@State private var showMore: Bool = false
10+
11+
public var body: some View {
12+
if !controller.recentMapItems.isEmpty {
13+
Section {
14+
ForEach(controller.recentMapItems.sliced(upTo: showMore ? RecentMapItemsController.Constants.maximumNumberOfRecentMapItems : 3), id: \.location) { mapItem in
15+
SearchCell(mapItemController: .init(item: mapItem), coordinator: coordinator)
16+
}
17+
} header: {
18+
HStack(alignment: .bottom) {
19+
Text("search.recentMapItems", bundle: .module)
20+
Spacer()
21+
if controller.recentMapItems.count > 3 {
22+
Button {
23+
showMore.toggle()
24+
} label: {
25+
Text(showMore ? "search.recentMapItems.showLess" : "search.recentMapItems.showMore", bundle: .module)
26+
.textCase(.none)
27+
.font(.footnote)
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}

Sources/MapItemPicker/UI/Sheets/SearchSheet.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,8 @@ struct SearchSheet<SearchView: View>: View {
142142
searcher.search(with: item)
143143
}
144144
}
145-
ForEach(searcher.items) { itemCoordinator in
146-
let item = itemCoordinator.item
147-
SearchCell(
148-
systemImageName: item.imageName,
149-
color: item.color,
150-
title: item.name,
151-
subtitle: item.subtitle
152-
) {
153-
coordinator.manuallySet(selectedMapItem: itemCoordinator)
154-
}
145+
ForEach(searcher.items) { itemController in
146+
SearchCell(mapItemController: itemController, coordinator: coordinator)
155147
}
156148
} header: {
157149
VStack {

Sources/MapItemPicker/UI/StandardSearchView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public struct StandardSearchView: View {
88

99
public var body: some View {
1010
List {
11+
RecentMapItemsSection()
1112
Section("search.category".moduleLocalized) {
1213
ForEach(MapItemCategory.allCases) { category in
1314
SearchCell(

0 commit comments

Comments
 (0)