Присваивание @State в теме `assign (to: on:)` комбайна не приводит к обновлению представления - PullRequest
0 голосов
/ 26 марта 2020

У меня есть вид карты, в котором есть кнопка, которая при нажатии должна центрировать карту по текущему местоположению пользователя. Я пытаюсь достичь этого с помощью фреймворка Swift. Я попытался решить эту проблему, добавив свойство @State с именем mapCenter и присвоив этому свойству в теме 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. Я решил эту проблему, используя ObservableObject объект модели представления со свойством @Published var mapCenter, однако я не знаю, почему мое первоначальное решение с использованием @State не работает. Что не так с обновлением @State, как я это делал выше?

Обратите внимание, что если вы попытаетесь воспроизвести это, вам нужно будет добавить ключ NSLocationWhenInUseUsageDescription со значением, например «Это приложение требует доступа к вашему location "в вашем файле Info.plist, чтобы иметь возможность предоставлять разрешения для местоположения.

Ответы [ 2 ]

1 голос
/ 27 марта 2020

НОВЫЙ ОТВЕТ:

хорошо, я новичок комбайна ie, но он не позволил мне go, поэтому я попробовал и попробовал ... и теперь это работает, даже в симуляторе.

проблема была в том, что вы сделали все в 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)
        }
    }
}
0 голосов
/ 28 марта 2020

да, ваш код работает. Попробуйте это:

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
                   }
        }
    }

, и вы увидите, что карта постоянно движется. Может быть, вы пробовали свой код в симуляторе? там местоположение пользователя никогда не меняется, поэтому ничего не происходит, если вы снова нажмете на кнопку ... попробуйте на реальном устройстве и начните двигаться;)

...