SwiftUI Xcode 12.5 with very basic NavigationView layout Issues - ios

I have been developing iOS for about a decade and every time I try to take the dive into SwiftUI I spend more time than ever wrestling what should seemingly be a simple task. While working on an app with very simple navigation setup I kept seeing two errors in the console: Unable to present. Please file a bug. and Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVVS_22_VariadicView_Children7ElementGVS_18StyleContextWriterVS_19SidebarStyleContext___: 0x7fd913d0bd90>. The first happens when there are at least 3 Views to navigate from (I don't understand why this is a significant threshold and my end goal uses a LazyVGrid with a ForEach) with navigation links and the second error happens on rotating to landscape and then back to portrait. I believed this to be related to how the phone is presenting the sidebar but even changing to StackNavigationViewStyle produced similar problems.
If this were a UIKit application I can absolutely solve for the Unbalanced calls situation but SwiftUI really takes away some of the lower level capabilities that I am used to having control of when it comes to building Views and navigation stacks.
I finally attempted to just start a new project from scratch and place the minimal amount of code in the ContentView:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Number 1")) {
Text("Number 1")
}
NavigationLink(destination: Text("Number 2")) {
Text("Number 2")
}
NavigationLink(destination: Text("Number 3")) {
Text("Number 3")
}
}
}
}
}
Running the above code produced the issues on rotation. Another issue that I came across was when you press the < back button after these issues start, the navigation stops working completely and the "detail" view never gets updated.
It is clear that the direction is SwiftUI so I am trying to really go this direction (maybe next week will have some amazing improvements) but this seems to be a pretty significant issue on a very simple set of code. I am hoping that I am just doing something wrong that someone can point out quickly.
EDITED:
After more exploration I found that the second error happens on the 11, 11 Pro Max, and 12 Pro Max where the navigation stack changes to the sidebar by default. Changing the style to StackNavigationViewStyle does eliminate the second error (but doesn't help if I do want to use the sidebar style) but the first error remains. To Schottky's point, changing VStack to List will also eliminate the first error. However a newer convention (based on WWDC videos at least) to solve for collection views I believe is to use a LazyV/HGrid with a ForEach within it which is actually what I am attempting to accomplish. I didn't put that as my code here since I wanted the be able to reproduce the error in the simplest form of course to ensure it wasn't something buried in my view hierarchy.

The way I was able to accomplish this was via a post I found at HackingWithSwift here. I still had to use .navigationViewStyle(StackNavigationViewStyle()) to eliminate a different error as it seems that the initial iterations of SwiftUI focuses on the 80% of cases where multiple NavigationLinks only really exist in the List View type. I have not tested this with the latest beta but if you try to use the other navigation style (Column) it will throw another error: unbalanced calls to begin/end appearance transactions. I am okay with this for now as this at least allows me to create a screen that does not have line separators and navigation arrows....I just don't have the Master-Detail setup by default.

Related

Why am I getting this: [SystemGestureGate] <0x102210320> Gesture: System gesture gate timed out

My iOS Swift 5 (Xcode 14.1) app is humming along. After the last Xcode update, I noticed that when I push a particular view controller with a table view inside a stack, inside a navigation controller, I get the following warning:
[SystemGestureGate] <0x102210320> Gesture: System gesture gate timed out
When I swipe back to pop the troubled view controller, it acts erratically, and sometimes pops just fine. By "erratic", I mean sometimes it partially displays the destination view controller, sometimes freezes in the middle of the swipe for a second or two, etc. When I try to push again into the troubled view controller (after the warning occurs), it freezes the app.
The view controller, stack, table, and cells do NOT have a custom gesture recognizer.
I have not seen this warning anywhere else on the app (including view controllers with stacks, and tables).
My questions are:
What is "SystemGestureGate: System gesture gate timed out"? I did not find substantive information online on this error.
What are the condition(s) that would yield such a warning?
I suspect there is something wrong with the table I'm using and am investigating further.
Some troubleshooting steps I took:
When I remove the table completely, the warning and behavior goes away.
When I remove the table partially (a section or two), the warning and behavior randomly occurs (I couldn't find a pattern).
I placed breakpoints and noticed that the error occurs after viewWillAppear, but before viewDidAppear.
I am expecting that the view controller will pop by swiping in the same fashion as all the other view controllers in a navigation controller.
I ran into the same error today. It seems to occur upon long-pressing a button (or maybe any gesture-handling element) near the bottom of the screen.
Here's a dumb view I tested with:
struct vertButtons: View {
#State var i = 0
func action() {i=(i+1)%10}
var body: some View {
VStack {
Text(String(i))
.font(.title)
Spacer()
Button(action: {action()}) {
Text("Good Button") // This one is fine
.font(.title)
}
Spacer()
Button(action: {action()}) {
Text("Good Button") // This one is fine
.font(.title)
}
Spacer()
Button(action: {action()}) {
Text("Good Button") // This one is fine
.font(.title)
}
Spacer()
Button(action: {action()}) {
Text("\"Bad\" Button") // Long-press produces "gesture gate timeout"
.font(.title)
}
}
}
}
Long-pressing the "Bad" button produces the error ("Gesture: System gesture gate timed out") while doing the same with any of the "Good" buttons does not. The counter text is there to demonstrate that the error message does not interfere with the button action being executed upon release of the long press.
My best guess is that iOS includes or included some feature (the "system gesture gate") designed to prevent interference between app gesture handling and special system gestures in the bottom part of the screen, and that a recent iOS update introduced a bug in that feature that's causing it to time out instead of doing what it's supposed to do.
The error had no effect on expected functionality in either this test view or my actual app, so I wonder if your woes have some other cause.
I wish I could give a more authoritative answer, but hope this was helpful. Will caveat that I'm very new to Swift and SwiftUI.

SwiftUI: Conditionally hide a view without recreating it

The issue with Conditional View Modifiers
I made heavy use of conditional view modifiers in SwiftUI until I had some issues with it and recently discovered it is a bad idea.
From my understanding now, doing something like this:
if condition {
view
} else {
view.hidden()
}
means that SwiftUI will treat view in both cases as completely different views. So if you jump from if to else or vice versa, view is recreated, including running onAppear and any initial animations. Besides that, it also breaks animating view from one branch into the other. Even adding .id() doesn't seem to solve this.
I attached a minimum viable example so you can see this behaviour for yourself. And needless to say, this is bad. It has major performance implications, breaks animations and is even worse if you perform some heavy operation (like a network request) in your onAppear.
The solution?
The solution appears to be to do something like this:
view.padding(condition? 10 : 0)
Everything works peachy, SwiftUI keeps view around, great.
The problem: .isHidden()
Now, I need to conditionally hide a view. And to be honest, I have no idea how to achieve it. .isHidden() has no parameter to enable or disable it, so there is no way to use the same approach as shown above.
So: How can I conditionally hide a view without recreating it? The reason I don't want to recreate the view is that onAppear gets called again and it reset the state of the view, triggering animations to the initial state again. Or am I using the wrong approach entirely in SwiftUI world here?
A possible approach is to use opacity+disabled, like
view
.opacity(isHidden ? 0 : 1)
.disabled(isHidden ? true : false)
In SwiftUI a View struct is just data, SwiftUI is what diffs the data and decides what actual UIViews to add, remove, update on screen. So if you want to hide something just give it empty data, e.g.
Text(showText ? "Some text" : "")
It would be helpful to know what data it is you are trying to show/hide.

iOS15 - First NavigationLink doesn't work if onAppear of new view updates ObservedObject

This is a really weird issue that I haven't been able to resolve.
It happens only on iOS 15 and only the 1st time I try to open this specific NavigationLink.
I have 3 SwiftUI Views:
ContentView (entry point)
DetailView
ItemView
ContentView is a list of DetailViews and in detail views there can be any amount if ItemViews.
I have an ObservableObject with a Published property count, that is only accessed from ContentView. In ItemView I have function that's triggered by onAppear, that updates count in the ObservedObject.
When I open an ItemView for the first time after opening the App, it moves to the view but immediately goes back to DetailView. It doesn't happen, when I open it again, even though count is updated again.
I have spent a lot of time debugging this issue, but I'm out of ideas. I have tried reproducing it in a Swift Playground, but everything I've tried there seems to work as expected.
I hope someone has an idea on how I can resolve this issue, I would be most grateful!
If you are using a nav link, I would highly suggest using a custom one instead. I can't understand why this is only happening in ios 15 for you, but I have had a similar bugs with the same views/logic in previous OS versions.

SwiftUI: Putting a LazyVStack or LazyHStack in a ScrollView causes stuttering (Apple bug??)

XCode Version 12.4 (12D4e)
I have encountered this every time I have implemented a Lazy stack within a ScrollView:
Add a LazyHStack to a horizontal ScrollView or a LazyVStack to a vertical ScrollView
Add enough content such that the content size of the scroll view exceeds its bounds
Scenario 1 - Pull the scrollview beyond the bounds (as if you were pulling to refresh)
Expected behavior: It behaves as expected where the scrollview stays under your finger
Observed behavior: It stutters and jumps
Scenario 2 - Scroll fast to the edge so that it has to bounce
Expected behavior: It bounces smoothly
Observed behavior: It stops and jitters when it reaches the edge, but doesn't bounce
My theory
My theory is that due to using a Lazy stack, when a view goes off the screen it gets removed from the view hierarchy, creating a stutter.
I'm wondering if anyone else has encountered this? Is this a bug in SwiftUI? I've reliably reproduced this for months across different projects and end up resorting to not using Lazy stacks which I wish I could.
Sample code
ScrollView {
LazyVStack {
ForEach(viewModel.items) { items in
SomeView(viewModel: .init(context: viewModel.context, item: item))
}
}
Note: Stutter only happens at the top of the scroll view
** Updated July 10, 2021 **
This is still happening in iOS 15, Version 13.0 beta (13A5155e).
In the video below, notice the behavior of the scrollbar and the stuttering when we get to the bottom:
https://youtu.be/z2pybl5yYqk
** Updated July 19, 2021 **
I ripped everything out in my view and built it back up one by one — the LazyVStack begins to stutter as soon as I put a VStack/HStack/ZStack around a simple Text element.
If I add fixedSize(horizontal: false, vertical: true) to the Text element it seems to stop stuttering. As soon as I add a UIViewRepresentable of variable height, it starts to stutter again.
It seems like in a LazyStack, every child needs to be some sort of fixed size or a purely SwiftUI view to work.
I'll keep digging in. Must... solve...
I got a response from the DTS, and they confirmed it’s a bug, but no workaround. You can reference my feedback ID and file a feedback item. I imagine they’ll address it with the new swift version, because I think it’s probably a legacy defect, and might cause breaking changes. In other words, it has to do with the native components and navigation bar, and they have to break some things to fix it. This means SwiftUI apps in iOS 14 might not be compatible, ever. But I’m just speculating. I’ll let everyone know if I get any news. It’s really a major blocker, and completely ruins the user experience IMO.
I can confirm the stutter issue is still present in iOS 15 with Xcode 13. I am not sure what is causing it, but it seems related to the way LazyStacks create and layout items.
This is a MWE that reproduces the problem:
ScrollView(.horizontal) {
LazyHStack {
Color.red.frame(width: 450)
Color.green.frame(width: 250)
Color.blue.frame(width: 250)
}
}
.frame(width: 350)
In this example the frame of the red color seems "wide enough" to cause the stutter when bouncing on the leading edge of the screen.
Reducing the width slightly makes the stutter go away:
ScrollView(.horizontal) {
LazyHStack {
Color.red.frame(width: 400)
Color.green.frame(width: 250)
Color.blue.frame(width: 250)
}
}
.frame(width: 350)
Note: tested on an iPhone Xs Max with Xcode 13 beta 1 and iOS 15 beta 1. For this particular example the issue only happens on the device (maybe because I cannot scroll fast enough on the simulator). But I've had this issue on more complex views on the simulator too.
I added a clear rectangle
ScrollView {
LazyVStack {
Rectangle().foregroundColor(.clear).frame(height: 1.0)
...
}
}
and it seemed to solve most of my problems I have with stuttering in the LazyVStack under ScrollView.
This still seems to be an issue on IOS 16. I am unable to fix it on the simulator but the answer by user14518353 seems to work on a device. For those who cannot find the mentioned users answer add this to onAppear or init -
UIScrollView.appearance().bounces = false
EDIT:
While it mostly seems fixed i have managed to reproduce the issue once with the above fix & adding Rectangle().foregroundColor(.clear).frame(height: 1.0) to the top of the LazyVStack seems to also help. Currently I recommend setting both bounces and adding in the rectangle as that combination seems to be the best fix for me.
I had a very similar issue and disabling the edgesForExtendedLayout worked for me.
If the SwiftUI view is used within a UIHostingController you could try adding the following to the UIHostingController subclass:
class MyViewController: UIHostingController<MyView> {
init() {
let view = MyView()
super.init(rootView: view)
}
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
}
This was reproducible under xcode 13.2.1 and iOS 15.2
I have confirmed that the issue is resolved in iOS 15. Unsure if this helped, but also recompiled the app with Xcode 13. 🚀
Try disabling the ScrollView bounce.
Add the below line in onAppear or init,
UIScrollView.appearance().bounces = false

A single component's animation is messing up the whole view — SwiftUI

I'm building a SwiftUI app for my wife. Her avatar pulses with a repeating animation. It looks cool and seems to work just fine on its own. This is a component that lives in its own SwiftUI file.
When I bring a bunch of the components together to create a view, however, the animation messes it all up.
What a mess, right?
I assumed this happened because I did not define the width of the view. I used the fix from this issue, but it didn't make a difference.
I'm feeling pretty stuck here. Here's the repo for the project, with the code for this view. Does anyone have an idea for how I can fix this animation issue?
You need to remove the animation(nil) in the AvatarComponent
You also need to change all .frame(width: 352) to .frame(width: UIScreen.main.bounds.size.width)
The answer from E.Coms above was very helpful! But instead of replacing the width (325) of all my UI elements, I appended .frame(width: UIScreen.main.bounds.size.width) to the top-level wrapper. It seems to work just fine now!

Resources