NavigationView pops back to root, omitting intermediate view - swift

In my navigation, I want to be able to go from ContentView -> ModelListView -> ModelEditView OR ModelAddView.
Got this working, my issue now being that when I hit the Back button from ModelAddView, the intermediate view is omitted and it pops back to ContentView; a behaviour that
ModelEditView does not have.
There's a reason for that I guess – how can I get back to ModelListView when dismissing ModelAddView?
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List{
NavigationLink(
destination: ModelListView(),
label: {
Text("1. Model")
})
Text("2. Model")
Text("3. Model")
}
.padding()
.navigationTitle("Test App")
}
}
}
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing:
NavigationLink(
destination: ModelAddView(modelViewModel: $modelViewModel), label: {
Image(systemName: "plus")
})
)
}
}
struct ModelEditView: View {
#Binding var model: Model
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelAddView: View {
#Binding var modelViewModel: ModelViewModel
#State var model = Model(id: UUID(), titel: "")
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelViewModel {
var modelValues: [Model]
init() {
self.modelValues = [ //mock data
Model(id: UUID(), titel: "Foo"),
Model(id: UUID(), titel: "Bar"),
Model(id: UUID(), titel: "Buzz")
]
}
}
struct Model: Identifiable, Equatable {
let id: UUID
var titel: String
}

Currently placing a NavigationLink in the .navigationBarItems may cause some issues.
A possible solution is to move the NavigationLink to the view body and only toggle a variable in the navigation bar button:
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
#State var isAddLinkActive = false // add a `#State` variable
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
}
)
}
.background( // move the `NavigationLink` to the `body`
NavigationLink(destination: ModelAddView(modelViewModel: $modelViewModel), isActive: $isAddLinkActive) {
EmptyView()
}
.hidden()
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: trailingButton)
}
// use a Button to activate the `NavigationLink`
var trailingButton: some View {
Button(action: {
self.isAddLinkActive = true
}) {
Image(systemName: "plus")
}
}
}

Related

SwiftUI - Save status when language is selected

I have successfully displayed the language in the UI, but I have a problem: when I click the "Save" button it still doesn't save the language I selected.
I want when I have selected the language and clicked the Save Button , it will return to the previous page and when I click it again it will show the language I selected before.
Above data is simulation only, I mainly focus on its features
All my current code
struct ContentView: View {
var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView(language: language)) {
Text("Language")
.padding()
Spacer()
Text(language!)
.padding()
}
}
}
}
}
struct LanguageView: View {
#Environment(\.presentationMode) var pres
#State var language: String?
#State var selectedLanguage: String? = ""
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language)
Button(action: {
language = selectedLanguage
pres.wrappedValue.dismiss()
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
Edit to my previous answer, since something is blocking my edit to my previous answer, this one shows all the code I used to make it works well for me:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination: LanguageView(language: $language)) {
Text("Language").padding()
Spacer()
Text(language!).padding()
}
}
}
}
}
struct LanguageView: View {
#Environment(\.presentationMode) var pres
#Binding var language: String?
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language) // <--- here
Button(action: { pres.wrappedValue.dismiss() }) { // <--- here
Text("Save").foregroundColor(.black)
}.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage).padding(.trailing,40)
Rectangle().fill(Color.gray).frame( height: 1,alignment: .bottom)
}.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark").resizable().frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
you could use #State var language throughout and use dismiss, such as this,
to achieve what you want:
struct LanguageView: View {
#Environment(\.dismiss) var dismiss // <--- here
#Binding var language: String?
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language) // <--- here
Button(action: {
dismiss() // <--- here
})
{
Text("Save").foregroundColor(.black)
}
.padding()
Spacer()
}
}
}

SwiftUI: NavigationLink weird behaviour

