In general, variable marked as #State #ObservedObject #StateObject #FetchRequest can cause the UI refreshing. But in a big source, how to identify which variable cause the UI refreshing?
In most cases, onChange will do it, but in some cases, such as coredata, it's hard.
SwiftUI provides a special, debug-only method call we can use to identify what change caused a view to reload itself.
Updated for Xcode 13.0
var body: some View {
print(Self._printChanges())
return Text("Hello")
}
Related
The information I got about react is that even if you have a large component with lots of subcomponents when the value state of one of those components change react is smart enough to update only the part of the component that needs change instead of the whole component thus being really performant during UI re-render,
My question is does SwiftUI works the same way?
If I have a Text() that is updated by #Published property inside an Observed class the value happens to be the same as before will the UI actually re-render?
what if
class StringFetcher: ObservableObject {
#Published var stringA: String = "foo"
#Published var stringB: String = "bar"
#Publisher var showScreenA: Bool = true
}
struct MyView: View {
#ObservedObject var fetcher: StringFetcher
var body: some View {
VStack {
if fetcher.showScreenA {
Text(fetcher.stringA)
} else {
Text(fetcher.stringB)
}
}
}
}
Will a change in stringB publisher trigger an UI re-rendering even if B isn't visible? at the moment?
I couldn't find much resource on how the process works, does anyone know that or know where I could read more in depth about it?
Thank you in advance )
Yes if you supply the same data to a View init as last time (e.g. the same string to Text), SwiftUI will not run the body of that View and thus that part of the View hierarchy diff will not have changed and thus it won't update those UIKit views on screen. However, if you supply the same number to Text and the region changes, then it will run its own body because it also is listening for region changes so it can update UILabel with new number formatting.
Some property wrappers cause body to run every time though, so sometimes you need to do some defensive sub View wrapping to prevent body being called unnecessarily, e.g. #FetchRequest has this problem.
Swift works most efficiently with value types like structs so always try to use those instead of objects. With property wrappers and DynamicProperty you can usually do everything in the View struct, especially now we have async/await and the task modifier.
My problem is SecureField in SwiftUI doesn’t display characters input by the user for any time at all, it just directly shows the '•' symbol for each character as it's typed - whereas in UIKit, UITextField (with isSecureTextEntry = true) shows the latest character for a second before hiding it behind '•'.
UX testers at my company have requested I bring back the "old behaviour" - but this behaviour doesn't seem part of any public API.
Interestingly this goes for UITextField custom classes injected into SwiftUI using UIViewRepresentable too - they behave in the "SwiftUI way" described above. So there's some contextual behaviour modification going on in SwiftUI for all secure UITextField behaviour? I'd have to completely rewrite my SwiftUI form into a full UIViewController to get back the behaviour (modally pushed UIViewControllers with secure UITextFields do exhibit the desired behaviour.)
Is this a sort of sideline bug in SwiftUI? I see the same thing for SwiftUI in both iOS13 and 14. Anyone seen a workaround or solution?
-EDIT-
After #Asperi's great explanation below, I noticed that my UITextField custom classes injected into SwiftUI using UIViewRepresentable were forcing this behaviour by unnecessarily setting the text binding in the updateUIView call. Using a Coordinator only to deal with text logic fixed the problem for me when using this method.
The observed effect is due to immediate apply to bound string state and immediate react/rebuild of view.
To bring desired behavior beck we need to postpone somehow state update and thus give a chance for SecuredField/UITextField to update self without synchronisation with state.
Here is a demo of possible direction (it is not ideal, but a way to go). Tested with Xcode 12.1 / iOS 14.1.
struct DemoSecureFieldView: View {
#State private var password = "demo"
var textBinding: Binding<String> {
Binding(get: { password },
set: { value in
// more logic can be added to delay _only_ if new symbol added,
// and force apply if next symbol came fast
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
password = value
}
}
)
}
var body: some View {
VStack {
SecureField("Placeholder", text: textBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}.background(Color.pink)
}
}
In UIKit-based application we can have custom navigation transitions using UIViewControllerAnimatedTransitioning protocol.
Is there an equivalent in SwiftUI?
I know we can already animate between removing and adding views.
But how do we do this when we push and pop to the navigation stack?
There isn't anything like this available in SwiftUI's APIs so far afaik. Make sure to open an enhancement request with Apple.
Implementing "navigation" on your own is a terrible idea, as you basically give all the accessibility and facility support afforded by UINavigationController.
Instead, here is a suggestion:
Either by means of a custom SwiftUI view or modifier, wrap the NavigationView with a UIViewControllerRepresentable wrapper, which in turn sets up a UIHostingController subclass, which waits for addChild(_:) to be called, which will be the navigation controller added as a child of the hosting controller. From here, if your animations are "static" (e.g. do not require any subview to subview transition), you can accomplish this here by implementing the navigation controller delegate, and providing the animation controller.
If you do need more evolved transitions (such as Photos.app's photo transition), you can create a custom UIViewRepresentable wrapper, which will serve as markers for "from" views and "to" views, you can then use those discover in UIKit, and e.g. can snapshot and animate in transitions.
Proposed API can look like this:
struct ContentView1: View {
var body: some View {
NavigationLink(destination: DetailView()) {
Image("small").customSourceTarget()
}.navigationBarTitle("Navigation")
}
}
struct ContentView2: View {
var body: some View {
Image("large").customTransitionTarget()
}
}
NavigationView {
ContentView1()
}.customAnimatable()
I've just come across an unfortunate issue with my code:
class MyViewModel: ObservableObject {
func load() {
// set up Combine subscriptions, etc.
}
}
struct MyView: View {
#ObservedObject private var viewModel = MyViewModel()
var body: some View {
SomeChildView()
.onAppear { self.viewModel.load() }
}
}
I have many views that are structured this way. (some) State and logic are offloaded to a view model, which is initialized in onAppear.
The problem is that the view may be re-rendered, but the onAppear is only executed once. The result is that anytime the view is re-rendered one or more times, none of the state in the view model is actually reflected in the view, because only the first instance had its load() method called.
There are a few ways around this, but all of them have drawbacks:
I could move the logic in load() into init(), but that seems smelly because init() is for simple things like property assignments, not for setting up subscriptions or other external access. Moreover, I want to be able to override the view model logic for use in tests, and init() can't be overridden. EDIT: The bigger issue here is that we only want to perform the load logic once when the view appears, not every time the view renders.
I could pass the view model in from the parent, but that may suffer from the same problem if the parent is re-rendered.
I could initialize the view model in SceneDelegate and pass it in as an #EnvironmentObject, but the view model is stateful and needs to be reinitialized every time the view reappears, so it would be best and easiest to not have to tie it to some "global" store.
I feel like there's a simple way to achieve this that I'm just missing. Thinking outside the box, the reason I'm using view models at all is for separation of concerns and for ease of unit testing. I want the view to just interact with the interface provided by the view model, and the view model can handle all the messy data access. I'm also not sure if there is another way to subscribe to a Combine Publisher without using an ObservableObject.
Not sure if I've understood your question, but for second option in your list there is a way to ensure that re-render of parent will not destroy the data - you can use #StateObject instead of #ObservedObject there:
#StateObject private var viewModel = MyViewModel()
and then pass it to child's #ObservedObject property
I have the following code example:
struct ContentView: View {
#State var showText = true
var body: some View {
VStack {
if self.showText {
Text("Hello")
}
//else {
// EmptyView()
// }
}
}
}
When it runs I get the text showing as normal. However, if I set showText to false it still compiles and runs as normal even though the VStack contains nothing - it just doesn't show anything when run. If I uncomment the else clause the example also runs as expected. If I remove all the contents of the VStack however, the example won't compile as nothing is returned.
So my questions are:
Is SwiftUI silently adding an EmptyView when nothing is in the VStack when showText is false ?
Should I really be adding the else clause and return an EmptyView ?
The question isn't completely academic either as I have a use case where I would prefer the whole view to be more or less 'thrown away' rather than have EmptyViews in the hierarchy, though my understanding of SwiftUI is pretty limited at the moment so it may not matter.
VStack is a function builder so it expects to get some value back from the closure. In the case of the if it invokes the buildEither version of the function builder so that satisfies the condition that its not empty. See the function builder proposal. Any ways you should not worry about the EmptyViews. A SwiftUI.View is just a blue print to build the actual view (as apple calls it a cheap value on the stack). It is not the real view object in memory and on the graphics card, as with a UIView or CALayer. The rendering system is going to translate your EmptyView into a noop. SwiftUI.Views get created and discarded all the time and are designed to be cheap unlike UIViews, and the system diff's the SwiftUI.View tree and only applies the delta to the real views in graphics memory similarl to how Flutter, React, and Android Compose work.