Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: ViewModifier doesn't listen to onReceive events

Tags:

ios

swift

swiftui

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:

import SwiftUI
import Combine

class MyViewModel: ObservableObject {
    @Published var myProperty: Bool = false
}
struct ContentView: View {
    @ObservedObject var viewModel: MyViewModel

    var body: some View {
        Text("Hello, World!")
        .modifier(MyOnReceive(viewModel: viewModel))
            .onTapGesture {
                self.viewModel.myProperty = true
        }
    }
}

struct MyOnReceive: ViewModifier {
    @ObservedObject var viewModel: MyViewModel

    func body(content: Content) -> some View {
        content
            .onReceive(viewModel.$myProperty) { theValue in
                print("The Value is \(theValue)") // <--- this is not executed
        }
    }
}

is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.

like image 938
JAHelia Avatar asked Apr 13 '20 14:04

JAHelia


People also ask

What is a viewmodifier?

A modifier that you apply to a view or another view modifier, producing a different version of the original value. Adopt the ViewModifier protocol when you want to create a reusable modifier that you can apply to any view.

Is it possible to split a view in SwiftUI?

However, thanks to SwiftUI’s highly composable design, it’s often quite easy to split a view’s body up into separate pieces, which might not even require any new View types to be created.

What are closures used for in SwiftUI?

Often when working with interactive SwiftUI views, we’re using closures to define the actions that we wish to perform when various events occur. For example, the following AddItemView has two interactive elements, a TextField and a Button, that both enable the user to add a new text-based Item to our app:


2 Answers

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
    }
}
like image 56
workingdog support Ukraine Avatar answered Oct 23 '22 17:10

workingdog support Ukraine


ObservableObject and @Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:

import SwiftUI

struct MyConfig {
    var myProperty: Bool = false

    mutating func myMethod() {
         myProperty = !myProperty
    }
}
struct ContentView: View {
    @State var config = MyConfig()

    var body: some View {
        Text("Hello, World!")
        .onTapGesture {
            config.myMethod()
        }
    }
}

Old answer:

Try onChange instead

https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)

.onChange(of: viewModel.myProperty) { newValue in
        print("newValue \(newValue)")
    }

But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like @State will give you the reference semantics you are used to with objects.

like image 35
malhal Avatar answered Oct 23 '22 18:10

malhal