skip to Main Content

The following is a minimum example code with the problem.

struct TestView: View {
    @State var text = "Hello"
    let useCase = TestUseCase()

    init() {
        useCase.output = self
    }

    var body: some View {
        Text(text)
            .onAppear {
                // ① useCase.output = self
                useCase.show()
            }
    }
}
extension TestView: TestUseCaseOutput {
    func showText(text: String) {
        self.text = text
    }
}

class TestUseCase {
    var output: TestUseCaseOutput?
    func show() {
        output?.showText(text: "Changed")
    }
}
protocol TestUseCaseOutput {
    func showText(text: String)
}

This code changes the text from "Hello" to "Changed" when the view is displayed, but the change is not reflected. The showText method was called, but it had no effect. I also found that if I set the delegate at ①, the text was updated correctly.

Can anyone tell me the cause of this problem?

2

Answers


  1. SwiftUI views are structs, and therefore immutable. Whenever a SwiftUI changes, a new instance of that view struct is created.

    When you update text, SwiftUI needs to create a new instance of TestView. But, the new instance has text set to Hello (and it was also have a new instance of TestUseCase) so you don’t see any change.

    The sequence of events is:

    1. You create an instance of TestView – This is initialised with text = "Hello"
    2. You update text which triggers SwiftUI to recreate TestView
    3. The newly created instance of TestView is initialised with text = "Hello"`

    In SwiftUI no object should ever need to hold a reference to a View.

    init() {
        useCase.output = self
    }
    

    The self you store in your TestUseCase instance will be thrown away as soon as the view is updated. It simply isn’t useful to try and hold references to SwiftUI views.

    You should structure your code so that views respond to changes in your model (Via @Published for example). Your model should never try and update a view directly.

    Login or Signup to reply.
  2. You are mixing up the old Objective-C and reference type related protocol/delegate pattern with modern SwiftUI. That’s not a good idea.

    Use the @ObservableObject/@Published pattern SwiftUI provides. It’s specially designed for SwiftUI lightweight views which are structs unlike UI/AppKit reference type views.

    Instead of creating a custom protocol adopt ObservableObject, move text to TestUseCase and mark it as @Published.

    class TestUseCase : ObservableObject {
        @Published var text = "Hello"
        
        func show() {
            text = "Changed"
        }
    }
    

    In the view make useCase a @StateObject with – together with the ObservableObject protocol – enables the functionality to refresh the view immediately whenever text in useCase changes.

    struct TestView: View {
        @StateObject var useCase = TestUseCase()
    
        var body: some View {
            Text(useCase.text)
                .onAppear {
                    useCase.show()
                }
        }
    }
    

    As you can see it’s much less code and – more important – it’s efficient and it works.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search