I'm trying to build a store in order to navigate in a SwifUI flow.
The idea is that every screen should observe the state and push into the next one using a NavigationLink.
It seems to work for pushing one view, but a soon as I push several views into the stack, it starts behaving oddly: the view pops itself.
Luckily I was able to reproduce it in a separate project. When I move from 2 to 3, the third screen appears but the NavigationView resets to it's original state (ContentView):
import SwiftUI
#main
struct NavigationTestApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
}
class Store: ObservableObject{
#Published var state: StoreState
struct StoreState {
var flowState: FlowState = .none
}
enum FlowState {
case none, one, two, three
}
enum Action {
case moveTo1, moveTo2, moveTo3
}
init(state: StoreState) {
self.state = state
}
func send(_ action: Action) {
switch action {
case .moveTo1:
state.flowState = .one
case .moveTo2:
state.flowState = .two
case .moveTo3:
state.flowState = .three
}
}
}
struct ContentView: View {
#ObservedObject var store = Store(state: .init())
var body: some View {
VStack {
Button(action: {
store.send(.moveTo1)
}, label: {
Text("moveTo1")
})
NavigationLink(
destination: ContentView1().environmentObject(store),
isActive: Binding(
get: { store.state.flowState == .one },
set: { _ in
}
),
label: {}
)
}
}
}
struct ContentView1: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
Button(action: {
store.send(.moveTo2)
}, label: {
Text("moveTo2")
})
NavigationLink(
destination: ContentView2().environmentObject(store),
isActive: Binding(
get: { store.state.flowState == .two },
set: { _ in
}
),
label: {}
)
}
}
}
struct ContentView2: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
Button(action: {
store.send(.moveTo3)
}, label: {
Text("moveTo3")
})
NavigationLink(
destination: ContentView3().environmentObject(store),
isActive: Binding(
get: { store.state.flowState == .three },
set: { _ in
}
),
label: {}
)
}
}
}
struct ContentView3: View {
#EnvironmentObject var store: Store
var body: some View {
Text("Hello, world!")
.padding()
}
}
What am I missing?
I figured out a solution. The issue seems to be that your model pops the previous view from the stack when you "move to" the next. I modified it (slightly) so that the system now can determine which views need to be on the navigation stack.
I have to admit, though, that I would have expected that your example would work, since we are just declaring which (single) view is on the stack.
So, the slightly modified code which keeps the previous views on the navigation stack (I also sprinkled in some print statements to see what's going on):
import SwiftUI
class Store: ObservableObject{
#Published var state: StoreState
struct StoreState {
var flowState: FlowState = .none
}
enum FlowState: Int {
case none = 0, one, two, three
}
enum Action {
case moveTo1, moveTo2, moveTo3
}
init(state: StoreState) {
self.state = state
}
func send(_ action: Action) {
print("state: \(state), action: \(action)")
switch action {
case .moveTo1:
state.flowState = .one
case .moveTo2:
state.flowState = .two
case .moveTo3:
state.flowState = .three
}
print("new state: \(state)")
}
}
struct ContentView: View {
#ObservedObject var store = Store(state: .init())
var body: some View {
VStack {
Button(action: {
print("moveTo1")
store.send(.moveTo1)
}, label: {
Text("moveTo1")
})
NavigationLink(
destination: ContentView1().environmentObject(store),
isActive: Binding(
get: { store.state.flowState.rawValue >= Store.FlowState.one.rawValue },
set: { newValue in
store.state.flowState = .none
print("ContentView1: \(newValue)")
}
),
label: {}
)
}
}
}
struct ContentView1: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
Button(action: {
print("moveTo2")
store.send(.moveTo2)
}, label: {
Text("moveTo2")
})
NavigationLink(
destination: ContentView2().environmentObject(store),
isActive: Binding(
get: { store.state.flowState.rawValue >= Store.FlowState.two.rawValue },
set: { newValue in
store.state.flowState = .one
print("ContentView2: \(newValue)")
}
),
label: {}
)
}
}
}
struct ContentView2: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
Button(action: {
print("moveTo3")
store.send(.moveTo3)
}, label: {
Text("moveTo3")
})
NavigationLink(
destination: ContentView3().environmentObject(store),
isActive: Binding(
get: { store.state.flowState.rawValue >= Store.FlowState.three.rawValue },
set: { newValue in
store.state.flowState = .two
print("ContentView3: \(newValue)")
}
),
label: {}
)
}
}
}
struct ContentView3: View {
#EnvironmentObject var store: Store
var body: some View {
Text("Hello, world!")
.padding()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}.navigationViewStyle(StackNavigationViewStyle())
)
Notes:
It will be probably better to add an event (aka action) back and determine the new state in the view model, instead letting it doing from the view.

Edit details of a dynamic list of core data object

