SwiftUi - prevent update - swiftui

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{}
}
}

Related

Is SwiftUI analogous to React when it comes to rendering? does it renders only the pieces that needs change?

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.

How to use custom view transitions in SwiftUI when navigating?

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()

Hide statusbar in swift ui, when .statusbar(hidden: true) does not work or react

I have an app that loads a tabbar with a view as initial screen, RecipeList(). Inside of RecipeList I call another view to show a recipe full screen. In RecipeList file I have code to show or hide the status bar checking if the recipe detail fullscreen view is loaded or not. It works perfectly if I preview it in xcode, BUT when I preview the code below, which is my Home() view file, and what I want to load as initial screen due to tabbar need, THEN the code inside of RecipeList to show or hide statusbar doesnt work anymore, and status bar is always on.
If i try to hide the statusbar in the code below, it works, but then is always off, something i dont want. Only wanna hide it for the fullscreen view.
I actually used this Introspect package from Github to hide the tabbar when the child view is loaded full screen, and i made it work!
SwiftUI hide TabBar in subview
https://github.com/siteline/SwiftUI-Introspect
Actually, I wonder if anyone has used Introspect to hide the statusbar like the tabbar. I tried to use it, but I am a rockie, I only know a bit of SwiftUI, no Swift, no view controller experience, nothing.
But I have a totally functional app with only this issue, and I am super frustrated not to have the skills to know why the tabbar view is forcing a persistent status bar.
Any help, please?
var body: some View {
ZStack {
Color("background2")
.edgesIgnoringSafeArea(.all)
TabView {
RecipeList().tabItem {
Image(systemName: "book.fill")
.font(.system(size: 24, weight: .bold))
Text("GalerĂ­a")
}
PostList(section: sectionData[0]).tabItem {
Image(systemName: "list.bullet")
.font(.system(size: 22, weight: .bold))
Text("Listado")
}
}
.accentColor(Color("accent"))
.introspectTabBarController { tabBarController in
// customize here the UITabBarViewController if you like
self.viewModel.tabBarController = tabBarController
}
}
}
I can suggest you using the #EnvironmentObject wrapper which basically allows you to use an object as global state. You can find fair amount of tutorials explaining how to do that and inject it in your initial view so that this object is accessible in the whole view hierarchy.
Once you have that global state set up, you can hide your status bar conditionally like this:
MyOutterWrapper {
Text("Some text")
}
.statusBar(hidden: myGlobalState.statusBarHidden)
If you are using NavigationView note that hiding the status bar works best if you set it up there (also assuming navigation view is your first view that appears).
Now all you got to do is inside your view set the variable to true when entering full screen and set it back to false when exiting. Hope that helps!
EDIT: Forgot to mention that hiding status bar as of June 26, 2020 only works if it's set on the initial view. You cannot change it later and that's the reason we set up this variable in order to go back and change the value dynamically.

SwiftUI and EmptyViews

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.

What is the equivalent of WKInterfaceController.didAppear() in SwiftUI on watchOS?

I'm building an Apple Watch app, and there is code I want to run every time the app is brought to the foreground.
Previously, if I wanted to do this in a watchOS with a WKInterfaceController, I would put this code in didAppear().
In SwiftUI, there is onAppear(), but when I call that on watchOS it only seems to be called the first time the app loads up, so it behaves like WKInterfaceController.willActivate() instead. The app has just a single view.
If onAppear() is the equivalent of WKInterfaceController.willActivate(), is there a different SwiftUI function that is the equivalent of WKInterfaceController.didAppear()?
Here's my current example code:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello World").font(.footnote)
.onAppear {
print("onAppear called")
}
}
}
In the meantime, I am going to experiment with triggering what I need to do within the ExtensionDelegate, but I'm just trying to learn my way around SwiftUI on WatchOS, so knowing the answer to this would be helpful in the future.