我正在深入Combine
,并试图将逻辑从VM分离到服务,以便在应用程序中重用其他地方。我已经用几层(网络调用和获取用户位置)做到了这一点,但面临的问题是,如果逻辑在ViewModel之外,则无法连接@Publishers
,但它在其中时工作完美。对不起,写了这么多代码…
不要忘记将Privacy - Location When In Use Usage Description
添加到Info.plist
以请求位置
UI
struct ContentView: View {
@ObservedObject var weatherViewModel = WeatherViewModel()
var body: some View {
VStack {
Button(action: {
weatherViewModel.fetchLocation() }) {
Text("Get Location")
}
.buttonStyle(.bordered)
Text(weatherViewModel.currentWeather?.name ?? "Nothing yet")
}
.padding()
}
}
用户位置
import Foundation
import CoreLocation
import Combine
enum LocationError: Error {
case unauthorized
case unableToDetermineLocation
}
final class LocationManager: NSObject {
private let locationManager = CLLocationManager()
private var authorizationRequests: [(Result<Void, LocationError>) -> Void] = []
private var locationRequests: [(Result<CLLocation, LocationError>) -> Void] = []
override init() {
super.init()
locationManager.delegate = self
}
func requestWhenInUseAuthorization() -> Future<Void, LocationError> {
guard locationManager.authorizationStatus == .notDetermined else {
return Future { $0(.success(())) }
}
let future = Future<Void, LocationError> { completion in
self.authorizationRequests.append(completion)
}
locationManager.requestWhenInUseAuthorization()
return future
}
func requestLocation() -> Future<CLLocation, LocationError> {
guard locationManager.authorizationStatus == .authorizedAlways ||
locationManager.authorizationStatus == .authorizedWhenInUse
else {
return Future { $0(.failure(LocationError.unauthorized)) }
}
let future = Future<CLLocation, LocationError> { completion in
self.locationRequests.append(completion)
}
locationManager.requestLocation()
return future
}
private func handleLocationRequestResult(_ result: Result<CLLocation, LocationError>) {
while locationRequests.count > 0 {
let request = locationRequests.removeFirst()
request(result)
}
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
let locationError: LocationError
if let error = error as? CLError, error.code == .denied {
locationError = .unauthorized
} else {
locationError = .unableToDetermineLocation
}
handleLocationRequestResult(.failure(locationError))
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
handleLocationRequestResult(.success(location))
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
while authorizationRequests.count > 0 {
let request = authorizationRequests.removeFirst()
request(.success(()))
}
}
}
网络>import Combine
final class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url):
return "Bad response from URL: (url)"
case .unknown:
return "Unknown Error"
}
}
}
static func download(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ try handleURLResponse(output: $0, url: url)})
.eraseToAnyPublisher()
}
static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw NetworkingError.badURLResponse(url: url)
}
return output.data
}
static func handleCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
}
}
struct WeatherResponseModel: Codable {
let name: String
}
最后是ViewModel
。这里我想删除getCurrentWeather()
到WeatherService的逻辑
import Combine
import CoreLocation
final class WeatherViewModel: ObservableObject {
@Published var currentWeather: WeatherResponseModel?
private var cancellables = Set<AnyCancellable>()
private var weatherSubscription: AnyCancellable?
private let locationManager = LocationManager()
private let weatherService = WeatherService()
func fetchLocation() {
locationManager.requestWhenInUseAuthorization()
.flatMap { self.locationManager.requestLocation() }
.sink { _ in
} receiveValue: { location in
//MARK: Works
self.getCurrentWeather(for: location.coordinate)
//MARK: Don't work, need help here!
// self.weatherService.getWeather(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
// self.currentWeather = self.weatherService.weather
}
.store(in: &cancellables)
}
//MARK: Wanna remove this logic to WeatherService
private func getCurrentWeather(for coordinate: CLLocationCoordinate2D) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=(coordinate.latitude)&lon=(coordinate.longitude)&appid=299279dbd985cab3b71f59d2c2593766") else { return }
weatherSubscription = NetworkingManager.download(url: url)
.decode(type: WeatherResponseModel.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] (returnedWeather) in
self?.currentWeather = returnedWeather
self?.weatherSubscription?.cancel()
})
}
}
和WeatherService,我尝试在VM中连接其他实用程序,但不成功
import Combine
final class WeatherService {
@Published var weather: WeatherResponseModel?
private var weatherSubscription: AnyCancellable?
public func getWeather(latitude: Double, longitude: Double) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=(latitude)&lon=(longitude)&appid=299279dbd985cab3b71f59d2c2593766") else { return }
weatherSubscription = NetworkingManager.download(url: url)
.decode(type: WeatherResponseModel.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] (returnedWeather) in
self?.weather = returnedWeather
self?.weatherSubscription?.cancel()
})
}
}
我刚刚在你的代码中发现了一些可能导致你的问题的东西。
…编辑:
//MARK: Don't work, need help here!
// self.weatherService.getWeather(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
// self.currentWeather = self.weatherService.weather
第二行是不对的,因为self.weatherService.weather是一个异步更新的变量。你应该在它被更新后赋值。例如:
self.weatherService.$weather.receive(on: RunLoop.main)
.sink(receiveValue: { updatedWeather in
self.currentWeather = updatedWeather
}).store(in: &cancellables) // Don't forget to store it!