在 Combine 的 `assign(to:on:)` 主题中分配给 @State 不会导致视图更新

Posted

技术标签:

【中文标题】在 Combine 的 `assign(to:on:)` 主题中分配给 @State 不会导致视图更新【英文标题】:Assigning to @State in Combine's `assign(to:on:)` subject does not result in view update 【发布时间】:2020-03-26 06:34:44 【问题描述】:

我有一个地图视图,它有一个按钮,当按下该按钮时,应该将地图居中放在用户的当前位置上。我正在尝试使用 Swift 的 Combine 框架来实现这一点。我尝试通过添加一个名为mapCenter@State 属性并在Combine 的assign(to:on:) 主题中分配给该属性来解决此问题,如下所示:

struct MapWithButtonView: View 

    // What the current map view center should be.
    @State var mapCenter = CLLocationCoordinate2D(latitude: 42.35843, longitude: -71.05977)

    // A subject whose `send(_:)` method is being called from within the CenterButton view to center the map on the user's location.
    private var centerButtonTappedPublisher = PassthroughSubject<Bool, Never>()

    // A publisher that turns a "center button tapped" event into a coordinate.
    private var centerButtonTappedCoordinatePublisher: AnyPublisher<CLLocationCoordinate2D?, Never> 
        centerButtonTappedPublisher
            .map  _ in LocationManager.default.currentUserCoordinate 
            .eraseToAnyPublisher()
    

    private var coordinatePublisher: AnyPublisher<CLLocationCoordinate2D, Never> 
        Publishers.Merge(LocationManager.default.$initialUserCoordinate, centerButtonTappedCoordinatePublisher)
            .replaceNil(with: CLLocationCoordinate2D(latitude: 42.35843, longitude: -71.05977))
            .eraseToAnyPublisher()
    

    private var cancellableSet: Set<AnyCancellable> = []

    init() 
        // This does not result in an update to the view... why not?
        coordinatePublisher
            .receive(on: RunLoop.main)
            .handleEvents(receiveSubscription:  (subscription) in
                    print("Receive subscription")
                , receiveOutput:  output in
                    print("Received output: \(String(describing: output))")
                , receiveCompletion:  _ in
                    print("Receive completion")
                , receiveCancel: 
                    print("Receive cancel")
                , receiveRequest:  demand in
                    print("Receive request: \(demand)")
                )
            .assign(to: \.mapCenter, on: self)
            .store(in: &cancellableSet)
    

    var body: some View 
        ZStack 
            MapView(coordinate: mapCenter)
                .edgesIgnoringSafeArea(.all)

            CenterButton(buttonTappedPublisher: centerButtonTappedPublisher)
        
    

MapView 是一个 UIViewRepresentable 视图,如下所示:

struct MapView: UIViewRepresentable 
    // The center of the map.
    var coordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView 
        let mapView = MKMapView(frame: .zero)
        mapView.showsUserLocation = true
        return mapView
    

    func updateUIView(_ view: MKMapView, context: Context) 
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    

CenterButton 是一个简单的按钮,如下所示:

struct CenterButton: View 
    var buttonTappedPublisher: PassthroughSubject<Bool, Never>

    var body: some View 
        Button(action: 
            self.buttonTappedPublisher.send(true)
        ) 
            Image(systemName: "location.fill")
                .imageScale(.large)
                .accessibility(label: Text("Center map"))
        
    

LocationManager 是一个ObservableObject,它发布了用户的当前和初始位置:

class LocationManager: NSObject, ObservableObject 

    // The first location reported by the CLLocationManager.
    @Published var initialUserCoordinate: CLLocationCoordinate2D?
    // The latest location reported by the CLLocationManager.
    @Published var currentUserCoordinate: CLLocationCoordinate2D?

    private let locationManager = CLLocationManager()

    static let `default` = LocationManager()

    private override init() 
        super.init()

        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.pausesLocationUpdatesAutomatically = true
        locationManager.activityType = .other
        locationManager.requestWhenInUseAuthorization()
    


extension LocationManager: CLLocationManagerDelegate 

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) 
        switch status 
        case .authorizedAlways, .authorizedWhenInUse:
            NSLog("Location authorization status changed to '\(status == .authorizedAlways ? "authorizedAlways" : "authorizedWhenInUse")'")
            enableLocationServices()
        case .denied, .restricted:
            NSLog("Location authorization status changed to '\(status == .denied ? "denied" : "restricted")'")
            disableLocationServices()
        case .notDetermined:
            NSLog("Location authorization status changed to 'notDetermined'")
        default:
            NSLog("Location authorization status changed to unknown status '\(status)'")
        
    

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) 
        // We are only interested in the user's most recent location.
        guard let location = locations.last else  return 
        // Use the location to update the location manager's published state.
        let coordinate = location.coordinate
        if initialUserCoordinate == nil 
            initialUserCoordinate = coordinate
        
        currentUserCoordinate = coordinate
    

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) 
        NSLog("Location manager failed with error: \(error)")
    

    // MARK: Helpers.

    private func enableLocationServices() 
        locationManager.startUpdatingLocation()
    

    private func disableLocationServices() 
        locationManager.stopUpdatingLocation()
    