I used the Xcode default CoreData template to build my app.
I have tried to use CoreData and create an entity like this:
I then created a AddItemView which allows me to add item to the view.
struct AddItemView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State private var notes = ""
#State private var selectedDate = Date()
var body: some View {
NavigationView {
Form {
Section {
TextField("notes", text: $notes)
}
Section {
DatePicker("", selection: $selectedDate, displayedComponents: .date)
Text("Your selected date: \(selectedDate)")
}
Section {
Button("Save") {
let newItem = Item(context: self.viewContext)
newItem.notes = self.notes
newItem.recordDate = self.selectedDate
newItem.timestamp = Date()
try? self.viewContext.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
It works well and can add items.
Then I want to click on each of the item to go to a Detail View. In the DetailView, there should be an edit button to allow me to modify the object.
I therefore created three files for the purpose: ItemHost, DetailView, EditorView
The Navigation Destination of the item will go to the ItemHost.
struct ItemListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ItemHost(item: item)) {
VStack {
Text("Item at \(item.timestamp!, formatter: FormatterUtility.dateTimeFormatter)")
Text("notes: \(item.notes ?? "")")
Text("Item Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
#if os(iOS)
EditButton()
#endif
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {self.showingAddScreen.toggle()}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.viewContext)
}
}
}
The ItemHost as follows:
struct ItemHost: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var contextView
#State var item: Item
var body: some View {
NavigationView {
if editMode?.wrappedValue == .active {
Button("Cancel") {
editMode?.animation().wrappedValue = .inactive
}
}
if editMode?.wrappedValue == .inactive {
ItemDetailView(item: item)
} else {
ItemEditor(item: item)
}
}.navigationBarTitle("EditMode Problem")
.navigationBarItems(trailing: EditButton())
}
}
The DetailView is just a view to display the details, without any special.
struct ItemDetailView: View {
#Environment(\.managedObjectContext) var contextView
#Environment(\.presentationMode) var presentationMode
#State private var showingDeleteAlert = false
let item: Item
var body: some View {
VStack {
Text("notes: \(item.notes ?? "")")
Text("Record Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
.navigationBarTitle(Text("Item Detail"), displayMode: .inline)
.alert(isPresented: $showingDeleteAlert) {
Alert(title: Text("Delete Item"), message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteItem()
}, secondaryButton: .cancel()
)
}
.navigationBarItems(trailing: Button(action: {
self.showingDeleteAlert = true
}) {
Image(systemName: "trash")
})
}
// Problem here
// Can delete the item and go back to list page. But the actual item in the CoreData has not been removed. If I call contextView.save() it will crash.
func deleteItem() {
contextView.delete(item)
presentationMode.wrappedValue.dismiss()
}
}
The EditorView like this:
struct ItemEditor: View {
#Environment(\.presentationMode) var presentation
#State var item: Item
var body: some View {
List {
HStack {
Text("Notes").bold()
TextField("Notes", text: $item.notes) // Error
}
// Error
DatePicker(selection: $item.recordDate, displayedComponents: .date) {
Text("Record Date").bold()
}
}
}
}
A few problem here:
ItemEditor: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding'. I have no way to pick the original item object values and display it to let the user know what was the old value inside the object.
Nothing to be displayed once I click on the individual navigation item. I expect that it will originally (not edit mode) and then show the detail view. If it is edit mode, then show the editor.
I get confused with the #binding and how to pass the item into the DetailView and also the Editor. How the editor save the data back to the item object in the contextView?
For the deleteItem() in the ItemDetailView. It can remove the item and go back to the ItemListView apparently. However, when I quit the app, and then run again. I found that the item re-appeared again, not really deleted.
Click on the item now, it shows this:
Don't use #State to var Item in Core Data. You should use #ObservedObject instead. It will refresh a parent view after updating data.
Please read this article:
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme

Push, pop view controller equivalent in SwiftUI

What's equivalent to the Push and Pop of a view controller in SwiftUI?
Root :
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(Model()))
iOS version 13.1 :
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back") {
self.model.pushed = false
}
}
}
Removing the default back button and adding our own will let us get through, until the bug gets fixed by Apple.
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back") {
self.model.pushed = false
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
more to refer

Passing data from modal presentation

How to pass data to a "presentation modal" view and that data can be retrieved in Detail
I need to pass the variable title to Detail ()
struct ContentView: View {
#State var showingDetail = false
let title = "My Title"
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
Detail()
}
}
}
struct Detail: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
ScrollView {
VStack {
Text("Details view")
Text("Details view")
}
}
.navigationBarTitle("Booking", displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
print("close")
}) { Image(systemName: "xmark") }).accentColor(.pink)
}
}
}
Just declare it as a viariable/constant on Detail like this:
struct Detail: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
let title: String
var body: some View {
NavigationView {
ScrollView {
VStack {
Text(title)
//...end so on
and then pass it into the initialiser in ConotentView:
struct ContentView: View {
//...
}.sheet(isPresented: $showingDetail) {
Detail(title: self.title)
}
// ...