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
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.
Basic question: how can I prevent a SwiftUI subview to redraw when the parent redraws?
I am experiencing performance issues on my project because so many nested views are constantly redrawn. Now, so of them are redrawn without anything being changed. To debug my problem, I even tried to simplify the problem to its core:
struct StupidView: View {
init() {
print("redraw")
}
var body: some View {
ZStack{}
}
}
This empty view, of course, does not have any moving part that requires redrawing, but it gets redrawn every time its parent is redrawn.
I even tried to add a .id(1) to it with no results. Of course, my problem is with complex views whose useless redrawing slows down the app. How to not redraw parts of a view?
Initializing a View has no meaning of rendering!
It does not rendered but got initialized try this down code, in SwiftUI initializing a View is pretty normal thing! A View could get Initializied 100 times but it will rendered if it needs! if your View has complex Content that SwiftUI get confused we can help SwiftUI in this way that make our View Equatable, then SwiftUI will understand when really need to rendered!
struct StupidView: View {
init() {
print("initializing!")
}
var body: some View {
print("rendering!")
return ZStack{}
}
}
This is from Apple documentation on the SwiftUI App protocol:
#main
struct Mail: App {
#StateObject private var model = MailModel()
var body: some Scene {
WindowGroup {
MailViewer()
.environmentObject(model) // Passed through the environment.
}
Settings {
SettingsView(model: model) // Passed as an observed object.
}
}
}
Why do we need to use the #StateObject propertyWrapper in this case? Why isn't a normal property enough?
I suspect that the "App" struct is a configuration object, just like views in SwiftUI? And that we can't count on that struct to hang around once the body has been read? Correct?
The main reason is for previews, models that are initialised with #StateObject are not initialised. WWDC 2020 Structure your app for SwiftUI previews
8:19
Secondly, the model changes, #StateObject will allow SwiftUI to detect the change and cause the body to be re-computed because model is referenced (twice) within the body, which SwiftUI knows by dependency tracking. This means that MailViewer and SettingsView will be re-created with the new data in the model. Then if anything changed in those View structs, SwiftUI will detect that by diffing the new structs from the ones returned previously and update the screen with whatever changes are needed to bring the screen up to date.
As you say, we can't guarantee structs to hang around, in fact they do not, they are created, the screen is rendered and they are thrown away. That is why we use property wrappers so when the struct is created again it is given the same data to use for the property. In the case of #StateObject the object is created once, the first time the body is computed of the first struct. If a struct is no longer being created, e.g. it is excluded by an if statement, then the object is deinit. If the struct is created again in the future then a new object is created, this is more of a feature for Views than for Apps. This means that the state object's life cycle is tied to the life cycle of the View being shown on screen which is very powerful.
If we were to use normal properties to init objects on SwiftUI structs then those objects would be created every time a struct is re-created which is a heap allocation which fills up RAM and slows down SwiftUI and should be avoided at all costs.
According to Apple #StateObject guaranties that model in
#StateObject private var model = MailModel()
will be created only once. That's all difference from #ObservedObject. So if that is not important for you (or not the case) and you don't need to observe it at that level then you can use regular property declaration.
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 have a TabView and I want to do animated switching between pages, like this:
https://stackoverflow.com/a/54774397/5376525
Is it possible for SwiftUI?
I actually just implemented this using SwiftUI. Here's what I did:
1) Create a SwiftUI view that conforms to UIViewControllerRepresentable. I used the init method to provide it an array of SwiftUI views of type AnyView. There is some work to do in the makeUIViewController and the updateUIViewController methods so we'll come back to that.
I created a typealias to pass a tuple of Views, their Image Name (assuming you're using system images) and the View name. It looks like this:
typealias TabBarItemView = (ViewName: String, ImageName: String, TargetView: AnyView)
2) You'll need to create a class that conforms to the UITabBarControllerDelegate. Within that delegate, you can override the tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning method.
Part of this requires you to create another class, one that conforms to UIViewControllerAnimatedTransitioning. This class requires you to implement two functions: transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval which just needs you to specify how long you want the animation to last.
The second function is animateTransition(using transitionContext: UIViewControllerContextTransitioning) which requires you to do some heavier lifting. Within this function, you'll need to design the animation that moves the views. Essentially, you should be applying a CGAffineTransform that moves the view from off screen (imagine a stage to the left or right of the screen) onto the screen while moving the other view in the same direction off-screen. At the end of the function, you can animate your transforms using something like:
UIView.animate(withDuration: animationDuration, animations: moveViewsClosure, completion: cleanUpClosure)
where animationDuration specifies how long this will take, moveViewsClosure is applying the transforms, and cleanUpClosure is actually replacing the views with one-another.
Once you have the class conforming to UIViewControllerAnimatedTransitioning you should be able to return it as the output of the UIViewControllerAnimatedTransitioning function.
3) Now that you have your delegate and animation classes created, you can assign the delegate to a UITabViewController within the SwiftUI view we started in. At the top of the struct, I created a variable of type UITabViewController and used the default initializer. Within the init function, you should set the delegate to an instance of the delegate class we created above.
4) Now we can implement the makeUIViewController and the updateUIViewController functions. Within makeUIViewController you'll need to load the array of views using the UIHostingController to enable your SwiftUI views to sit in the UIKit view controller. Once you have all your views loaded, you can return the UITabViewController from the top. Within the updateUIViewController, you will likely need to reset the delegate to the view controller. Because SwiftUI is using structs, it is not uncommon for your view to get recreated as it is updated; I have found it will lose the reference to the delegate as a result and this was how I solved it.
I realize I could have provided all of my code, but it's fairly lengthy and I think you'll have an easier time troubleshooting it if you understand the process.