不幸的是,上述方法不起作用。当CenterButton 被点击时,视图永远不会更新。我最终通过使用具有@Published var mapCenter 属性的ObservableObject 视图模型对象解决了这个问题,但是我不知道为什么我使用@State 的初始解决方案不起作用。像我上面所做的那样更新@State 有什么问题?

请注意,如果尝试重现此问题,您需要在 Info.plist 文件中添加具有诸如“此应用需要访问您的位置”之类的值的 NSLocationWhenInUseUsageDescription 键,以便能够授予位置权限.

【问题讨论】:

你检查过这个吗? ***.com/questions/57799548/… 向我们展示您的地图视图代码.... @Chris 我已经添加了MapViewCenterButton 代码:) 可能你的locationmanager有缺陷......我们不知道,因为你没有向我们展示所有相关代码,只是一些代码...... @Chris,我很抱歉遗漏了代码。我包含了我认为是问题所在的代码。我不希望像你这样的读者不必阅读一堆其他代码。但显然这是必要的,所以我道歉。我现在已经包含了所有代码。什么都没有。 【参考方案1】:

新答案:

好的,我是一个组合新手,但它并没有让我离开,所以我尝试了又尝试......现在它可以工作了,即使在模拟器中也是如此。

问题是,您在 struct/Contentview 中完成了所有操作,而不是在发布您想要更改的值的单独类中进行组合。

看看这个:

class Model : ObservableObject 

    @Published var mapCenter : CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)


    // A subject whose `send(_:)` method is being called from within the CenterButton view to center the map on the user's location.
      public var centerButtonTappedPublisher = PassthroughSubject<Bool, Never>()

      // A publisher that turns a "center button tapped" event into a coordinate.
      private var centerButtonTappedCoordinatePublisher: AnyPublisher<CLLocationCoordinate2D?, Never> 
          centerButtonTappedPublisher
              .map  _ in
                  print ("new loc in pub: ", LocationManager.default.currentUserCoordinate)
                  return LocationManager.default.currentUserCoordinate

          
          .eraseToAnyPublisher()
      

      private var coordinatePublisher: AnyPublisher<CLLocationCoordinate2D, Never> 

          Publishers.Merge(LocationManager.default.$initialUserCoordinate, centerButtonTappedCoordinatePublisher)
              .replaceNil(with: CLLocationCoordinate2D(latitude: 2.0, longitude: 2.0))
          .eraseToAnyPublisher()
      

      private var cancellableSet: Set<AnyCancellable> = []
      var cancellable: AnyCancellable?

      init() 
            // This does not result in an update to the view... why not?

            coordinatePublisher
                .receive(on: RunLoop.main)
    //            .handleEvents(receiveSubscription:  (subscription) in
    //                print("Receive subscription")
    //            , receiveOutput:  output in
    //                print("Received output: \(String(describing: output))")
    //
    //            , receiveCompletion:  _ in
    //                print("Receive completion")
    //            , receiveCancel: 
    //                print("Receive cancel")
    //            , receiveRequest:  demand in
    //                print("Receive request: \(demand)")
    //            )
                .assign(to: \.mapCenter, on: self)
                .store(in: &cancellableSet)

            print(cancellableSet)

            self.cancellable = self.coordinatePublisher.receive(on: DispatchQueue.main)
                                                      .assign(to: \.mapCenter, on: self)
        


struct ContentView: View 

    @ObservedObject var model = Model()


    var body: some View 
        VStack 
            MapView(coordinate: model.mapCenter)
                .edgesIgnoringSafeArea(.all)

            CenterButton(buttonTappedPublisher: model.centerButtonTappedPublisher)
        
    


struct MapView: UIViewRepresentable 
    // The center of the map.
    var coordinate: CLLocationCoordinate2D

    let mapView = MKMapView(frame: .zero)

    func makeUIView(context: Context) -> MKMapView 
        mapView.showsUserLocation = true
        return mapView
    

    func updateUIView(_ view: MKMapView, context: Context) 
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        print("map new coordinate", coordinate)
        view.setRegion(region, animated: true)
    


class LocationManager: NSObject, ObservableObject 

    // The first location reported by the CLLocationManager.
    @Published var initialUserCoordinate: CLLocationCoordinate2D?
    // The latest location reported by the CLLocationManager.
    @Published var currentUserCoordinate: CLLocationCoordinate2D?

    private let locationManager = CLLocationManager()

    static let `default` = LocationManager()

    private override init() 
        super.init()

        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.pausesLocationUpdatesAutomatically = true
        locationManager.activityType = .other
        locationManager.requestWhenInUseAuthorization()

        enableLocationServices()
    


