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
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 ofTestView
. But, the new instance hastext
set toHello
(and it was also have a new instance ofTestUseCase
) so you don’t see any change.The sequence of events is:
TestView
– This is initialised withtext = "Hello"
text
which triggers SwiftUI to recreateTestView
TestView is initialised with
text = "Hello"`In SwiftUI no object should ever need to hold a reference to a
View
.The
self
you store in yourTestUseCase
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.You are mixing up the old
Objective-C
and reference type related protocol/delegate pattern with modernSwiftUI
. That’s not a good idea.Use the
@ObservableObject/@Published
patternSwiftUI
provides. It’s specially designed forSwiftUI
lightweight views which are structs unlikeUI/AppKit
reference type views.Instead of creating a custom protocol adopt
ObservableObject
, movetext
toTestUseCase
and mark it as@Published
.In the view make
useCase
a@StateObject
with – together with theObservableObject
protocol – enables the functionality to refresh the view immediately whenevertext
inuseCase
changes.As you can see it’s much less code and – more important – it’s efficient and it works.