diff --git a/DevAcademy.xcodeproj/project.pbxproj b/DevAcademy.xcodeproj/project.pbxproj index b0fbf25..c92e7fe 100644 --- a/DevAcademy.xcodeproj/project.pbxproj +++ b/DevAcademy.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 84FA93B72A68317F00DFC974 /* Point.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FA93B62A68317F00DFC974 /* Point.swift */; }; 84FA93B92A68319800DFC974 /* Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FA93B82A68319800DFC974 /* Properties.swift */; }; BE1E12592AA5C6CB000BA3D8 /* StoreadAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE1E12582AA5C6CB000BA3D8 /* StoreadAsyncImage.swift */; }; + BE38A88A2AA7294400EB5431 /* UserLocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE38A8892AA7294400EB5431 /* UserLocationService.swift */; }; BEEF9EC12A67C74E002126F4 /* PlacesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF9EC02A67C74E002126F4 /* PlacesService.swift */; }; C05610492A5C78CA007FB970 /* DevAcademyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05610482A5C78CA007FB970 /* DevAcademyApp.swift */; }; C056104B2A5C78CA007FB970 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C056104A2A5C78CA007FB970 /* RootView.swift */; }; @@ -44,6 +45,8 @@ 84FA93B62A68317F00DFC974 /* Point.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Point.swift; sourceTree = ""; }; 84FA93B82A68319800DFC974 /* Properties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Properties.swift; sourceTree = ""; }; BE1E12582AA5C6CB000BA3D8 /* StoreadAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreadAsyncImage.swift; sourceTree = ""; }; + BE38A8882AA7285B00EB5431 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + BE38A8892AA7294400EB5431 /* UserLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocationService.swift; sourceTree = ""; }; BEEF9EC02A67C74E002126F4 /* PlacesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesService.swift; sourceTree = ""; }; C05610452A5C78CA007FB970 /* DevAcademy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DevAcademy.app; sourceTree = BUILT_PRODUCTS_DIR; }; C05610482A5C78CA007FB970 /* DevAcademyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevAcademyApp.swift; sourceTree = ""; }; @@ -110,6 +113,7 @@ children = ( C0B74A2E2A8280D80018D10F /* Services.swift */, BEEF9EC02A67C74E002126F4 /* PlacesService.swift */, + BE38A8892AA7294400EB5431 /* UserLocationService.swift */, ); path = Services; sourceTree = ""; @@ -142,6 +146,7 @@ C05610472A5C78CA007FB970 /* DevAcademy */ = { isa = PBXGroup; children = ( + BE38A8882AA7285B00EB5431 /* Info.plist */, C05610482A5C78CA007FB970 /* DevAcademyApp.swift */, C056104A2A5C78CA007FB970 /* RootView.swift */, C0CAEAC42A801F3C008D87C2 /* Environment */, @@ -266,6 +271,7 @@ buildActionMask = 2147483647; files = ( 849BA6402A79094500D8E0F0 /* PlaceDetailView.swift in Sources */, + BE38A88A2AA7294400EB5431 /* UserLocationService.swift in Sources */, C0CAEAC12A8017E7008D87C2 /* MapView.swift in Sources */, C0CAEAD12A8023FB008D87C2 /* PlacesObservableObject.swift in Sources */, C0CAEABB2A8009A4008D87C2 /* PlacesViewState.swift in Sources */, @@ -417,6 +423,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DevAcademy/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -446,6 +453,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DevAcademy/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/DevAcademy/Environment/ObservableObjects.swift b/DevAcademy/Environment/ObservableObjects.swift index c929f3b..d113450 100644 --- a/DevAcademy/Environment/ObservableObjects.swift +++ b/DevAcademy/Environment/ObservableObjects.swift @@ -16,7 +16,7 @@ final class ObservableObjects { extension ObservableObjects { convenience init(services: Services) { - let places = PlacesObservableObject(placesService: services.placesService) + let places = PlacesObservableObject(placesService: services.placesService, locationService: services.locationService) self.init( places: places diff --git a/DevAcademy/Environment/ObservableObjects/PlacesObservableObject.swift b/DevAcademy/Environment/ObservableObjects/PlacesObservableObject.swift index 234fc23..90a814e 100644 --- a/DevAcademy/Environment/ObservableObjects/PlacesObservableObject.swift +++ b/DevAcademy/Environment/ObservableObjects/PlacesObservableObject.swift @@ -1,4 +1,5 @@ import Foundation +import CoreLocation final class PlacesObservableObject: ObservableObject { @Published var places: [Place] = [] @@ -15,9 +16,29 @@ final class PlacesObservableObject: ObservableObject { didSet { updatePlaces() } } private let placesService: PlacesService - - init(placesService: PlacesService) { + private let locationService: UserLocationService + private var lastUpdatedLocation: CLLocation? + + + init(placesService: PlacesService, locationService: UserLocationService) { self.placesService = placesService + self.locationService = locationService + + self.locationService.listenDidUpdateLocation { [weak self] location in + DispatchQueue.main.async { + self?.locationDidUpdate(location: location) + } + } + + self.locationService.listenDidUpdateStatus { [weak self] status in + switch status { + case .notDetermined: + self?.locationService.requestAuthorization() + case .authorizedWhenInUse, .authorizedAlways: + self?.beginLocationUpdates() + default: break + } + } } func set(place: Place, favourite setFavourite: Bool) { @@ -76,6 +97,20 @@ final class PlacesObservableObject: ObservableObject { private func updatePlaces() { var regularPlaces = rawPlaces + + if let lastUpdatedLocation { + regularPlaces.sort { lPlace, rPlace in + guard let rPoint = rPlace.geometry?.cllocation else { + return false + } + guard let lPoint = lPlace.geometry?.cllocation else { + return true + } + + return lastUpdatedLocation.distance(from: lPoint).magnitude < lastUpdatedLocation.distance(from: rPoint).magnitude + } + } + var presentOnTop: [Place] = [] let favouritePlaces = self.favouritePlaces ?? [] @@ -90,4 +125,18 @@ final class PlacesObservableObject: ObservableObject { self.places = presentOnTop + regularPlaces } + + private func shouldUpdate(location: CLLocation) -> Bool { + lastUpdatedLocation.flatMap { $0.distance(from: location).magnitude > 500 } ?? true + } + + private func beginLocationUpdates() { + self.locationService.startUpdatingLocation() + } + + private func locationDidUpdate(location: [CLLocation]) { + guard let userLocation = location.first, shouldUpdate(location: userLocation) else { return } + self.lastUpdatedLocation = userLocation + updatePlaces() + } } diff --git a/DevAcademy/Info.plist b/DevAcademy/Info.plist new file mode 100644 index 0000000..3554006 --- /dev/null +++ b/DevAcademy/Info.plist @@ -0,0 +1,8 @@ + + + + + NSLocationWhenInUseUsageDescription + Najít nejbližší podnik + + diff --git a/DevAcademy/Model/Point.swift b/DevAcademy/Model/Point.swift index 06b00dc..076e588 100644 --- a/DevAcademy/Model/Point.swift +++ b/DevAcademy/Model/Point.swift @@ -1,3 +1,5 @@ +import CoreLocation + struct Point: Decodable { var latitude: Double var longitude: Double @@ -7,3 +9,9 @@ struct Point: Decodable { case longitude = "x" } } + +extension Point { + var cllocation: CLLocation { + .init(latitude: latitude, longitude: longitude) + } +} diff --git a/DevAcademy/Services/Services.swift b/DevAcademy/Services/Services.swift index e808846..e89e832 100644 --- a/DevAcademy/Services/Services.swift +++ b/DevAcademy/Services/Services.swift @@ -2,20 +2,25 @@ import Foundation final class Services { let placesService: PlacesService + let locationService: UserLocationService init( - placesService: PlacesService + placesService: PlacesService, + locationService: UserLocationService ) { self.placesService = placesService + self.locationService = locationService } } extension Services { convenience init() { let placesService = ProductionPlacesService() + let locationService = ProductionUserLocationService() self.init( - placesService: placesService + placesService: placesService, + locationService: locationService ) } } @@ -24,6 +29,7 @@ extension Services { extension Services { static let mock = Services( - placesService: MockPlacesService() + placesService: MockPlacesService(), + locationService: MockLocationService() ) } diff --git a/DevAcademy/Services/UserLocationService.swift b/DevAcademy/Services/UserLocationService.swift new file mode 100644 index 0000000..e9186bb --- /dev/null +++ b/DevAcademy/Services/UserLocationService.swift @@ -0,0 +1,63 @@ +import Combine +import CoreLocation + +protocol UserLocationService { + func startUpdatingLocation() + func stopUpdatingLocation() + func requestAuthorization() + + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) +} + +final class ProductionUserLocationService: NSObject, UserLocationService { + + private let manager = CLLocationManager() + private var stateChangeHandler: ((CLAuthorizationStatus) -> Void)? + private var locationChangeHandler: (([CLLocation]) -> Void)? + + override init() { + super.init() + manager.delegate = self + } + + func requestAuthorization() { + manager.requestWhenInUseAuthorization() + } + + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) { + self.locationChangeHandler = handler + } + + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) { + self.stateChangeHandler = handler + handler(manager.authorizationStatus) + } + + func stopUpdatingLocation() { + manager.stopUpdatingLocation() + } + + func startUpdatingLocation() { + manager.startUpdatingLocation() + } + +} + +extension ProductionUserLocationService: CLLocationManagerDelegate { + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + self.stateChangeHandler?(manager.authorizationStatus) + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + self.locationChangeHandler?(locations) + } +} + +final class MockLocationService: UserLocationService { + func startUpdatingLocation() { /* nop */ } + func stopUpdatingLocation() { /* nop */ } + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) { /* nop */ } + func requestAuthorization() { /* nop */ } + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) { /* nop */ } +} diff --git a/doc/l9assignment.md b/doc/l9assignment.md new file mode 100644 index 0000000..6e38088 --- /dev/null +++ b/doc/l9assignment.md @@ -0,0 +1,110 @@ +# Lekce 9: Zadání + +Lokační služby + + +## Úkol 1: User Location Service +Motivace: Potřebujeme vytvořit třídu, která bude zaobalovat komunikaci s API pro Polohové služby. Tato service musí mluvit s managerem pomocí patternu *delegate*, ale sama poskytuje API s closure. + + 1. V adresáři `Services` vytvořme soubor `UserLocationService.swift` pro novou `UserLocationService`. + 2. Využijte template (viz níže) + 3. Tuto service přidejte na všechny nutná místa (viz předchozí lekce - přidání `PlaceService`) + 4. Tuto service přidejte do `PlacesObservableObjectu`. + 5. Implementujte jednotlivé funkce service: + - Projděte si dokumentaci [CLLocationManager](https://developer.apple.com/documentation/corelocation/cllocationmanager) a implementujte následující funkce: + - Konstruktor vytvoří instanci CLLocationManager a uloží ji do proměnné `manager`. Nastavte delegáta na `self`. + - Funkce `requestAuthoriation` požádá manager o pravomoc získávat lokaci, pokud je aplikace **na popředí**. + - Funkce `listenDidUpdateLocation` nastaví novou closure pro naslouchání na změnu lokace + - Funkce `listenDidUpdateStatus` nastaví novou closure pro naslouchání na změnu stavu autorizace + - Funkce `stopUpdatingLocation` vypne získávání polohy + - Funkce `startUpdatingLocation` zapne získávání polohy + - Funkce v delegátu `locationManagerDidChangeAuthorization` do odpovídajícího handleru pošle nový stav autorizace + - Funkce v delegátu `locationManager(_:didUpdateLocations:)` do odpovídajícího handleru pošle aktuální polohu + +## Úkol 2: Příprava na nasazení: +Motivace: Před tím, než můžeme začít používat naše nové API si musíme ještě připravit nějaké věci. + +1. V souboru `Point.swift` vytvořte `extension` nad typem `Point`. Tato `extension` bude obsahovat *computed property* `var cllocation: CLLocation`, která z `Point` vytvoří `CLLocation`. (Budete muset importovat `CoreLocation`). +2. iOS vyžaduje odůvodnění, proč chcete používat polohu. Toto odůvodnění **musí** být lokalizované a v Apple jej kontrolují! Otevřete soubor `Info.plist` a přidejte nový klíč `NSLocationWhenInUseUsageDescription` a odpovídající slovní popis. + +## Úkol 3: Implementace řazení podle polohy +Motivace: Nyní můžeme implementovat ve třídě `PlacesObservableObject` řazení polohy. + +1. Přidejte novou proměnnou `private var lastUpdatedLocation: CLLocation?` - zde budete ukládat poslední bod, podle kterého byla místa seřazena. +2. Do funkce `updatePlaces` přidejte novou funkcionalitu. Pokud je proměnná `lastUpdatedLocation ` ne-nil, seřaďte místa podle vzdálenosti. Použijte na pole `regularPlaces` funkci `sort` +3. Přidejte funkci `beginLocationUpdates`, která zapne aktualizaci polohy na service. +4. Přidejte funkci `func shouldUpdate(location: CLLocation) -> Bool`. Tato funkce vrátí `true`, pokud je **první prvek v poli** v argumentu `location` vzdálen od `lastUpdatedLocation` více, než je (vámi zvolená) vzdálenost. Pokud je `lastUpdatedLocation` nil, vrátí automaticky `true`. +5. V konstruktoru Observable Objectu nastavte pro `locationService` handler `listenDidUpdateLocation`. Pokud funkce `self.shouldUpdate(location:)` vrátí `true`, aktualizujte `lastUpdatedLocation` a zavolejte `updatePlaces()`. +6. V konstruktoru Observable Object nastavte pro `locationService` handler `listenDidUpdateStatus`. + - Pokud je status `notDetermined`, zavolejte na `locationService` funkci `requestAuthorization()` + - Pokud je status `authorizedWhenInUse` nebo `authorizedAlways`, započněte získávání polohy + - Jinak nedělejte nic + + +--- + +Template pro service + +```swift +import Combine +import CoreLocation + +protocol UserLocationService { + func startUpdatingLocation() + func stopUpdatingLocation() + func requestAuthorization() + + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) +} + +final class ProductionUserLocationService: NSObject, UserLocationService { + + private let manager: CLLocationManager + private var stateChangeHandler: ((CLAuthorizationStatus) -> Void)? + private var locationChangeHandler: (([CLLocation]) -> Void)? + + override init() { + // TODO + } + + func requestAuthorization() { + // TODO + } + + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) { + // TODO + } + + + func stopUpdatingLocation() { + // TODO + } + + func startUpdatingLocation() { + // TODO + } + + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) { + // TODO + } +} + +extension ProductionUserLocationService: CLLocationManagerDelegate { + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + // TODO + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // TODO + } +} + +final class MockLocationService: UserLocationService { + func startUpdatingLocation() { /* nop */ } + func stopUpdatingLocation() { /* nop */ } + func listenDidUpdateLocation(handler: @escaping ([CLLocation]) -> Void) { /* nop */ } + func requestAuthorization() { /* nop */ } + func listenDidUpdateStatus(handler: @escaping (CLAuthorizationStatus) -> Void) { /* nop */ } +} +``` \ No newline at end of file