extension LocationManager: CLLocationManagerDelegate 

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) 
        switch status 
        case .authorizedAlways, .authorizedWhenInUse:
            NSLog("Location authorization status changed to '\(status == .authorizedAlways ? "authorizedAlways" : "authorizedWhenInUse")'")

            ()
        case .denied, .restricted:
            NSLog("Location authorization status changed to '\(status == .denied ? "denied" : "restricted")'")
            disableLocationServices()
        case .notDetermined:
            NSLog("Location authorization status changed to 'notDetermined'")
        default:
            NSLog("Location authorization status changed to unknown status '\(status)'")
        
    

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) 
        // We are only interested in the user's most recent location.
        guard let location = locations.last else  return 
        // Use the location to update the location manager's published state.
        let coordinate = location.coordinate
        if initialUserCoordinate == nil 
            initialUserCoordinate = coordinate
        
        currentUserCoordinate = coordinate
    

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) 
        NSLog("Location manager failed with error: \(error)")
    

    // MARK: Helpers.

    public func enableLocationServices() 
        locationManager.startUpdatingLocation()
    

    private func disableLocationServices() 
        locationManager.stopUpdatingLocation()
    


struct CenterButton: View 
    var buttonTappedPublisher: PassthroughSubject<Bool, Never>

    var body: some View 
        Button(action: 
            self.buttonTappedPublisher.send(true)
        ) 
            Image(systemName: "location.fill")
                .imageScale(.large)
                .accessibility(label: Text("Center map"))
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

旧答案:

好的,因为你没有给我们一个可复制的可重现的例子,我做了一个简单的例子。随意复制并使用它来解决您的问题。

struct MapView: UIViewRepresentable 

    var coordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView 
        let mapView = MKMapView(frame: .zero)
        mapView.showsUserLocation = true
        return mapView
    

    func updateUIView(_ view: MKMapView, context: Context) 
        let span = MKCoordinateSpan(latitudeDelta: 1.02, longitudeDelta: 1.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    


struct ContentView: View 

    @State var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)

    var body: some View 
        VStack 
            Button(action: 
                self.coordinate.latitude += 10
                self.coordinate.longitude = 30
            ) 
                Text("new coordinate")
            
            MapView(coordinate: coordinate)
        
    

【讨论】:

谢谢@Chris!我已经更新了我的问题以提供完整的、可重复的示例。我再次为最初遗漏它而道歉。 嘿@Chris,感谢您花时间解决这个问题!我很高兴您至少在此过程中学到了很多关于 Combine 的知识 :) 您更新的解决方案确实有效! “问题是,你在 struct/Contentview 中完成了所有工作,而不是在一个单独的类中进行组合,它发布了你想要更改的值” - 你知道为什么这需要在一个单独的类中发生吗?我想我只是不明白为什么直接分配给assign(to:on:) 中的@State 属性不起作用。【参考方案2】:

是的,您的代码有效。试试这个:

var body: some View 
        ZStack 
            MapView(coordinate: mapCenter)
                .edgesIgnoringSafeArea(.all)

            CenterButton(buttonTappedPublisher: centerButtonTappedPublisher)
        .onAppear() 

            Timer.scheduledTimer(withTimeInterval: 1, repeats: true)  (timer) in
                       self.mapCenter.latitude += 0.1
                   
        
    

您会看到,地图在不断移动。 也许您在模拟器中尝试过您的代码?那里的用户位置永远不会改变,所以如果你再次点击你的按钮什么也不会发生......在真实设备上尝试并开始移动;)

【讨论】:

嘿@Chris!我的代码可以在真实设备上运行吗?我在模拟器上试了一下,是的。然而,在模拟器的Debug -&gt; Location 菜单中,我选择了“City Run”,它模拟用户在加利福尼亚州库比蒂诺四处走动,因此理论上点击“中心地图”按钮应该会导致地图重新居中,即使在模拟器上也是如此.你确定我做的事情正确吗?我目前没有开发者帐号,所以无法在真机上试用代码。 我不知道 - 没有在设备上测试。仅使用我的代码就可以了-这就是我的意思。您的代码中的问题是地图中心永远不会改变,尽管当前用户位置发生了变化.....所以它永远不会更新地图视图 问题一定出在您的发布者链中...输出显示正确的值(坐标),但 mapcenter 没有获得新的/更新的值...。 我更新了我的答案,它现在可以工作了;)我学到了很多关于结合的知识;)【参考方案3】:

您不需要CLLocationManager,只需在UIViewRepresentable 中访问当前用户注释,例如map.userLocation.location.coordinate

https://developer.apple.com/documentation/mapkit/mkuserlocation/1452415-location?language=objc

【讨论】:

以上是关于在 Combine 的 `assign(to:on:)` 主题中分配给 @State 不会导致视图更新的主要内容,如果未能解决你的问题,请参考以下文章