From 4409913abb74278568ea27456e831aa1c3854cc6 Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Tue, 23 Apr 2024 15:48:25 -1000 Subject: [PATCH] fix: Toggle location saving and improve location acquisition reliability --- Thoughts.xcodeproj/project.pbxproj | 18 ++-- .../CLAuthorizationStatus.swift} | 26 ++--- Thoughts/{ => Model}/ApplicationModel.swift | 95 ++++++++++++------- Thoughts/Model/Document.swift | 2 +- .../{Location.swift => LocationDetails.swift} | 2 +- Thoughts/Model/Metadata.swift | 2 +- Thoughts/Views/ComposeView.swift | 3 + Thoughts/Views/ContentView.swift | 18 +++- 8 files changed, 105 insertions(+), 61 deletions(-) rename Thoughts/{Model/ComposeModel.swift => Extensions/CLAuthorizationStatus.swift} (72%) rename Thoughts/{ => Model}/ApplicationModel.swift (70%) rename Thoughts/Model/{Location.swift => LocationDetails.swift} (97%) diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index f763f97..251bce6 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ D80E08A62B6761370023DD4F /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80E08A52B6761370023DD4F /* Document.swift */; }; D83C2F882B857B8C00CE60F7 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83C2F872B857B8C00CE60F7 /* URL.swift */; }; D84010C52BD74C760049715E /* yams-license in Resources */ = {isa = PBXBuildFile; fileRef = D84010C42BD74C760049715E /* yams-license */; }; - D840967F2BD73CBE00926C85 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = D840967E2BD73CBE00926C85 /* Location.swift */; }; + D840967F2BD73CBE00926C85 /* LocationDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = D840967E2BD73CBE00926C85 /* LocationDetails.swift */; }; D850C2D42B69D85400338546 /* KeyedDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850C2D32B69D85400338546 /* KeyedDefaults.swift */; }; D850C2D62B69D8D500338546 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850C2D52B69D8D500338546 /* ContentView.swift */; }; D852AF022B6027CA00B77A3D /* ThoughtsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF012B6027CA00B77A3D /* ThoughtsApp.swift */; }; @@ -23,7 +23,6 @@ D852AF642B604AF500B77A3D /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF632B604AF500B77A3D /* ApplicationModel.swift */; }; D852AF662B604B1200B77A3D /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF652B604B1200B77A3D /* ComposeView.swift */; }; D852AF692B604B3E00B77A3D /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF682B604B3E00B77A3D /* MainMenu.swift */; }; - D852AF6B2B604B5100B77A3D /* ComposeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF6A2B604B5100B77A3D /* ComposeModel.swift */; }; D892249A2BD70CC300DA0932 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = D89224992BD70CC300DA0932 /* Yams */; }; D892249C2BD735A100DA0932 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D892249B2BD735A100DA0932 /* Metadata.swift */; }; D892249E2BD735BE00DA0932 /* ThoughtsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D892249D2BD735BE00DA0932 /* ThoughtsError.swift */; }; @@ -32,6 +31,7 @@ D89224A42BD7368F00DA0932 /* TimeZoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */; }; D89224A72BD736B900DA0932 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A62BD736B900DA0932 /* Date.swift */; }; D89224A92BD736DF00DA0932 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A82BD736DF00DA0932 /* TimeZone.swift */; }; + D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */; }; D8F06D4B2B67275B0039AEF4 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4A2B67275B0039AEF4 /* Diligence */; }; D8F06D4E2B6727630039AEF4 /* Interact in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4D2B6727630039AEF4 /* Interact */; }; D8F06D512B6731CE0039AEF4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F06D502B6731CE0039AEF4 /* SettingsView.swift */; }; @@ -62,7 +62,7 @@ D80E08A52B6761370023DD4F /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; D83C2F872B857B8C00CE60F7 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; D84010C42BD74C760049715E /* yams-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "yams-license"; sourceTree = ""; }; - D840967E2BD73CBE00926C85 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; + D840967E2BD73CBE00926C85 /* LocationDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDetails.swift; sourceTree = ""; }; D850C2D32B69D85400338546 /* KeyedDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedDefaults.swift; sourceTree = ""; }; D850C2D52B69D8D500338546 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; D852AEFE2B6027CA00B77A3D /* Thoughts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thoughts.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -78,7 +78,6 @@ D852AF632B604AF500B77A3D /* ApplicationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = ""; }; D852AF652B604B1200B77A3D /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; D852AF682B604B3E00B77A3D /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; - D852AF6A2B604B5100B77A3D /* ComposeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeModel.swift; sourceTree = ""; }; D892249B2BD735A100DA0932 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; D892249D2BD735BE00DA0932 /* ThoughtsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThoughtsError.swift; sourceTree = ""; }; D892249F2BD735DA00DA0932 /* RegionalDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionalDate.swift; sourceTree = ""; }; @@ -86,6 +85,7 @@ D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneTests.swift; sourceTree = ""; }; D89224A62BD736B900DA0932 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; D89224A82BD736DF00DA0932 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; + D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLAuthorizationStatus.swift; sourceTree = ""; }; D8F06D502B6731CE0039AEF4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D8F06D532B6733650039AEF4 /* thoughts-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "thoughts-license"; sourceTree = ""; }; D8F06D552B6734540039AEF4 /* material-icons-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "material-icons-license"; sourceTree = ""; }; @@ -123,9 +123,9 @@ D80E089F2B67572E0023DD4F /* Model */ = { isa = PBXGroup; children = ( - D852AF6A2B604B5100B77A3D /* ComposeModel.swift */, + D852AF632B604AF500B77A3D /* ApplicationModel.swift */, D80E08A52B6761370023DD4F /* Document.swift */, - D840967E2BD73CBE00926C85 /* Location.swift */, + D840967E2BD73CBE00926C85 /* LocationDetails.swift */, D892249B2BD735A100DA0932 /* Metadata.swift */, D892249F2BD735DA00DA0932 /* RegionalDate.swift */, D892249D2BD735BE00DA0932 /* ThoughtsError.swift */, @@ -136,6 +136,7 @@ D80E08A22B675AD40023DD4F /* Extensions */ = { isa = PBXGroup; children = ( + D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */, D850C2D32B69D85400338546 /* KeyedDefaults.swift */, D89224A12BD7365C00DA0932 /* TimeZone.swift */, D83C2F872B857B8C00CE60F7 /* URL.swift */, @@ -169,7 +170,6 @@ D852AF0A2B6027CC00B77A3D /* Thoughts.entitlements */, D8F8D7232B68C77900464A37 /* ThoughtsRelease.entitlements */, D80E089E2B674E900023DD4F /* Info.plist */, - D852AF632B604AF500B77A3D /* ApplicationModel.swift */, D852AF012B6027CA00B77A3D /* ThoughtsApp.swift */, D852AF052B6027CC00B77A3D /* Assets.xcassets */, D80E08A22B675AD40023DD4F /* Extensions */, @@ -384,7 +384,8 @@ files = ( D852AF642B604AF500B77A3D /* ApplicationModel.swift in Sources */, D850C2D62B69D8D500338546 /* ContentView.swift in Sources */, - D840967F2BD73CBE00926C85 /* Location.swift in Sources */, + D840967F2BD73CBE00926C85 /* LocationDetails.swift in Sources */, + D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */, D852AF692B604B3E00B77A3D /* MainMenu.swift in Sources */, D83C2F882B857B8C00CE60F7 /* URL.swift in Sources */, D89224A02BD735DA00DA0932 /* RegionalDate.swift in Sources */, @@ -397,7 +398,6 @@ D80E08A12B675AAC0023DD4F /* ComposeWindow.swift in Sources */, D89224A22BD7365C00DA0932 /* TimeZone.swift in Sources */, D892249C2BD735A100DA0932 /* Metadata.swift in Sources */, - D852AF6B2B604B5100B77A3D /* ComposeModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Thoughts/Model/ComposeModel.swift b/Thoughts/Extensions/CLAuthorizationStatus.swift similarity index 72% rename from Thoughts/Model/ComposeModel.swift rename to Thoughts/Extensions/CLAuthorizationStatus.swift index 37b641c..26a4f66 100644 --- a/Thoughts/Model/ComposeModel.swift +++ b/Thoughts/Extensions/CLAuthorizationStatus.swift @@ -18,19 +18,23 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Combine -import SwiftUI +import CoreLocation -class ComposeModel: ObservableObject { +extension CLAuthorizationStatus { - @Published var error: Error? - - let applicationModel: ApplicationModel - - private var cancellables: Set = [] - - init(applicationModel: ApplicationModel) { - self.applicationModel = applicationModel + var name: String { + switch self { + case .notDetermined: + return "not determined" + case .restricted: + return "restricted" + case .denied: + return "denied" + case .authorizedAlways: + return "authorized always" + @unknown default: + return "unknown (\(self.rawValue))" + } } } diff --git a/Thoughts/ApplicationModel.swift b/Thoughts/Model/ApplicationModel.swift similarity index 70% rename from Thoughts/ApplicationModel.swift rename to Thoughts/Model/ApplicationModel.swift index e8929e0..ebb2df4 100644 --- a/Thoughts/ApplicationModel.swift +++ b/Thoughts/Model/ApplicationModel.swift @@ -29,6 +29,7 @@ class ApplicationModel: NSObject { enum SettingsKey: String { case rootURL + case shouldSaveLocation } var rootURL: URL? { @@ -41,6 +42,20 @@ class ApplicationModel: NSObject { } } + var shouldSaveLocation: Bool { + didSet { + keyedDefaults.set(shouldSaveLocation, forKey: .shouldSaveLocation) + if shouldSaveLocation { + updateUserLocation() + } else { + document.location = nil + locationManager.stopUpdatingLocation() + } + } + } + + var locationRequests: [(Result) -> Void] = [] + var document = Document() { didSet { guard let url = rootURL else { @@ -56,10 +71,10 @@ class ApplicationModel: NSObject { let keyedDefaults = KeyedDefaults() let locationManager = CLLocationManager() - var lastKnownLocation: Location? = nil override init() { rootURL = try? keyedDefaults.securityScopedURL(forKey: .rootURL) + shouldSaveLocation = keyedDefaults.bool(forKey: .shouldSaveLocation, default: false) super.init() locationManager.delegate = self } @@ -67,17 +82,18 @@ class ApplicationModel: NSObject { func new() { dispatchPrecondition(condition: .onQueue(.main)) document = Document() - document.location = lastKnownLocation + updateUserLocation() } - func userLocation() { - guard locationManager.authorizationStatus != .notDetermined else { - print("Requesting location authorization...") - locationManager.requestWhenInUseAuthorization() - return + func updateUserLocation() { + requestUserLocation { result in + switch result { + case .success(let location): + self.document.location = location + case .failure(let error): + print("Failed to fetch location with error \(error)") + } } - print("Requsting location...") - locationManager.requestLocation() } func setRootURL() { @@ -98,67 +114,76 @@ class ApplicationModel: NSObject { extension ApplicationModel: CLLocationManagerDelegate { + func requestUserLocation(completion: @escaping (Result) -> Void) { + guard shouldSaveLocation else { + return + } + locationRequests.append(completion) + guard locationManager.authorizationStatus != .notDetermined else { + print("Requesting location authorization...") + locationManager.requestWhenInUseAuthorization() + return + } + print("Requsting location...") + locationManager.requestLocation() + } + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + dispatchPrecondition(condition: .onQueue(.main)) print("Location authorization status = \(manager.authorizationStatus.name)") guard manager.authorizationStatus == .authorized else { return } print("Requsting location...") + guard locationRequests.count > 0 else { + return + } manager.requestLocation() } - func resolveLocation(_ location: CLLocation) async -> Location { + func resolveLocation(_ location: CLLocation) async -> LocationDetails { let geocoder = CLGeocoder() do { guard let placemark = try await geocoder.reverseGeocodeLocation(location).first else { - return Location(location) + return LocationDetails(location) } - return Location(placemark) + return LocationDetails(placemark) } catch { print("Failed to geocode location with error \(error).") - return Location(location) + return LocationDetails(location) } } + func resolveRequests(_ result: Result) { + dispatchPrecondition(condition: .onQueue(.main)) + for locationRequest in self.locationRequests { + locationRequest(result) + } + locationRequests = [] + } + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + print("Did update locations.") Task { guard let location = locations.first else { return } - let lastKnownLocation = await resolveLocation(location) - print(lastKnownLocation) + let locationDetails = await resolveLocation(location) await MainActor.run { - self.lastKnownLocation = lastKnownLocation + resolveRequests(.success(locationDetails)) } } } func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { print("Location manager did fail with error \(error).") + resolveRequests(.failure(error)) } func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: any Error) { print("Location manager monitoring did fail with error \(error).") - } - -} - -extension CLAuthorizationStatus { - - var name: String { - switch self { - case .notDetermined: - return "not determined" - case .restricted: - return "restricted" - case .denied: - return "denied" - case .authorizedAlways: - return "authorized always" - @unknown default: - return "unknown (\(self.rawValue))" - } + resolveRequests(.failure(error)) } } diff --git a/Thoughts/Model/Document.swift b/Thoughts/Model/Document.swift index 8d929cc..e294974 100644 --- a/Thoughts/Model/Document.swift +++ b/Thoughts/Model/Document.swift @@ -28,7 +28,7 @@ struct Document { var date: Date var content: String var tags: String - var location: Location? = nil + var location: LocationDetails? = nil var isEmpty: Bool { return content.isEmpty diff --git a/Thoughts/Model/Location.swift b/Thoughts/Model/LocationDetails.swift similarity index 97% rename from Thoughts/Model/Location.swift rename to Thoughts/Model/LocationDetails.swift index 0165334..919174f 100644 --- a/Thoughts/Model/Location.swift +++ b/Thoughts/Model/LocationDetails.swift @@ -21,7 +21,7 @@ import CoreLocation import Foundation -struct Location: Codable { +struct LocationDetails: Codable { var latitude: CLLocationDegrees? var longitude: CLLocationDegrees? diff --git a/Thoughts/Model/Metadata.swift b/Thoughts/Model/Metadata.swift index 3ff1719..87b2432 100644 --- a/Thoughts/Model/Metadata.swift +++ b/Thoughts/Model/Metadata.swift @@ -25,6 +25,6 @@ struct Metadata: Codable { let date: RegionalDate let tags: [String] - let location: Location? + let location: LocationDetails? } diff --git a/Thoughts/Views/ComposeView.swift b/Thoughts/Views/ComposeView.swift index 8d86b66..0f68b1a 100644 --- a/Thoughts/Views/ComposeView.swift +++ b/Thoughts/Views/ComposeView.swift @@ -50,6 +50,9 @@ struct ComposeView: View { let longitude = location.longitude { Text("\(latitude), \(longitude)") } + } else if applicationModel.shouldSaveLocation { + ProgressView() + .controlSize(.small) } } .foregroundStyle(.secondary) diff --git a/Thoughts/Views/ContentView.swift b/Thoughts/Views/ContentView.swift index 3f1b843..610efb1 100644 --- a/Thoughts/Views/ContentView.swift +++ b/Thoughts/Views/ContentView.swift @@ -24,6 +24,18 @@ struct ContentView: View { var applicationModel: ApplicationModel + var systemImage: String { + if applicationModel.shouldSaveLocation { + if applicationModel.document.location != nil { + return "location.fill" + } else { + return "location" + } + } else { + return "location.slash" + } + } + var body: some View { HStack { if applicationModel.rootURL != nil { @@ -44,11 +56,11 @@ struct ContentView: View { .toolbar { ToolbarItem { Button { - applicationModel.userLocation() + applicationModel.shouldSaveLocation.toggle() } label: { let hasLocation = applicationModel.document.location != nil - Label("Use Location", systemImage: hasLocation ? "location.fill" : "location") - .foregroundColor(.purple) + Label("Use Location", systemImage: systemImage) + .foregroundColor(hasLocation ? .purple : nil) } .disabled(applicationModel.rootURL == nil) }