I'm following the example of "Thinking in Swift UI" book.
I'm creating an Observable Object
final class Contact: ObservableObject, Identifiable {
let id = UUID()
@Published var name: String
@Published var city: String
@Published var profile: String
init(name: String, city: String, profile: String) {
self.name = name
self.city = city
self.profile = profile
}
}
Content View is defined as list of Button which selects the contact. And it stores the contacts as normal array property
struct ContentView: View {
@State var selection: Contact?
var contacts: [Contact]
var body: some View {
HStack {
ForEach(contacts) { contact in
Button(contact.name) {
self.selection = contact
}
}
}
if let c = selection {
Detail2(contact: c)
}
}
}
And here we have Detail View which calls the id modifier to change the identify of the view and supposedly make "onAppear" be called again - which doesn't work
struct Detail: View {
@ObservedObject var contact: Contact
@StateObject var loader = ImageLoader()
var body: some View {
HStack {
loader.imageView
VStack {
Text(contact.name).bold()
Text(contact.city)
}
}
.id(contact.id)
.onAppear {
loader.load(image: contact.profile)
}
}
}
And here I mocked the Loader
class ImageLoader: ObservableObject {
var image: String = ""
init() {
print("Initializer of Image Loader was called")
}
func load(image: String) {
print("Brrr.... loading image")
print("The image is: \(image)")
self.image = image
}
}
The app is defined as Content View which passes the array of contacts:
@main
struct SwiftUILearning2App: App {
var body: some Scene {
WindowGroup {
ContentView(contacts: [ Contact(name: "name1", city: "city1", profile: "profile_1"),
Contact(name: "name2", city: "city2", profile: "profile_2")])
}
}
}
Why doesn't onAppear - and in effect load.loader( ) is not called when the new contact is assigned?
According to the book .id(contact.id)
modifier called on Detail View should be able to make onAppear to be called again.
After the example without the .id(contact.id
modifier the book states:
"Upon first try, this seems to work, but when we change the contact object (for example, by selecting a different person) the view never reloads the image. The simplest way to solve this problem is by telling SwiftUI that it should change the identity of the view:"
Here is a link to full code: https://gist.github.com/pbrewczynski/63f2dfdba96dec2efcf7d81d304622f6
CodePudding user response:
In according with the swiftUI documentation:
When the proxy value specified by the id parameter changes, the identity of the view — for example, its state — is reset.
The view doesn't call onAppear() method because it's not created again, change its state and parameters. My suggestion is to call onChange() method:
.onChange(of: contact) { newValue in
loader.load(image: newValue.profile)
}
There are a lot of solutions, but id modifier is usually used for:
- Resetting State Values
- Triggering Transitions
- Improving the List View Performance
An article here i hope is helpful
CodePudding user response:
A better way to do this is using .task(id:)
, e.g.
.task(id: contact.id) {
downloads = await Contacts.download()
}
It's called on appear and when contact.id
changes. But the best thing is the async task is automatically cancelled if it hasn't finished before the contact changes or it disappears.
If you just want to display then info then store the downloads in @State
. If you want to persist it then you could set it on a @StateObject
that does it later.