Navigate back after saving in Swift UI - swift

I have a simple detail view where users can input data. After pressing the save button, I would like the app to navigate back to the previous list view. The detail view is opened through a NavigationLink. I assume the action for the button needs to be adjusted in some way. Any advice is appreciated.

In order to do this you need to grab an Environment variable in your detail view:
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
and then call it's dismiss method in your button's action like this:
self.presentationMode.wrappedValue.dismiss()
A more detailed example:
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var someData = ""
var body: some View {
TextField("Placeholder", text: $someData)
Button(action: {
saveData()
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save data and go back")
}
}
}

Related

Why does my SwiftUI View not update on updating of an #State var?

I am having a strange issue with an #State var not updating an iOS SwiftUI view.
I have an edit screen for themes for a small game with a NavigationView with a list of game themes. When in edit mode and I select one of these themes, I open up an editor view, passing the theme as a binding to the editor view struct.
In my editor view I then have sections that allow the user to edit properties of the theme. I do not want to use bindings to the various theme properties in my edit fields because I do not want the changes to take effect immediately. Instead, I have created #State vars for each of these properties and then use bindings to these in the edit fields. That way, I give the user the option to either cancel without and changes taking effect, or select "Done" to assign the changes back to the theme via the binding.
In order to initialise the #State vars I have an onAppear block that assign the #State vars values from the respective theme properties.
The issue I am having is that when the onAppear block is executed and the vars are assigned, the relevant edit fields are not updating!
Here is a cut-down version of my code:
struct EditorView: View {
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#Binding var theme: GameTheme
#State private var name = ""
...
var body: some View {
NavigationView {
Form {
nameSection
...
}
.navigationTitle("Edit \(theme.name)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancel)
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: saveTheme)
.disabled(!canSaveTheme)
}
}
.onAppear {
name = theme.name
...
}
}
.frame(minWidth: Constants.minViewSize.width, minHeight: Constants.minViewSize.height)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
...
}
So the view gets shown an on appearing, the #State var name does correctly get assigned the value from theme.name; however, this allocation does not cause an update of the view and the value of "name" is not entered into the TextField.
Interestingly, and I do not know if this is a good thing to do, if I wrap the contents of the onAppear block in a DispatchQueue.main.async, everything works fine!
i.e.
.onAppear {
DispatchQueue.main.async {
name = theme.name
...
}
}
Does anyone have any idea as to how, within the onAppear, I can force a view refresh? Or, why the assignment to "name" does not force an update?
Thanks.
This isn't the answer per se, but I went ahead and created a new iOS project with the following code (based on your post, but I cleaned it up a bit and came up with the missing GameTheme object myself).
It's more or less the same, and shows that your posted structure does re-render.
I'm wondering if there's more to the code we can't see in your post that could be causing this.
Are you possibly setting the name state variable anywhere else in a way that could be overriding the value on load?
import SwiftUI
#main
struct TestIOSApp: App {
#State var gameTheme: GameTheme = GameTheme(name: "A game theme")
var body: some Scene {
WindowGroup {
ContentView(theme: $gameTheme)
}
}
}
struct GameTheme {
var name:String;
}
struct ContentView: View {
#Binding var theme:GameTheme;
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#State private var name = "DEFAULT SHOULD NOT BE DISPLAYED"
var body: some View {
NavigationView {
Form {
nameSection
}
.navigationTitle("Edit \(theme.name)")
.onAppear {
name = theme.name
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: {})
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: {})
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
}
I seem to have solved my problem with an init(). I created init(theme: Binding<GameTheme>) and then within the init assigned the theme via _theme = theme and then assigned the name via _name = State(initialValue: theme.name.wrappedValue).

SwiftUI Deep Link Navigation bouncing

I've built a deep linking schema with SwiftUI and have it mostly working. The only issue that I'm suffering from is that when I'm in the same tab and in a navigation stack, a deep link that routes to a different view in the same tab causes this weird bouncing effect where it navigates to the main tab, back to the deep linked view then back out.
in the Main tab view I have this code
.onOpenURL { url in
handleDeepLink(url: url)
}
this goes to the tab and if the deep link is valid it triggers a hidden navigation link
NavigationLink(destination: CompanyPageView(symbol: deepLinkedCompany), isActive: $isDeepLinkingIn, label: { EmptyView() }).hidden()
this works in most cases but for some reason if I'm already in the tab and inside another navigation link in page, opening a deep link does the bounce thing.
How my navState is :
class NavState: ObservableObject {
#Published var firstLevel: String? = nil
var secondLevel: String? = nil
var thirdLevel: String? = nil
}
Subview
#EnvironmentObject var navState: NavState
#Binding var deepLinkedCompany: String
VStack {
//random view stuff
NavigationLink(destination: CompanyPageView(symbol: deepLinkedCompany), tag: "hoboken" , selection: $navState.firstLevel, label: { EmptyView() }).hidden()
}
CompanyPageView
#EnvironmentObject var navState: NavState
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
//view stuff
.onOpenURL{url in
self.mode.wrappedValue.dismiss()
}

Refresh view when navigating to it

In SwiftUI 2, when I navigate from a parent view to a child view and then back to the parent view, the parent view does not refresh its contents. How can I force it to refresh its contents?
In order to test if the contents get refreshed, in my parent view I displayed a random number using the following code:
Text("Random number is \(Int.random(in: 1..<100))")
When I navigate to a child view, and then I tap the Back button in the navigation bar to return to this parent view, the random number displayed remains the same. This indicates that the view is not refreshing.
How can I force the view to refresh itself whenever the user navigates back to it?
You could force SwiftUI to update the list by adding an .id(viewID) view modifier to the source view with an #State variable, in this case called "viewID". Then update this viewID in .onDisappear() of the destination view:
struct ContentView: View {
#State private var viewID: Int = 0
var body: some View {
NavigationView {
VStack {
Text("Hello, random world number \(Int.random(in: 1...100))")
.padding()
.id(viewID)
NavigationLink(
destination: destinationView,
label: { labelView })
}
}
}
private var labelView: some View {
Text("Go to Destination View")
}
private var destinationView: some View {
return Text("I am the Destination.")
.onDisappear{
viewID += 1
}
}
}
SwiftUI is declarative - as in you define the states the view could be in and Swift UI will take care of when it should update. In your example, there is no change in state, and thus, the view doesn't update.
If you want the view to refresh, you need to update state on appear events. You can do so like this:
struct ContentView: View {
#State var intValue: Int = 0
var body: some View {
Text("Random number is \(self.intValue)")
.onAppear {
intValue = Int.random(in: 1..<100)
}
}
}
You'll find that if you push a different view using a NavigationView/NavigationLink, and then navigate back, the label should update with a new random value.

How to pop a UIViewControllerRepresentable from a SwiftUI navigation stack programmatically

Here is my SwiftUI setup:
I have a SwiftUI view (called MapSearchView) that has a NavigationView that contains a NavigationLink.
var body: some View {
VStack {
NavigationView {
Form {
Section (header: Text("Location")){
NavigationLink (locationString, destination: GooglePlacesViewController(mapTools: $mapTools))
}...
The NavigationLink's destination is a UIViewControllerRepresentable which wraps a GooglePlacesViewController. When I am done with my GooglePlaces search (either because I made a selection or I cancelled the search), I want to programmatically pop back to the MapSearchView.
I have tried adding the following in the UIViewControllerRepresentable (GooglePlacesViewController):
#Environment(\.presentationMode) var presentationMode
var popToPrevious : Bool = false {
didSet {
if popToPrevious == true {
self.presentationMode.wrappedValue.dismiss()
}
}
}
and setting the popToPrevious to true in my Coordinator using
parent.popToPrevious = true
It does get called, but does nothing.
I have also tried in the Coordinator to call
view.viewController().navigationController.popViewController(animated: true)
(viewController() is an extension that gets the viewController for any view)
This also does nothing.
Ideas?
If you create Coordinator with owner view (ie. your representable), then try to call directly when needed
ownerView.presentationMode.wrappedValue.dismiss()

Fire some code when dismissing SwiftUI modal

I have a modal built with SwitftUI which has a TextField with onCommit: code which saves user input from #State variable to file when user taps "return" on keyboard.
However, if user types something inside TextField and then dismisses the modal without pressing "return", the onCommit: code doesn't fire and user input stays unsaved. How do I fire some code accessing inner variable of my modal View when it is dismissed?
Try the following:
Instead having a private #State var on your modal, make it an internal #Binding that you pass into the modal from the call site. This way the modified bound variable is available on both the caller and the modal view.
import SwiftUI
struct ContentView: View {
#State var dismiss = false
#State var txt = ""
#State var store = ""
var body: some View {
VStack {
Text("modal").sheet(isPresented: $dismiss, onDismiss: {
self.store = self.txt
}) {
TextField("txt", text: self.$txt) {
self.store = self.txt
}.padding().border(Color.red)
}.onTapGesture {
self.dismiss.toggle()
}
Text(store)
}
}
}
struct ContetView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}