SwiftUI Lists & OnAppear - swiftui

I have noticed some odd behavior on OnAppear events for List views. I'd think the OnAppear closure would run whenever a view appears on the screen, but it appears to run all at once when the List is loaded.
For example the following code:
#State var rows: [String] = Array(repeating: "Item", count: 20)
var body: some View {
List(0..<rows.count, id: \.self) { index in
Text(verbatim: self.rows[index])
.onAppear {
print("BOOOOM")
}
.frame(height:400)
}
}
...I would expect the print command to run a couple times on load, then continue to print as I scrolled down. Instead it prints 20 times at once, then again as I start to scroll down.
Any thoughts?

I think it is behaving as expected.
For me it printed 15 times on a Simulator with iPhone 7 and all 20 times on a Simulator with an iPhone 11.
I made a slight change to the print("BOOOOM \(index)")
There is probably a balance with performance and resources that the List abides by in the background.
Load too little and the user will "Get stuck" if scrolling too fast vs loading too much and slowing the scroll animation.

This is has been a thorn in my proverbial claw for a good month now
I can't explain why this is happening, but it's something to do with Lists in iOS 14, and custom cell heights. If you remove .frame(height: 400) you'll notice that "Boom" is printed the correct number of times. When your cell height has been enlarged beyond just a normal Label, iOS will still load the same number of cells, but then backtrack for those off screen (have a look at the .onDisappar() view modifier).
What can you do?
1. Update to iOS 15
This behaviour is correct in iOS 15 and 13, so if you can, just up your minimum deployment target and say goodbye to iOS 14
2. Switch to LazyVStack
True to their name: LazyVStack and LazyVGrid don't do anything more than they have to, and will "Boom" the correct number of times. The downside is that you lose iOS 13 compatibility, and all the automatic layout magic that comes with List
If you have since found a way to make onAppear() work correctly in List for iOS 14, PLEASE let me know

Related

SwiftUI programatic navigation loses binding

I'm building a "Charade-like-game" app with five screens and a simple GameModel class consisting of five objects, including teams, round, and scores.
Screen1 dynamically determines the number of teams playing via #StateObject.
Screen2 is a scoreboard that displays in a VStack the dynamic number of teams playing via #ObservedObject, alongside scores from two rounds of play.
Screen3 shows the Charade criteria, and Screen4 is a timer. Neither of these affect the GameModel.
Screen5 gives the other teams a chance to score the performance. That value is then passed back to the Screen2 scoreboard.
The navigation flow begins at 1 then moves to 2 > 3 > 4 > 5 then back to 2 to repeat the cycle until the users close the app, like this:
I got tangled up in the navigation because I wanted the back button on Screen2 to start the game over, rather than return to Screen5. When I used #Environment(\.presentationMode) and presentationMode.wrappedValue.dismiss(), it didn't navigate properly.
Therefore I tried building the navigation programmatically using a #State Boolean, to change the screen like this:
if playGamePressed {
Screen2View()
} else {
Screen1 code...
Select # of teams binded via #StateObject (my source of truth)
Button(action: {
playGamePressed = true
}
}
Now, the #StateObject binding no longer passes the dynamic number of teams. Everything worked prior to changing the navigation to programmatic.
I used this as a reference: Swiftui nested navigation issue while using navigation link, but the solution was .presentationMode that didn't work for me.
What am I missing?

SwiftUI, Alert disrupts NavigationLink?

previous threads on this question have non-swift answers that I can't figure out how to implement in SwiftUI. Please note that I'm using Xcode 12.5.
I created a playground in an attempt to post a "demo" version of the code, but wouldn't you know it, the playground runs flawlessly. So, going to post little bits of the code without making your eyes bleed. I'm getting this warning:
pushViewController:animated: called on <TtGC7SwiftUI41StyleContextSplitViewNavigationControllerVS_19SidebarStyleContext 0x103021c00> while an existing transition or presentation is occurring; the navigation stack will not be updated.
Here's how the code operates:
Navigate from View A to View B. In View A's NavigationLink, I run this:
NavigationLink(destination: MainScreen(user: user)) {
CapsuleButtonView(txt: "LOGIN")
}.simultaneousGesture(TapGesture().onEnded {
user.login()
})
.padding([.leading, .bottom, .trailing])
user.login() checks to see if the username and password are entered (bindings). If not, it flips a #Published Bool which the view that runs the above code uses to trigger the alert message:
.alert(isPresented: $user.alert, content: {
Alert(title: Text("Message"), message: Text(user.alertMsg), dismissButton: .destructive(Text("Ok")))
})
The Alert pops as it should, but I think it does it after the NavigationLink has started to navigate away from View A to View B. So, after the Alert pops, the NavigationLink's simultaneousGesture block still runs the code in it (I checked with print statement), but does NOT navigate to View B.
Thanks for your help guys. After 3 days of looking a this, I'm about to tear out what little hair I have left.
Try adding navigationViewStyle(.stack)
NavigationView {
//Your view
}
.navigationViewStyle(.stack)

iOS 14 AssistiveTouch Dwell Control sometimes not working on SwiftUI Buttons

I have a SwiftUI App which uses regular Button views and our customers use AssistiveTouch's DwellControl feature quite often. On iOS 13 DwellControl was working fine but on iOS 14 beta 8 DwellControl fails to be able click on those buttons.
The Buttons can be tapped by finger and clicked with the normal mouse but not with the DwellControl feature.
I have tried adding accessibility traits like isButton, but nothing works.
Has anyone encountered the same problem and has a solution or any insight at all? I know this is quite niche but any help is appreciated!
EDIT:
I have tested it with a very simple example, including one a Button and it works fine. It seems to be a side effect of some sorts in my specific App.
I have several ZStacks which show and hide some views like modals and popovers. Could this be the source of failure?
What I don't get is that the Buttons can be tapped and clicked... If a view was blocking the Button then this shouldn't be possible right?
I have tried Release and Debug builds which does not make a difference.
When DwellControl is activated and the cursor is on the Button that shall be clicked, tapping doesn't work either. But when the cursor is nowhere near the Button, tapping works fine.
I have send a report via Feedback to Apple.
EDIT:
I found the cause. ScrollView somehow prevents all DwellControl clicks from happening. That happens whenever a ScrollView is somewhere present in one of the Views.
Minimal example:
struct ContentView: View {
#State var show = false
var body: some View {
ZStack {
ScrollView {
Button(action: {
print("This should be printed but isn't....")
}, label: {
Text("Button in ScrollView")
})
}
}
}
}
I know its niche but maybe someone needs this.
There are other components which are affected and cause the same behaviour:
ScrollView
TextField
Slider
Stepper
UIViewRepresentable
For me this is a big issue, since our customers use DwellControl quite often.
EDIT:
This can be replicated in the newest iOS 14.2 beta 1:
import SwiftUI
#main
struct TempSwitUIApp: App {
#State private var text: String = "I am a text"
var body: some Scene {
WindowGroup {
Button(action: {
print("\(UUID()) \(self.text)")
}, label: {
Text("Print text to the console")
})
TextField("Hello", text: self.$text)
}
}
}
I updated my issue with Apple but haven't heard from them yet.

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.