Is there a .onReappear equivalent in SwiftUI - ios

I have this view that has a simple VStack that contains 4 IF conditions, each condition will show the right view:
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
if(showSplash){
Text("Splash screen")
}
if(showNoInternet){
noInternetView(showNoInternet: $showNoInternet, showSplash: $showSplash)
}
}
For this view I have a .onAppear() to run code when this view appears:
#State var showLogin = false
#State var showMain = false
#State var showNoInternet = false
#State var showSplash = true
var body: some View {
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
if(showSplash){
Text("Splash screen")
}
if(showNoInternet){
noInternetView(showNoInternet: $showNoInternet, showSplash: $showSplash)
}
}
.onAppear(){ ... }
}
Within the code of .onAppear() I check if the user has internet connection, if they don't then I toggle both the splash and the showNoInternetView to hide the splash screen and show the No Internet connection message. When the user presses a button in the showNoInternetView, it does the opposite to hide the message and show the splash screen again, which I would like to run the same code again within the .onAppear(). Obviously I know to re-render a view you'd have to change a state - which is what I do to update the views.
How would I invoke the .onAppear() function again each time I go back to the original splash view (as if it's the first time the view is being run)?

Attach it instead to splash view directly, so once you come into condition of splash view it will run as expected.
if(showSplash){
Text("Splash screen")
.onAppear(){ ... } // << move here !!
}

Related

Presence of #Environment dismiss causes list to constantly rebuild its content on scrolling

I need to build a list of TextFields where each field is associated with focus id, so that I can auto scroll to such a text field when it receives focus. In reality the real app is a bit more complex which also includes TextEditors and many other controls.
Now, I found out that if my view defines #Environment(\.dismiss) private var dismiss then the list is rebuilding all the time during manual scrolling. If I just comment out the line #Environment(\.dismiss) private var dismiss then there is no rebuilding of the list when I scroll. Obviously, I want to be able to dismiss my view when user clicks some button. In the real app it's even worse: during scrolling everything is lagging, I cannot get smooth scrolling. And my list is not huge it's just 10 items or so.
Here is a demo example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
DismissListView()
} label: {
Text("Go to see the list")
}
}
}
}
struct DismissListView: View {
#Environment(\.dismiss) private var dismiss
enum Field: Hashable {
case line(Int)
}
#FocusState private var focus: Field?
#State private var text: String = ""
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
Button("Dismiss me") {
dismiss()
}
Section("Section") {
ForEach((1...100), id: \.self) {num in
TextField("text", text: $text)
.id(Field.line(num))
.focused($focus, equals: .line(num))
}
}
}
.listStyle(.insetGrouped)
.onChange(of: focus) {_ in
withAnimation {
proxy.scrollTo(focus, anchor: .center)
}
}
}
}
}
The questions are:
Why is the list rebuilding during manual back and forth scrolling when #Environment(\.dismiss) private var dismiss is defined, and the same is NOT happening when dismiss is NOT defined?
Is there any workaround for this: I need to be able to use ScrollProxyReader to focus any text field when the focus changes, and I need to be able to dismiss the view, but in the same time I need to avoid constant rebuilds of the list during scrolling, because it drops app performance and scrolling becomes jagged...
P.S. Demo app constantly outputs "body is rebuilding" when dismiss is defined and the list is scrolled, but if any text field gets a focus manually, then the "body is rebuilding" is not printed anymore even if the dismiss is still defined.
I could make an assumption, but that would be really rather a guess (based on experience, observations, etc). In a fact, all WHYs like "why this sh... (bug) happens" should be asked on https://developer.apple.com/forums/ (there are Apple's engineers there) or reported to https://developer.apple.com/bug-reporting/
A solution is to separate dismiss depenent part into dedicated view, so hiding it from parent body (and so do not affect it)
struct DismissView: View {
// visible only for this view !!
#Environment(\.dismiss) private var dismiss
var body: some View {
Button("Dismiss me") {
// affects current context, so it does not matter
// in which sub-view is called
dismiss()
}
}
}
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
DismissView() // << here !!
// ... other code

Unable to reshow a sheet if triggered from navigationBar

please help!
I have a simple test code below, to display a sheet when a button is pressed. The issue is when I place the button inside a .toolbar and for the first press the sheet is shown as expected. However, if I were to dismiss the sheet and press the button again, nothing happens and no error is printed out in the console.
If i were to place the button at .bottomBar or withing VStack everything works as expected, I can show and dismiss the sheet multiple times.
I am using Xcode Version 13.2.1 (13C100) and running on iPhone 13 (iOS 15.2)simulator. Haven't tried running on a real device. Video Example
I've been trying to understand what is happening for 2 days and still have no idea what is the cause.
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
VStack {
Button("Show Sheet") {
showingSheet.toggle()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Show once?!") {
showingSheet.toggle()
}
}
}
.sheet(isPresented: $showingSheet) {
Text("Drag to dismiss")
}
}
}
}
Solution:
Press and hold button and then drag it down just a little bit (other directions are no no) till the button is registered as clicked and .sheet will be presented as intended. Ridiculous :D

Three Column Navigation View does not update until you click to show the sidebar on iPad

Given this View:
struct ContentView: View {
#State var link1Active: Bool = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("Link1 Destination"), isActive: $link1Active) {
Text("Click me 1")
}
NavigationLink(destination: Text("Link2 Destination")) {
Text("Click me 2")
}
}
Text("View 1")
Text("View 2")
}
.onAppear {
link1Active = true
print("in here!")
}
}
}
When the app starts on iPad in landscape mode, I see two columns "View 1" and "View2".
I would expect to see "Link 1 Destination" in the left column, but its not shown.
However when I click the toggle sidebar button the "View 1" is replaced.
Is this an issue with the way the views are setup in swiftui?
The problem is simply that the NavigationLink is not in existence when the .onAppear is called. .onAppear sets link1Active to true (I verified this by printing the value in your print() statement in .onAppear), but nothing is using it until the main List view is opened and on screen. I really don't think this is a true "fix", but the only way you are going to show the destination is simple with a conditional like:
(!link1Active ? Text("View 1") : Text("Link1 Destination"))
Since this is a minimal reproducible example, I am not sure of your use case, but I think you need to rethink how you are showing your views.

