Combine Onchange and Onappear Events in Swiftui View

Combine onChange and onAppear events in SwiftUI view?

It looks like onReceive may be what you need. Instead of:

.onChange(of: model.counter, perform: someLogic)
.onAppear { someLogic(counter: model.counter) }

you could do:

.onReceive(model.$counter, perform: someLogic)

The difference between onChange and onReceive is that the latter also fires when the view is initialised.


onChange

If you take a closer look at onChange, you'll see that it performs an action only when a value changes (and this doesn't happen when a view is initialised).

/// Adds a modifier for this view that fires an action when a specific
/// value changes.
/// ...
@inlinable public func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some View where V : Equatable

onReceive

However, the counter's publisher will emit the value also when a view is initialised. This will make onReceive perform an action passed as a parameter.

/// Adds an action to perform when this view detects data emitted by the
/// given publisher.
/// ...
@inlinable public func onReceive<P>(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher, P.Failure == Never

Just note that onReceive is not an equivalent of onChange+onAppear.

onAppear is called when a view appears but in some cases a view may be initialised again without firing onAppear.

Infinite loop when using onAppear in SwiftUI

This issue is a bug in iOS 14 with Group, and will still happen even when building with Xcode 13.

What's happening is the SwiftUI runtime is messing up its view diff, so it believes that it "appeared" when really it only reloaded, and thus did not disappear (which is a requirement for onAppear to be called, hence a bug).

To compensate for this issue, you'll need to use initialisers rather than relying on onAppear.

I've amended your code, this amended version does not infinitely loop.

struct DetailView: View {

@StateObject var viewModel = DetailViewModel()

var body: some View {
Group {
switch viewModel.state {
case .loading:
Text("Loading...")
case .loaded:
HStack {
Text("Loaded")
Button("Retry") {
viewModel.fetchData()
}
}
}
}
}
}

class DetailViewModel: ObservableObject {

enum State {
case loading
case loaded
}

@Published var state: State = .loading

init() {
self.fetchData()
}

func fetchData() {
state = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .loaded
}
}
}

Hope the project goes well!

How to make a reusable modifier for view lifecycle events (onAppear, onDisappear)?

What you try to do is already present and named opacity transition, which is written in one modifier.

Here is a demo:

demo

struct ActiveView: View {
@State var showCode: Bool = false
var body: some View {
ZStack {
if self.showCode {
Color.black.opacity(0.7)
.transition(AnyTransition.opacity.animation(.default))
}
Button("Demo") { self.showCode.toggle() }
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

Using SwiftUI and Combine to conditionally display a view based on authorization status?

Try adding @ObservedObject var healthKitAuthManager = HealthKitAuthManager() instead of let healthKitAuthManager = HealthKitAuthManager(). This way the @Published variable will trigger a new view rendering.

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


Related Topics



Leave a reply



Submit