SwiftUI programatic navigation loses binding - swiftui

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?

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.

SwiftUI: implement fae-in/out animation when inserting/removing items to list

I wonder if it's possible to customize List animation when inserting/removing/etc. items? The default animation is based on the diff result. What I'd like to have, however, is fade-in/out, just like the default transition animation between two lists.
Background: in my app I have multiple lists and user can select any one to show. Usually this should be implemented by using switch statement or if/else statement or id() modifier and SwiftUI's default transition animation should just work fine. However, due to this bug, I have to implement them by using a single list and swap in/out list content based on the current logical list user selects. Since they are different lists logically, I don't want the default diff based animation, instead I'd like to have fade-in/out animation.
I have tried to emulate the fade-in/out animation as below. While the code works, the UI effect is not satisfying. The root cause is that, to implement fade-in/out animation, I need to get the snapshot of the old list (that is, the fade-out animation should be implemented before the list was changed), which is impossible in hack like this.
MyList(listContent: selectedList)
.opacity(listOpacity)
// This emulates fade-out.
.onChange(of: selectedList) { newValue in
withAnimation {
listOpacity = 0
}
}
// This emulates fade-in.
.onChange(of: listOpacity) { newValue in
if newValue == 0 {
withAnimation {
listOpacity = 1
}
}
}
As far as I can tell, there isn't an API to customize list animation. But just in case, does anyone have suggestions on how to implement it? Thanks.

SwiftUI Lists & OnAppear

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

Button visibly clicking but event not registering