SwiftUI Changing State does not Dismiss Modal View

SwiftUI Changing State does not Dismiss Modal View
I have a basic master/detail app with SwiftUI life cycle. On the detail page, I
have a button to toggle an #State to present a modal for editing the detail item. I am
using the fullScreenCover modal. The #State variable in the Detail view is passed as
an #Binding to the Edit view.
The Edit view has a "Done" button to dismiss itself. I have tried coding with both the binding
and presentationMode methods.
This all works except that on RARE occasions tapping the "Done" button does not dismiss
the Edit view. I then have to hard close the app and restart. The edits which were made
are still saved as expected. There is simply no way to move from that screen.
The Detail view calls the Edit view like this:
.fullScreenCover(isPresented: $showEditModalView) {//showEditView
InvItemEditView(showEditModalView: $showEditModalView,
invItem: self.invItem,
inputCategory1: self.inputCategory1).environment(\.managedObjectContext, managedObjectContext)
}//full screen
The Done button is coded as this:
Button(action: {
self.saveEditedRecord()
print("Before change - Done button showEditModalView is \(self.showEditModalView)")
self.showEditModalView = false
print("I got passed showEditModalView = false")
self.presentationMode.wrappedValue.dismiss()
print("I got passed presentationMode dismiss")
print("After change - Done button showEditModalView is \(self.showEditModalView)")
}) {
Text("Done")
.font(.system(size: 20))
}//trailing button
.disabled(self.localDisableSaveButton)
.disabled(self.dataStore.pubDisableEditButton)
The saveEditedRecord does just that - it issues the Core Data saveContext.
I can't replicate the error on demand. It just happens occasionally. I was curious to
see that the Button action always executes all lines of code - I had expected it to
terminate once the variable controlling the view presentation changed. I searched for
others who may have had issues with fullScreenCover but found nothing relavant. I added the
print statements to see if there was an issue setting the #State variable. Here is an
example console output:
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate managedObjectContextSaved:](2092): <NSCloudKitMirroringDelegate: 0x2837a8680>: Observed context save: <NSPersistentStoreCoordinator: 0x2827ab4f0> - <NSManagedObjectContext: 0x2837a9790>
in invItemEditView saveEditedRecord, in do after context.save
Before change - Done button showEditModalView is true
I got passed showEditModalView = false
I got passed presentationMode dismiss
After change - Done button showEditModalView is false
When it fails, the first console statement after the Core Data item is:
Before change - Done button showEditModalView is false
Any guidance would be appreciated. Xcode 12.4 iOS 14.4
I guess the problem is that you missed adding the presentation environment in the SwiftUI Modal View.
Here is a complete example in SwiftUI:
struct ContentView: View {
#State private var showEditModalView = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.showEditModalView = true
}) {
Text("Show modal")
}.fullScreenCover(isPresented: $showEditModalView) {
InvItemEditView()
}
}
}
struct InvItemEditView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Group {
Text("Modal view")
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
.font(.system(size: 20))
}
}
}
}
I would also suggest reading this question: SwiftUI dismiss modal

Unwanted SplitView on modal view displayed on iPad

Testing my first SwiftUI app on iPad, I discovered that the modal views I display from my ContentView are displayed as Split views on the iPad, with the UI being truncated on the master side and the detail side is empty.
I did check both posts here :
Unwanted SplitView and,
What's the equality of the UISplitView controller
But their solution of applying the .navigationViewStyle(StackNavigationViewStyle) to the NavigationView does not work for me :
I display my modals through user input (tap of a button) using the following method :
When a button is pressed, an Int value is passed to a local var (modalViewCaller) and then to the sheetContent() function.
Here is the end of my var body: some View and the following sheetContent func :
} // END of main VStack
.sheet(isPresented: $isModalPresented, content: sheetContent)
} // END of body
// modalViewCaller is the Int var I set upon button tap
#ViewBuilder func sheetContent() -> some View {
if modalViewCaller == 1 {
firstModalView()
} else if modalViewCaller == 2 {
secondModalView()
} else if modalViewCaller == 3 {
thirdModalView()
}
} // END of func sheetContent
Then in each of these modalViews, I apply the .navigationViewStyle(StackNavigationViewStyle) modifier to the NavigationView that encapsulate my entire view in var body: some View, but I get the following error :
"Type 'StackNavigationViewStyle.Type' cannot conform to 'NavigationViewStyle'; only struct/enum/class types can conform to protocols"
Here is the end of my NavigationView in the modals :
} // End of VStack
.navigationBarItems(
leading:
Button("Done") {
self.saveEdits()
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
} // END of Button "Done"
)
.navigationBarTitle("Takeoff edition")
} // END of Navigation View
.navigationViewStyle(StackNavigationViewStyle)
.onAppear { // assigned fetched event date, here it is available (was not in init())
self.selectedDate = self.fetchedEvent.first?.eventDate ?? Date()
}
} // END of some View
I guess the solution posted was to apply that modifier from the ContentView NavigationView, but I don't have one (and don't want one because of all the screen real estate lost on top of my UI)
Here is fix (it has to be constructed, ie. StackNavigationViewStyle()):
} // END of Navigation View
.navigationViewStyle(StackNavigationViewStyle()) // << here !!

Resources