I'm looking for a way of observing #State or #Binding value changes in onReceive. I can't make it work, and I suspect it's not possible, but maybe there's a way of transforming them to Publisher or something while at the same time keeping the source updating value as it's doing right now?
Below you can find some context why I need this:
I have a parent view which is supposed to display half modal based on this library: https://github.com/AndreaMiotto/PartialSheet
For this purpose, I've created a #State private var modalPresented: Bool = false and I'm using it to show and hide this modal view. This works fine, but my parent initializes this modal immediately after initializing self, so I completely loose the onAppear and onDisappear modifiers. The problem is that I need onAppear to perform some data fetching every time this modal is being presented (ideally I'd also cancel network task when modal is being dismissed).
use ObservableObject / ObservedObject instead.
an example
import SwiftUI
class Model: ObservableObject {
#Published var txt = ""
#Published var editing = false
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
TextField("Email", text: self.$model.txt, onEditingChanged: { edit in
self.model.editing = edit
}).onReceive(model.$txt) { (output) in
print("txt:", output)
}.onReceive(model.$editing) { (output) in
print("editing:", output)
}.padding().border(Color.red)
}
}
Related
Given
a View with a simple List
an ItemView for each element of the list
a Model for the app
a model value (Deck)
Tapping on a button in the main view, is expected the model to change and propagate the changes to the ItemView.
The problem is that the changes only propagate if the model struct is stored in the ItemView as a normal variable; but if i add the #State property wrapper these do not happen. The view will update but not change (like if the data has been cached).
Question 1: is this an expected behaviour? If so, why? I was expecting to have the ItemView to only update when the model change by observing it throw #State, this way instead the view will always refresh whenever the list commands it, even if the data is not updated?
Question 2: Is it normal otherwise to have the items of a list using plain structs properties as models? Using observable classes would create much more complexity when handling the array in the view model and also make more complicated the List refreshing/identifying mechanism seems to me.
In the example the model does not need the #State, since changes are only coming from outside, in real world i would need it when it's the view itself to trigger the changes?
This is a stripped down version to reproduce the issue (create a project and replace ContentView with following):
import SwiftUI
struct Deck: Identifiable {
let id: Int
var name: String
init(_ name: String, _ id: Int) {
self.name = name
self.id = id
}
}
struct ItemView: View {
// #State var deck: Deck // DOES NOT WORK !!! <-------------------
let deck: Deck // WORKS (first element is updated)
var body: some View { Text(deck.name) }
}
class Model: ObservableObject {
#Published var decks: [Deck] = getData()
static func getData(changed: Bool = false) -> [Deck] {
let firstElement = changed ? "CHANGED ELEMENT" : "0"
return [Deck(firstElement, 0), Deck("1", 1), Deck("2", 2)]
}
func changeFirst() { self.decks = Self.getData(changed: true) }
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
List {
ForEach(model.decks) { deck in
ItemView(deck: deck)
}
Button(action: model.changeFirst) {
Text("Change first item")
}
}
}
}
Tested with Xcode 13 / iPhone13 Simulator (iOS 15)
Question 1
Yes, it is expected because #State and #Published are sources of truth. #State breaks the connection with #Published and makes a copy.
Question 2
If all the changes are outside (one-way connection) you don't need wrappers of any kind for the children when dealing with value types.
If you need a two-way connection you use #Binding when dealing with a struct/value type.
https://developer.apple.com/wwdc21/10022
https://developer.apple.com/documentation/swiftui/managing-user-interface-state
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
I have simple example of code in which i update #State and #Published property with same value (on button click). Now i see that then i assign the same value to #State there is no body call of view. But then i assign to #Published property then there is call on each assign. I know that it might be fast but still there might be an unnecessary calls to body of view.
So should i check that i actually change value of #Published property or there is some other ways?
class Object: ObservableObject {
#Published var state: Bool = false
func setState() {
state = false
}
}
struct ContentView: View {
#State var state: Bool = false
#ObservedObject var obj = Object()
var body: some View {
print(Self._printChanges())
return VStack {
Text("\(String(state))")
Button("Tap") {
self.state = false
obj.setState()
}
}
}
}
Output on tap clicks (3 times):
ContentView: _obj changed.
()
ContentView: _obj changed.
()
ContentView: _obj changed.
()
SwiftUI is declarative, so it could be argued that this specific behaviour is an implementation detail and I would not worry about it.
Just because SwiftUI is calling body does not necessarily mean that your UI is being redrawn.
Indeed, if each view has the same "Identity" with unchanged data, you do not need to worry about additional SwiftUI draw calls.
The system will know nothing has changed and won't incur UI overhead.
You don't need to work on patching around this unless you have specifically profiled this as being a performance bottleneck in your app.
Don't optimize anything unless you have good reason to, otherwise you're just wasting time (as there may be other bottlenecks to work on first).
I've been newly studying SwiftUI.
And since I've seen Data flow over SwiftUI video from Apple explaining difference between #ObjectBinding and #EnvironmentObject, a question has come to my mind.
What does apple mean by :
You have to pass around the model from hop to hop in #ObjectBinding ? (29':00")
Do we have to pass the object using #binding in another views for using them ?
What if we don't use #binding and reference to it using another #ObjectBinding ?
Does that make an inconvenience or make SwiftUI not to work correctly or views not being sync with each other ?
[Edit: note that #ObjectBinding is no longer around; instead you can use #State to mark an instance variable as requiring a view refresh when it changes.]
When a view declares an #State or #Binding variable, its value must be explicitly passed from the view's parent. So if you have a long hierarchy of views with some piece of data from the top being used in the bottom, you must code every level of that hierarchy to know about and pass down the data.
In his comment at 29:00, he is contrasting this to using #EnvironmentVariable in a child view, which searches the whole hierarchy for that piece of data. This means any views that do not explicitly need the data can effectively ignore it. Registering a variable needs only be done once (via .environmentObject(_) on a view).
Here is a contrived example. Given some data type conforming to ObservableObject,
class SampleData: ObservableObject {
#Published var contents: String
init(_ contents: String) {
self.contents = contents
}
}
Consider this view hierarchy:
struct ContentView: View {
#State private var data = SampleData("sample content")
var body: some View {
VStack {
StateViewA(data: self.data)
EnvironmentViewA()
.environmentObject(self.data)
}
}
}
struct StateViewA: View {
#State var data: SampleData
var body: some View {
StateViewB(data: self.data)
}
}
struct StateViewB: View {
#State var data: SampleData
var body: some View {
Text(self.data.contents)
}
}
struct EnvironmentViewA: View {
var body: some View {
EnvironmentViewB()
}
}
struct EnvironmentViewB: View {
#EnvironmentObject var data: SampleData
var body: some View {
Text(self.data.contents)
}
}
The result in ContentView will be two views that display the same piece of text. In the first, StateViewA must pass the data on to its child (i.e. the model is passed from "hop to hop"), whereas in the second, EnvironmentViewA does not have to be aware of the data at all.
Trying to build a simple MacOS app using SwiftUI. I have a View that contains a Picker bound to a State var. As a sanity check I have added a Text Views (the dwarves one and the volumes...itemName) that should also change with the Picker changes. And they do, but the View I want to rerender (the FileList) does not.
I suspect it has something to do with how I am trying to pass the new FileSystemItem (internal class) to the FileList. Like when the FilePanel rerenders the volumeSelection is back to 0 and the state is applied afterwards. So my problem is that I seem to be missing a fundamental concept on how this data is supposed to flow. I have gone through the WWDC info again and been reading other articles but I am not seeing the answer.
The desired behavior is changing the selection on the picker should cause a new FileSystemItem to be displayed in the FileList view. What is the right way to get this to happen? To state it more generically, how to you get a child view to display new data when a Picker selection changes?
struct FilePanel: View
{
#State var volumeSelection = 0
#State var pathSelection = 0
var volumes = VolumesModel() //should be passed in?
var dwarves = ["Schlumpy","Wheezy","Poxxy","Slimy","Pervy","Drooly"]
var body: some View {
VStack {
Picker(selection: $volumeSelection, label:
Text("Volume")
, content: {
ForEach (0 ..< volumes.count()) {
Text(self.volumes.volumeAtIndex(index: $0).itemName).tag($0)
}
})
FileList(item:volumes.volumeAtIndex(index: volumeSelection)).frame(minWidth: CGFloat(100.0), maxHeight: .infinity)
Text(dwarves[volumeSelection])
Text(volumes.volumeAtIndex(index: volumeSelection).itemName)
}
}
}
struct FileList: View {
#State var item : FileSystemItem
var body: some View {
VStack {
List(item.childItems){fsi in
FileCell(expanded:false, item: fsi)
}
Text(item.itemName)
}
}
}
#State is a state private to to the owning view, FileList will never see any change.
If VolumesModel was a simple struct turning FileList.item into a binding (in-out-state) may already work (the caller still needs to turn it's #State into a binding when passing it to the dependent using the $):
struct FileList: View {
#Binding var item : FileSystemItem
...
}
However, it feels as if VolumesModel was a more complex object having an array of members.
If this is the case the above will not suffice:
VolumesModel needs to be a class adopting ObservableObject
VolumesModel's important members need a bindable wrapper (#Published)
FileList.item should be turned into #ObservedObject (instead of #State or #Binding)
FilePanel.volumes also to be #ObservedObject wrapped
Hope that helps or at least points you into the right direction.
When a SwiftUI View binds to an ObservableObject, the view is automatically reloaded when any change occurs within the observed object - regardless of whether the change directly affects the view.
This seems to cause big performance issues for non-trivial apps. See this simple example:
// Our observed model
class User: ObservableObject {
#Published var name = "Bob"
#Published var imageResource = "IMAGE_RESOURCE"
}
// Name view
struct NameView: View {
#EnvironmentObject var user: User
var body: some View {
print("Redrawing name")
return TextField("Name", text: $user.name)
}
}
// Image view - elsewhere in the app
struct ImageView: View {
#EnvironmentObject var user: User
var body: some View {
print("Redrawing image")
return Image(user.imageResource)
}
}
Here we have two unrelated views, residing in different parts of the app. They both observe changes to a shared User supplied by the environment. NameView allows you to edit User's name via a TextField. ImageView displays the user's profile image.
The problem: With each keystroke inside NameView, all views observing this User are forced to reload their entire body content. This includes ImageView, which might involve some expensive operations - like downloading/resizing a large image.
This can easily be proven in the example above, because "Redrawing name" and "Redrawing image" are logged each time you enter a new character in the TextField.
The question: How can we improve our usage of Observable/Environment objects, to avoid unnecessary redrawing of views? Is there a better way to structure our data models?
Edit:
To better illustrate why this can be a problem, suppose ImageView does more than just display a static image. For example, it might:
Asynchronously load an image, trigged by a subview's init or onAppear method
Contain running animations
Support a drag-and-drop interface, requiring local state management
There's plenty more examples, but these are what I've encountered in my current project. In each of these cases, the view's body being recomputed results in discarded state and some expensive operations being cancelled/restarted.
Not to say this is a "bug" in SwiftUI - but if there's a better way to architect our apps, I have yet to see it mentioned by Apple or any tutorials. Most examples seem to favor liberal usage of EnvironmentObject without addressing the side effects.
Why does ImageView need the entire User object?
Answer: it doesn't.
Change it to take only what it needs:
struct ImageView: View {
var imageName: String
var body: some View {
print("Redrawing image")
return Image(imageName)
}
}
struct ContentView: View {
#EnvironmentObject var user: User
var body: some View {
VStack {
NameView()
ImageView(imageName: user.imageResource)
}
}
}
Output as I tap keyboard keys:
Redrawing name
Redrawing image
Redrawing name
Redrawing name
Redrawing name
Redrawing name
A quick solution is using debounce(for:scheduler:options:)
Use this operator when you want to wait for a pause in the delivery of events from the upstream publisher. For example, call debounce on the publisher from a text field to only receive elements when the user pauses or stops typing. When they start typing again, the debounce holds event delivery until the next pause.
I have done this little example quickly to show a way to use it.
// UserViewModel
import Foundation
import Combine
class UserViewModel: ObservableObject {
// input
#Published var temporaryUsername = ""
// output
#Published var username = ""
private var temporaryUsernamePublisher: AnyPublisher<Bool, Never> {
$temporaryUsername
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.eraseToAnyPublisher()
}
init() {
temporaryUsernamePublisher
.receive(on: RunLoop.main)
.assign(to: \.username, on: self)
}
}
// View
import SwiftUI
struct ContentView: View {
#ObservedObject private var userViewModel = UserViewModel()
var body: some View {
TextField("Username", text: $userViewModel.temporaryUsername)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I hope that it helps.