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.
Related
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.
So in an iOS app I'm using a UINavigationController as the root view controller. I then push a few ViewControllers on the stack, let's call them A, B, C and D.
Now if I try to pop D from the stack, I sometimes * run into the following problem:
After calling popViewControllerAnimated, the view of the topmost view controller (D) remains. If I display the stack however, it is displayed correctly, i.e. A-B-C is displayed and D is gone. The pop method seems to do just what it's supposed to but the view does not reflect this.
I have tried various other approaches, (popToRootViewController, popToViewController) instead but the behaviour doesn't change (i.e. the stack is printed correctly but D's view remains). I have called setNeedsDisplay on all views that seem to make sense.
After popping D, I can still push and remove other view controllers. The stack reflects those changes, the view does not. Also, D's view remains responsive, i.e. it is not frozen and the app does behave correctly in the background. All views are very simple, there are no tabbed views or the like, just a couple of labels and buttons.
I have read a number of posts on SO and elsewhere about people having similar problems but none of the solutions seem to fit here. Does anyone have an idea why this happens?
*_ I haven't been able to figure out under which circumstances exactly. I first thought it was because I am popping D very soon after pushing it, but it also occasionally happens when there is more time between push and pop.
Only reason i can think of where this might happen is if you are not doing that in a main thread. If you are doing it after some network call, then do make sure you are going back to main thread. Sometimes, app won't crash if you do UI work on background thread but rather will behave like what you are seeing!
The problem seems to have been one of timing. As I mentioned, the push and pop operations (of D) sometimes do happen rather quickly after one another. Adding a delay of 2500 ms solved the problem. Adding 500 ms did not (always). Since neither is acceptable to me, I switched to a custom container view controller and therefore didn't spend a lot of time on figuring out what the minimum delay would be.
Just as sidenote: Changing from dispatch_async to dispatch_sync didn't have any effect. The delay seems to be necessary either way.
Thanks for everyone's help. I'm going to close this thread - if someone can provide a more complete answer as to why things are the way they are, please post it and I'll mark it as the correct answer then.
I want to make a pretty interface. So after reflexion, I decide to make a pattern with to axes but with some essential points. The red screen is the primary screen, all start by this one.
We can only make this two move : red to another color and another color to red. And change are made with GestureRecognizer.
I also want to switch view by seeing the two at the same time. Obviously, it's following my finger and I can stay in this state.
Do you think that 5 viewController witch are all load at the start and we came to these with pretty segue (maybe custom) is possible ? And if yes, do you have an idea of how to make this ?
As a first thought I think you want to make a custom view controller navigation controller. Like UINavigationController, UITabBarController or UIPageViewController.
I'd also use a scroll view over a gesture recogniser maybe?
Either way it's gonna be a lot of work which means there are many many different ways to do it.
It sounds like you have a decent idea of how you want it to work. Give it a whirl and see what happens. I reckon you can have a decent bash at it.
If you get stuck let us know and we'll try to help :)
I know this has been asked before, but none of these solutions work, and that's the reason of my posting. Please do not close before considering my case.
My plist already has UIViewControllerBasedStatusBarAppearance = false.
I have already tried applying deltas, but to no result.
Changing the top level view frame in ViewWillAppear (like self.view.frame) did not succeed.
I thought of increasing the view height (storyboard attribute inspector), in combination with deltas, but my top level view X, Y are disabled in storyboard attribute inspector.
My main view doesn't have any children views because I load them into main view either dynamically or load them from XIBs which are again shared by more than view controllers. These XIBs provide layout for both Portrait and Landscape. I don't know what approach is ideal for this kind of configuration, but I would like it better if solution lies along these lines.
This approach worked partially, but gave me inconsistent results.
What makes the solution tricky is the fact that I have to support all 4 orientations - this is something I handle in code via didRotate and willRotate delegates for my other views, but failing to do it for statusbar.
Please help...
Could this link be of any help?
You might have to use the new setEdgesForExtendedLayout: method to get this working consistently?
Also, have a look at these official docs if you haven't already done so.
I ended up writing my own function to shift my all subviews (remember, not top level views whose frame is fixated by IB).
It didn't spoil my work but imagine if this was the case for a very big project with so many screens, the limitations would have made it a nightmare.
I want to get the keyboard size without using NSNotification. When I press the plus button, it can replace the keyboard with a custom UIView like this:
Then the plus button is pressed and the view loaded:
How can I achieve this?
I already made same rookie mistake like you want to do here. The problem is you will write a lot only to realize you do not want to avoid standard flow provided you by iOS team. For example you will definitely have a bad time dealing with issue like this one (there is additional bar which is part of standard keyboard for Chinese locale):
I solved this by using other people's work from DAKeyboardControl project. You do not need to attach observer (or if you use DAKeyboardControl - block) directly to your bar with buttons, but to your controller and check what user is trying to do and animate this bar accordingly. In the sources you can see how to get keyboard's animation duration and timing function. It may sound more complicated than it indeed is, just give it a try.