在 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 我已经添加了MapView
和CenterButton
代码:)
可能你的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 -> 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 不会导致视图更新的主要内容,如果未能解决你的问题,请参考以下文章
How To Use Logstash and Kibana To Centralize Logs On CentOS 6
How To Deploy OpenShift Container Platform 4.8 on KVM
How To Deploy OpenShift Container Platform 4.8 on KVM
How To Deploy OpenShift Container Platform 4.8 on KVM
How to Fix “Failed to play test tone” error on Windows 7, 8 and 10