I have a list of items, I want to make it possible to navigate to the details view. However, the first element in the list is always passed to this view, what could be the problem?
struct ContentView: View {
var array: [Object] = [Object(id: .init(),property: 1),Object(id: .init(),property: 2),Object(id: .init(),property: 3)]
#State var showAlert = false
#State var showDetailsView = false
var body: some View {
NavigationView{
List{
ForEach(array){ item in
VStack{
Text(String(item.property))
}.onTapGesture(){ self.showAlert.toggle()}
.alert(isPresented: $showAlert){
Alert(title: Text("show details view?"), message: Text(""),
primaryButton: .default (Text("Show")) {
showDetailsView.toggle()
},
secondaryButton: .cancel()
)
}
.fullScreenCover(isPresented: $showDetailsView){ DetailsView(property: item.property) }
}
}
}
}
}
struct Object: Identifiable {
let id: UUID
var property: Int
}
struct DetailsView: View {
var property: Int?
var body: some View {
Text(String(property!))
}
}
I will get this result regardless of which item in the list I select:
In this scenario we can pass clicked item like baton from ForEach to Alert to FullScreen to Details. And, of course, we should move corresponding modifiers out of cycle, they don't need to be applied to each row.
Here is a modified code. Tested with Xcode 12.1 / iOS 14.1.
struct ContentView: View {
var array: [Object] = [Object(id: .init(),property: 1),Object(id: .init(),property: 2),Object(id: .init(),property: 3)]
#State var alertItem: Object?
#State var selectedItem: Object?
var body: some View {
NavigationView{
List{
ForEach(array){ item in
VStack{
Text(String(item.property))
}.onTapGesture(){ self.alertItem = item}
}
}
.alert(item: $alertItem) { item in
Alert(title: Text("show details view?"), message: Text(""),
primaryButton: .default (Text("Show")) {
selectedItem = item
},
secondaryButton: .cancel()
)
}
.fullScreenCover(item: $selectedItem){ DetailsView(property: $0.property) }
}
}
}
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")
}
}
}
I want to use a quite complex view to update and create CoreData Managed Objects.
To make the code shorter the edit/create view 'Edit' here is shortened.
In the case of creating a new Person, my problem is, when to create the NSManagedObject Instance.
The code below crashes in Variant 1. It seem, the Edit View in the NavigationLink is called, before the button action, which creates the Object, is performed.
Another approach I tried was to create the Object in the NavigationLink parameter (Variant 2).
Here I have a quite strange beaviour, that the Edit View dismisses, without pressing a button, if job is changed to manager.
What approach would you recommend?
struct ContentView: View {
#FetchRequest( entity: Person.entity(), sortDescriptors: [],
predicate: NSPredicate(format: "job ='manager'"))
var persons: FetchedResults<Person>
#State var newPerson : Person?
#State var selection: Int? = nil
#Environment(\.managedObjectContext) var moc
var body: some View {
NavigationView {
VStack{
Text("\(persons.count) persons")
List(persons, id: \.self) { person in
HStack {
NavigationLink(destination: Edit(person: person)) {
HStack {
Text("\(person.name) -- \(person.job )")
}
Image(systemName: "trash").onTapGesture {
self.moc.delete(person)
try! self.moc.save()
}
}
}
}
/* Variante 1 */
NavigationLink(destination: Edit(person: self.newPerson!, new: true), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
self.newPerson = Person(context: self.moc)
self.newPerson?.job = "manager"
try! self.moc.save()
}) {
Text("New Person V1").bold()
}
}
/* Variant 2
NavigationLink(destination: Edit(person: Person(context: self.moc), new: true)) {
Text("New Person V2").bold()
}
*/
}
}
}
}
struct Edit: View {
#ObservedObject var person : Person
var new = false
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Environment(\.managedObjectContext) var moc
var body: some View {
VStack{
TextField("Name", text: $person.name)
TextField("Job", text: $person.job)
Spacer()
Button("Save"){
do {
try self.moc.save()
} catch {
print(error)
}
print("====================SAVE PRESSED")
self.presentationMode.wrappedValue.dismiss()
}
Button("Cancel"){
print("====================CANCEL PRESSED")
self.moc.refresh(self.person, mergeChanges: false)
self.presentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("\(self.new ? "New" : "Edit")")
.navigationBarBackButtonHidden(true)
}
}
What approach would you recommend?
Here is possible modification of Variant1 (it looks more appropriate to go). The idea is to hide navigation link and make it active only on button click. Also make destination conditional to avoid early creation of Edit view.
See also comments inline.
Button(action: {
print("login tapped")
self.newPerson = Person(context: self.moc)
self.newPerson?.job = "manager"
try! self.moc.save()
self.selection = 1 // activate link at the end !!
}) {
Text("New Person V1").bold()
}
.background(NavigationLink(destination:
Group { // safe variant, can be separated into computed property
if self.newPerson != nil {
Edit(person: self.newPerson!, new: true)
} else { EmptyView() }
},
tag: 1, selection: $selection) { EmptyView() })
Description:
When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.
Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)
struct DetailView: View {
#ObservedObject var event: Event
var body: some View {
Text("\(event.timestamp!, formatter: dateFormatter)")
.navigationBarTitle(Text("Detail"))
}
}
Steps to reproduce:
Create a new Master Detail App project with SwiftUI and Core Data.
In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.
struct ContentView: View {
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
TabView {
NavigationView {
MasterView()
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { Event.create(in: self.viewContext) }
}
) {
Image(systemName: "plus")
}
)
Text("Detail view content goes here")
.navigationBarTitle(Text("Detail"))
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.tabItem { Text("Main") }
Text("Other Tab")
.tabItem { Text("Other Tab") }
}
}
}
Add a few items. Interact with those items in any way.
Change tabs.
Change back to Main Tab.
Attempt to delete an item.
I found a pure SwiftUI working solution:
/// This View that init the content view when selection match tag.
struct SyncView<Content: View>: View {
#Binding var selection: Int
var tag: Int
var content: () -> Content
#ViewBuilder
var body: some View {
if selection == tag {
content()
} else {
Spacer()
}
}
}
You can use it then in this way:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
SyncView(selection: $selection, tag: 0) {
ViewThatNeedsRefresh()
}
.tabItem { Text("First") }
.tag(0)
Text("Second View")
.font(.title)
.tabItem { Text("Second") }
.tag(1)
}
}
}
You can use the SyncView for each view that needs a refresh.
I want to add a custom navigation button that will look somewhat like this:
Now, I've written a custom BackButton view for this. When applying that view as leading navigation bar item, by doing:
.navigationBarItems(leading: BackButton())
...the navigation view looks like this:
I've played around with modifiers like:
.navigationBarItem(title: Text(""), titleDisplayMode: .automatic, hidesBackButton: true)
without any luck.
Question
How can I...
set a view used as custom back button in the navigation bar? OR:
programmatically pop the view back to its parent?
When going for this approach, I could hide the navigation bar altogether using .navigationBarHidden(true)
TL;DR
Use this to transition to your view:
NavigationLink(destination: SampleDetails()) {}
Add this to the view itself:
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
Then, in a button action or something, dismiss the view:
presentationMode.wrappedValue.dismiss()
Full code
From a parent, navigate using NavigationLink
NavigationLink(destination: SampleDetails()) {}
In DetailsView hide navigationBarBackButton and set custom back button to leading navigationBarItem,
struct SampleDetails: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("ic_back") // set image here
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
Text("Go back")
}
}
}
var body: some View {
List {
Text("sample code")
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
SwiftUI 1.0
It looks like you can now combine the navigationBarBackButtonHidden and .navigationBarItems to get the effect you're trying to achieve.
Code
struct Navigation_CustomBackButton_Detail: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color("Theme3BackgroundColor")
VStack(spacing: 25) {
Image(systemName: "globe").font(.largeTitle)
Text("NavigationView").font(.largeTitle)
Text("Custom Back Button").foregroundColor(.gray)
HStack {
Image("NavBarBackButtonHidden")
Image(systemName: "plus")
Image("NavBarItems")
}
Text("Hide the system back button and then use the navigation bar items modifier to add your own.")
.frame(maxWidth: .infinity)
.padding()
.background(Color("Theme3ForegroundColor"))
.foregroundColor(Color("Theme3BackgroundColor"))
Spacer()
}
.font(.title)
.padding(.top, 50)
}
.navigationBarTitle(Text("Detail View"), displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
// Hide the system back button
.navigationBarBackButtonHidden(true)
// Add your custom back button here
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "arrow.left.circle")
Text("Go Back")
}
})
}
}
Example
Here is what it looks like (excerpt from the "SwiftUI Views" book):
Based on other answers here, this is a simplified answer for Option 2 working for me in XCode 11.0:
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "gobackward").padding()
}
.navigationBarHidden(true)
}
}
Note: To get the NavigationBar to be hidden, I also needed to set and then hide the NavigationBar in ContentView.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Link").padding()
}
} // Main VStack
.navigationBarTitle("Home")
.navigationBarHidden(true)
} //NavigationView
}
}
Here's a more condensed version using principles shown in the other comments to change only the text of the button. The chevron.left icon can also be easily replaced with another icon.
Create your own button, then assign it using .navigationBarItems(). I found the following format most nearly approximated the default back button.
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var backButton : some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 0) {
Image(systemName: "chevron.left")
.font(.title2)
Text("Cancel")
}
}
}
Make sure you use .navigationBarBackButtonHidden(true) to hide the default button and replace it with your own!
List(series, id:\.self, selection: $selection) { series in
Text(series.SeriesLabel)
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
iOS 15+
presentationMode.wrappedValue.dismiss() is now deprecated.
It's replaced by DismissAction
private struct SheetContents: View {
#Environment(\.dismiss) private var dismiss
var body: some View {
Button("Done") {
dismiss()
}
}
}
You can create a custom back button that will use this dismiss action
struct NavBackButton: View {
let dismiss: DismissAction
var body: some View {
Button {
dismiss()
} label: {
Image("...custom back button here")
}
}
}
then attach it to your view.
.navigationBarBackButtonHidden(true) // Hide default button
.navigationBarItems(leading: NavBackButton(dismiss: self.dismiss)) // Attach custom button
I expect you want to use custom back button in all navigable screens,
so I wrote custom wrapper based on #Ashish answer.
struct NavigationItemContainer<Content>: View where Content: View {
private let content: () -> Content
#Environment(\.presentationMode) var presentationMode
private var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("back_icon") // set image here
.aspectRatio(contentMode: .fit)
.foregroundColor(.black)
Text("Go back")
}
}
}
var body: some View {
content()
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
}
Wrap screen content in NavigationItemContainer:
Usage:
struct CreateAccountScreenView: View {
var body: some View {
NavigationItemContainer {
VStack(spacing: 21) {
AppLogoView()
//...
}
}
}
}
Swiping is not disabled this way.
Works for me. XCode 11.3.1
Put this in your root View
init() {
UINavigationBar.appearance().isUserInteractionEnabled = false
UINavigationBar.appearance().backgroundColor = .clear
UINavigationBar.appearance().barTintColor = .clear
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
UINavigationBar.appearance().shadowImage = UIImage()
UINavigationBar.appearance().tintColor = .clear
}
And this in your child View
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "gobackward")
}
You can use UIAppearance for this:
if let image = UIImage(named: "back-button") {
UINavigationBar.appearance().backIndicatorImage = image
UINavigationBar.appearance().backIndicatorTransitionMaskImage = image
}
This should be added early on in your app like App.init. This also preserves the native swipe back functionality.
All of the solutions I see here seem to disable swipe to go back functionality to navigate to the previous page, so sharing a solution I found that maintains that functionality. You can make an extension of your root view and override your navigation style and call the function in the view initializer.
Sample View
struct SampleRootView: View {
init() {
overrideNavigationAppearance()
}
var body: some View {
Text("Hello, World!")
}
}
Extension
extension SampleRootView {
func overrideNavigationAppearance() {
let navigationBarAppearance = UINavigationBarAppearance()
let barAppearace = UINavigationBar.appearance()
barAppearace.tintColor = *desired UIColor for icon*
barAppearace.barTintColor = *desired UIColor for icon*
navigationBarAppearance.setBackIndicatorImage(*desired UIImage for custom icon*, transitionMaskImage: *desired UIImage for custom icon*)
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().compactAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
}
}
The only downfall to this approach is I haven't found a way to remove/change the text associated with the custom back button.
Really simple method. Only two lines code 🔥
#Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()
Example:
import SwiftUI
struct FirstView: View {
#State var showSecondView = false
var body: some View {
NavigationLink(destination: SecondView(),isActive : self.$showSecondView){
Text("Push to Second View")
}
}
}
struct SecondView : View{
#Environment(\.presentationMode) var presentationMode
var body : some View {
Button(action:{ self.presentationMode.wrappedValue.dismiss() }){
Text("Go Back")
}
}
}
This solution works for iPhone. However, for iPad it won't work because of the splitView.
import SwiftUI
struct NavigationBackButton: View {
var title: Text?
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack {
VStack {
ZStack {
HStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.left")
.font(.title)
.frame(width: 44, height: 44)
title
}
Spacer()
}
}
Spacer()
}
}
.zIndex(1)
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
struct NavigationBackButton_Previews: PreviewProvider {
static var previews: some View {
NavigationBackButton()
}
}
I found this: https://ryanashcraft.me/swiftui-programmatic-navigation/
It does work, and it may lay the foundation for a state machine to control what is showing, but it is not a simple as it was before.
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct RootView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
If you want to hide the button then you can replace the DetailView with this:
struct LocalDetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
.navigationBarItems(leading: Text(""))
}
}
Just write this:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
}.onAppear() {
UINavigationBar.appearance().tintColor = .clear
UINavigationBar.appearance().backIndicatorImage = UIImage(named: "back")?.withRenderingMode(.alwaysOriginal)
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "back")?.withRenderingMode(.alwaysOriginal)
}
}
}
On iOS 14+ it's actually very easy using presentationMode variable
In this example NewItemView will get dismissed on addItem completion:
struct NewItemView: View {
#State private var itemDescription:String = ""
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
TextEditor(text: $itemDescription)
}.onTapGesture {
hideKeyboard()
}.toolbar {
ToolbarItem {
Button(action: addItem){
Text("Save")
}
}
}.navigationTitle("Add Question")
}
private func addItem() {
// Add save logic
// ...
// Dismiss on complete
presentationMode.wrappedValue.dismiss()
}
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct NewItemView_Previews: PreviewProvider {
static var previews: some View {
NewItemView()
}
}
In case you need the parent (Main) view:
struct SampleMainView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \DbQuestion.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("This is item detail page")
} label: {
Text("Item at \(item.id)")
}
}
}
.toolbar {
ToolbarItem {
// Creates a button on toolbar
NavigationLink {
// New Item Page
NewItemView()
} label: {
Text("Add item")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}.navigationTitle("Main Screen")
}
}
}