Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lekce 9: Lokační služby #10

Open
wants to merge 3 commits into
base: feature/L-8-persistence
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions DevAcademy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -44,6 +45,8 @@
84FA93B62A68317F00DFC974 /* Point.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Point.swift; sourceTree = "<group>"; };
84FA93B82A68319800DFC974 /* Properties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Properties.swift; sourceTree = "<group>"; };
BE1E12582AA5C6CB000BA3D8 /* StoreadAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreadAsyncImage.swift; sourceTree = "<group>"; };
BE38A8882AA7285B00EB5431 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
BE38A8892AA7294400EB5431 /* UserLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocationService.swift; sourceTree = "<group>"; };
BEEF9EC02A67C74E002126F4 /* PlacesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesService.swift; sourceTree = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -110,6 +113,7 @@
children = (
C0B74A2E2A8280D80018D10F /* Services.swift */,
BEEF9EC02A67C74E002126F4 /* PlacesService.swift */,
BE38A8892AA7294400EB5431 /* UserLocationService.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -142,6 +146,7 @@
C05610472A5C78CA007FB970 /* DevAcademy */ = {
isa = PBXGroup;
children = (
BE38A8882AA7285B00EB5431 /* Info.plist */,
C05610482A5C78CA007FB970 /* DevAcademyApp.swift */,
C056104A2A5C78CA007FB970 /* RootView.swift */,
C0CAEAC42A801F3C008D87C2 /* Environment */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion DevAcademy/Environment/ObservableObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import CoreLocation

final class PlacesObservableObject: ObservableObject {
@Published var places: [Place] = []
Expand All @@ -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) {
Expand Down Expand Up @@ -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 ?? []

Expand All @@ -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()
}
}
8 changes: 8 additions & 0 deletions DevAcademy/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Najít nejbližší podnik</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions DevAcademy/Model/Point.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CoreLocation

struct Point: Decodable {
var latitude: Double
var longitude: Double
Expand All @@ -7,3 +9,9 @@ struct Point: Decodable {
case longitude = "x"
}
}

extension Point {
var cllocation: CLLocation {
.init(latitude: latitude, longitude: longitude)
}
}
12 changes: 9 additions & 3 deletions DevAcademy/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand All @@ -24,6 +29,7 @@ extension Services {

extension Services {
static let mock = Services(
placesService: MockPlacesService()
placesService: MockPlacesService(),
locationService: MockLocationService()
)
}
63 changes: 63 additions & 0 deletions DevAcademy/Services/UserLocationService.swift
Original file line number Diff line number Diff line change
@@ -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 */ }
}
110 changes: 110 additions & 0 deletions doc/l9assignment.md
Original file line number Diff line number Diff line change
@@ -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 */ }
}
```