Swift 3/iOS 10/Xcode 8
I have a view controller (pieChart) that contains a label, two buttons and an empty view (which will contain a pie chart). The label and two buttons are incorporated into a horizontal stack view, which lies above the pie chart view.
The above VC is embedded into one of four container views (The main screen of the app is comprised of these four container views) when the app starts up.
In pieChart, I have linked both buttons up to their respective IBActions and IBOutlets. When clicking on button 2, a modal segue should occur to another VC but this does not happen. Visibly, the button is registering the click - ie it changes colour when you click it. I have placed a print statement in the IBAction method for button 2 but this too does not display. No error messages are displayed in console either.
The only reasons for this occurring that I have found after several hours of hunting are:
sub views have been added to the button itself so the click event signal passes by the button to be received by the added sub views. This is not the case for me. Order is Main App Window > Container View > Embedded VC > Stack View > Button. InteractionEnabled is set to true for all.
Button lies partially outside containing view (it's height/width might be greater than its containing view). This is not the case for me. The stack view, label and both buttons share the same height and the width of the stack view is equal to the sum of the widths of the label and buttons plus the spacing between the label and buttons.
I have also tried adding an event handler programmatically with:
SelectAnalyisButtonOutlet.addTarget(self, action: #selector(SelectAnalysisButtonClicked), for: .touchUpInside)
but the same outcome occurs.
Are there any other reasons for the click events seemingly not registering?
EDIT 1
The pieChart VC mentioned above is one of several VC's that are swapped out of the same container view (called detailContainerView), depending on which button (all of which work just fine) is clicked in one of the OTHER container views (called TabBar).
I placed a button in each of two other VC's that get displayed in detailContainerView and hooked them each up to an IBAction. Each IBAction contains a print statement that fires when the button is clicked. At the moment then, these two VC's only consist of a label and the newly inserted buttons. None of the buttons worked when I ran the app.
I then set one of the VC's of detailContainerView as the Initial View Controller in the Attributes Inspector and re-ran the app. Suddenly the buttons now work! If I then hook the buttons up to a segue, the segues work too!
Something seems to change when I swap out the VC's in detailContainerView. The code I am using to swap the VC's out is as follows:
func SwapOutControllers(vc: UIViewController, vcName: String){
//REMOVE OLD VC
detailPaneVCReference?.willMove(toParentViewController: nil)
detailPaneVCReference?.view.removeFromSuperview()
detailPaneVCReference?.removeFromParentViewController()
var newVc: UIViewController?
switch vcName {
case "Biography":
newVc = vc as! Biography
case "Social Media":
newVc = vc as! SocialMedia
case "News Feed":
newVc = vc as! NewsFeeds
case "Stats":
newVc = vc as! StatsAboutParliament
case "Petitions":
newVc = vc as! Petitions
default:
print("Error: No VC Found!")
}
//ADD NEW VC
ParentVC?.addChildViewController(newVc!)
let width = detailContainerView?.frame.width
let height = detailContainerView?.frame.height
newVc?.view.frame = CGRect(x: 0, y: 0, width: width!, height: height!)
detailContainerView?.addSubview((newVc?.view)!)
newVc?.didMove(toParentViewController: ParentVC)
}
detailPaneVCReference is a reference to whichever VC is currently being displayed by detailContainerView. ParentVC is the VC that contains the four container views.
The VC that is removed from the ParentVC still exists in the debugging view hierarchy after it has been removed/swapped out - could this be somehow blocking the click event from reaching the event handler?
SOLUTION!
The source of my problem has been that the references I had made to each of the view controllers that get swapped in and out of detailContainerView were incorrectly declared as weak references. I deleted "weak" (eg "weak var x: UIViewController?" --> "var x: UIViewController?") from each of the declarations and voila!, the code now works as intended!
The source of my problem has been that the references I had made to each of the view controllers that get swapped in and out of detailContainerView were incorrectly declared as weak references. I deleted "weak" (eg "weak var x: UIViewController?" --> "var x: UIViewController?") from each of the declarations and voila!, the code now works as intended!

Ionic 2 cancel hard BACK button override -> To close app on back button when user is on one of the main Tab pages

I want to have the android back button to close the app if the user is on one of the two main pages. Both pages can be navigated to with two tabs button, which are shown on those both pages. But on any other pages I want to keep normal stack pages behaviour.
I read about registerBackButtonAction and also got some information in this thread concerning Ionic 1.
I created a custom behaviour to close the app:
private registerSpecificActionOnBackButton = () => {
if(this.platform.is('android')||this.platform.is('windows')){
this.platform.registerBackButtonAction(function(e){
this.platform.exitApp();
}.bind(this),101);
}
}
My idea is to call the registerSpecificActionOnBackButton() function in the ionViewWillEnter() function on the pages where this behaviour is needed.
But I don't manage to cancel that behaviour on the ionViewWillLeave() function with a deRegisterSpecificActionOnBackButton() function, I've tried among other things:
private deRegisterSpecificActionOnBackButton = () => {
if(this.platform.is('android')||this.platform.is('windows')){
this.platform.registerBackButtonAction(function(e){return true},101);
}
}
Or
private deRegisterSpecificActionOnBackButton = () => {
if(this.platform.is('android')||this.platform.is('windows')){
this.platform.registerBackButtonAction(function(event){event.unbind()},101);
}
}
But I happen to be stuck. Has anyone any idea about canceling a custom registerBackButtonAction?
I've managed to make this work as I expect: When the app is on one of the pages that can be reached thru the tabs menu, it closes when the back button is hitten (on Android).
First, forget about the registerBackButtonAction() for the moment because as quoting what is explained in this thread of 2016-08-05:
it suggests not trying to override the default back button behavior.
So I've looked for other solutions. I've found one that is not really clean but works.
To begin with, I looked if I could reset the stack with the NavControler using remove(startIndex, removeCount, opts). But that doesn't work out because the two main pages are embeded in the tab page (like it is shown there).
So when you're on one of those pages the NavController is a Tab and the parent of that is a Tabs.
In Tabs there is a Array variable named _selectHistory. The _selectHistory array seems to have a role similar to the stack. So when navigating from one page to another using the two tab buttons, one can see in a console.info(this.[NavControler var of the page].parent._selectHistory) that the array grows as the tab buttons are hitten alternatively. And when trying on a real device, the back button take you back switching from one page to another until the array is empty and then the next back button hit closes the app.
Hence I thought: Let see what happens if I override the value of that array. It cannot be done thru a function to apply on a Tabs object (unlike what is possible with NavController).
So in the Page concerning my pages embedded in the Tab page, I added the following in ionViewWillEnter():
ionViewWillEnter(){
this.navCtrl.parent._selectHistory=[];
}
This.navCtrl is my NavController object passed in the constructor of the page.
That's it.