🕌
SwiftUIとCombineでCore Locationを扱う
Combineを理解するためにSwiftUIでCore Locationのデータを表示するコードを書いてみました。
ソースコードはこちらです。
Model
- Publisherとして
PassthroughSubject
を作成、Delegateでsend
LocationDataSource.swift
import CoreLocation
import Combine
final class LocationDataSource: NSObject {
private let locationManager: CLLocationManager = .init()
private let authorizationSubject: PassthroughSubject<CLAuthorizationStatus, Never> = .init()
private let locationSubject: PassthroughSubject<[CLLocation], Never> = .init()
override init() {
super.init()
locationManager.delegate = self
}
func authorizationPublisher() -> AnyPublisher<CLAuthorizationStatus, Never> {
return Just(CLLocationManager().authorizationStatus).merge(with: authorizationSubject).eraseToAnyPublisher()
}
func locationPublisher() -> AnyPublisher<[CLLocation], Never> {
return locationSubject.eraseToAnyPublisher()
}
func requestAuthorization() {
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
func startTracking() {
locationManager.startUpdatingLocation()
}
func stopTracking() {
locationManager.stopUpdatingLocation()
}
}
extension LocationDataSource: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationSubject.send(manager.authorizationStatus)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationSubject.send(locations)
}
}
ViewModel
- ModelのPublisherに対して
sink
でsubscribeする
ViewModel.swift
import CoreLocation
import Combine
final class ViewModel: NSObject, ObservableObject {
let model: LocationDataSource
var cancellables = Set<AnyCancellable>()
@Published var authorizationStatus = CLAuthorizationStatus.notDetermined
@Published var location: CLLocation = .init()
var latitude: CLLocationDegrees {
location.coordinate.latitude
}
var longitude: CLLocationDegrees {
location.coordinate.longitude
}
init(model: LocationDataSource) {
self.model = model
}
func requestAuthorization() {
model.requestAuthorization()
}
func activate() {
model.authorizationPublisher().print("dump:status").sink { [weak self] authorizationStatus in
guard let self = self else { return }
self.authorizationStatus = authorizationStatus
}.store(in: &cancellables)
model.locationPublisher().print("dump:location").sink { [weak self] locations in
guard let self = self else { return }
if let last = locations.last {
self.location = last
}
}.store(in: &cancellables)
}
func deactivate() {
cancellables.removeAll()
}
func startTracking() {
model.startTracking()
}
func stopTracking() {
model.stopTracking()
}
}
- Publisherに対して
print()
することでコンソールにログが流れます
dump:status: receive subscription: (Merge)
dump:status: request unlimited
dump:status: receive value: (Not Determined)
dump:location: receive subscription: (PassthroughSubject)
dump:location: request unlimited
dump:status: receive value: (Authorized When In Use)
dump:location: receive value: ([<+37.33442613,-122.06864035> +/- 5.00m (speed 33.67 mps / course 266.13) @ 5/23/21, 3:34:00 PM Japan Standard Time])
dump:location: receive value: ([<+37.33442613,-122.06864035> +/- 5.00m (speed 33.67 mps / course 266.13) @ 5/23/21, 3:34:02 PM Japan Standard Time])
dump:location: receive value: ([<+37.33435661,-122.06939204> +/- 5.00m (speed 33.72 mps / course 261.91) @ 5/23/21, 3:34:02 PM Japan Standard Time])
dump:location: receive value: ([<+37.33430397,-122.06976336> +/- 5.00m (speed 33.69 mps / course 259.10) @ 5/23/21, 3:34:03 PM Japan Standard Time])
dump:location: receive value: ([<+37.33424182,-122.07013275> +/- 5.00m (speed 33.59 mps / course 257.70) @ 5/23/21, 3:34:04 PM Japan Standard Time])
View
ContentView.swift
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack(spacing: 16) {
HStack(spacing: 16) {
Button("request") {
viewModel.requestAuthorization()
}
Button("start") {
viewModel.startTracking()
}
Button("stop") {
viewModel.stopTracking()
}
}
Text(viewModel.authorizationStatus.description)
Text(String(format: "longitude: %f", viewModel.longitude))
Text(String(format: "latitude: %f", viewModel.latitude))
}.onAppear {
viewModel.activate()
}.onDisappear {
viewModel.deactivate()
}
}
}
struct ContentView_Previews: PreviewProvider {
static let viewModel = ViewModel(model: LocationDataSource())
static var previews: some View {
ContentView(viewModel: viewModel)
}
}
App
App.swift
import SwiftUI
@main
struct MyApp: App {
@StateObject var viewModel = ViewModel(model: LocationDataSource())
var body: some Scene {
WindowGroup {
ContentView(viewModel: viewModel)
}
}
}
Extension
Extension.swift
import CoreLocation
extension CLAuthorizationStatus: CustomStringConvertible {
public var description: String {
switch self {
case .authorizedAlways:
return "Always Authorized"
case .authorizedWhenInUse:
return "Authorized When In Use"
case .denied:
return "Denied"
case .notDetermined:
return "Not Determined"
case .restricted:
return "Restricted"
@unknown default:
return "🤷♂️"
}
}
}
参考
Discussion