How can I get data from ObservedObject with onReceive in SwiftUI?
First in your view you need to request the HeadingProvider to start updating heading. You need to listen to objectWillChange notification, the closure has one argument which is the new value that is being set on ObservableObject.
I have changed your Compass a bit:
struct Compass: View {
@StateObject var headingProvider = HeadingProvider()
@State private var angle: CGFloat = 0
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(self.headingProvider.objectWillChange) { newHeading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = newHeading
}
}
Text(String("\(angle)"))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
} .onAppear(perform: {
self.headingProvider.updateHeading()
})
}
}
I have written an example HeadingProvider:
public class HeadingProvider: NSObject, ObservableObject {
public let objectWillChange = PassthroughSubject<CGFloat,Never>()
public private(set) var heading: CGFloat = 0 {
willSet {
objectWillChange.send(newValue)
}
}
private let locationManager: CLLocationManager
public override init(){
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
}
public func updateHeading() {
locationManager.startUpdatingHeading()
}
}
extension HeadingProvider: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.heading = CGFloat(newHeading.trueHeading)
}
}
}
Remember you need to handle asking for permission to read user's location and you need to call stopUpdatingHeading() at some point.
onReceive not getting called in SwiftUI View when ObservedObject changes
Well, here it is - your MenuView
and MenuItemView
use different instances of view model
1)
struct MenuView: View {
@ObservedObject private var viewModel: MenuViewModel
@State var showPicker = false
@State private var menu: Menu = Menu.mockMenu()
init(viewModel: MenuViewModel = MenuViewModel()) { // 1st one created
2)
struct MenuItemView: View {
var item: MenuItem
@ObservedObject private var viewModel: MenuViewModel = MenuViewModel() // 2nd one
thus, you modify one instance, but subscribe to changes in another one. That's it.
Solution: pass view model via .environmentObject
or via argument from MenuView
to MenuItemView
.
SwiftUI: ViewModifier doesn't listen to onReceive events
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
Why is onReceive block on SwiftUI View called when Publisher argument doesn't have any new values to emit?
You use Publishers.Sequence
in your code, which publishes the elements in the sequence one by one.
And with your code, [model.greeting].publisher
is re-evaluated and the publisher is re-created at each time body
is evaluated, which means the published element reset to the first.
With the following code, you can find that x
is printed out, at each time body
is evaluated:
struct ContentView: View {
@ObservedObject var model: Model = Model()
let pickerTitles = ["One", "Two", "Three"]
var body: some View {
VStack {
return Picker("Options", selection: $model.selectedIndex) {
ForEach(0 ..< pickerTitles.count) { index in
Text(self.pickerTitles[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
.onReceive(["x"].publisher){str in
print(str)
}
}
}
If you want to receive the changes only on greeting
, you can write something like this:
struct ContentView: View {
@ObservedObject var model: Model = Model()
let pickerTitles = ["One", "Two", "Three"]
var body: some View {
VStack {
return Picker("Options", selection: $model.selectedIndex) {
ForEach(0 ..< pickerTitles.count) { index in
Text(self.pickerTitles[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
.onReceive(model.$greeting){str in
print(str)
}
}
}
onReceive is not triggered after published value has changed
but why does it work?
Most probably you use AddChallengeView
in NavigationView
(or another container, which recreates content during workflow), so having
@ObservedObject var viewModel = AddChallengeViewModel()
creates new instance of AddChallengeViewModel
class on each such view re-creation, so any previous changes are lost. However
@StateObject var viewModel = AddChallengeViewModel()
preserves instance of model (created at first time) and injects it into new view of same type re-created in same place of view hierarchy. Moreover, new view is notified about all changes of that same model.
Actually @StateObject
property wrapper gives same behaviour for ObservableObject
as @State
gives for value types.
Related Topics
How to Check Object Is Nil or Not in Swift
How to Reload a UI View's Content Swift
Swift - How to Create a View with a Shape Cropped in It
What Is the Role of Avcapturedevicetype.Builtindualcamera
Get Header Data from a Request Response in Swift
How to Add Constraints Programmatically to My Uilabel
How to Open/View iOS Oslogs Stored on Device
Transparent Nscollectionview Background
Using Swift Library in Xamarin
Scene Created in Sprite Kit Level Editor Is Not Working
Occlusion Material or Hold-Out Shader in Arkit and Scenekit
Is There Any Way of Locking an Object in Swift Like in C#
Need to Check That Braces in Given Array Are Balanced or Not
How to Add a Double Tap Gesture Recognizer in Swift
How to Connect Aksequencer to a Akcallbackinstrument
Swiftui - Foreach Deletion Transition Always Applied to Last Item Only