How to resolve "Use of unresolved identifier 'PresentationLink'" error in swiftUI? [duplicate] - swift

I am attempting to dismiss a modal view presented via a .sheet in SwiftUI - called by a Button which is within a NavigationViews navigationBarItems, as per below:
struct ModalView : View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.presentationMode.value.dismiss()
}, label: { Text("Save")})
}
}
struct ContentView : View {
#State var showModal: Bool = false
var body: some View {
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button(action: {
self.showModal = true
}, label: { Text("Add") })
.sheet(isPresented: $showModal, content: { ModalView() })
)
}
}
}
The modal does not dismiss when the Save button is tapped, it just remains on screen. The only way to get rid of it is swiping down on the modal.
Printing the value of self.presentationMode.value always shows false so it seems to think that it hasn't been presented.
This only happens when it is presented from the NavigationView. Take that out and it works fine.
Am I missing something here, or is this a beta issue?

You need to move the .sheet outside the Button.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") {
self.showModal = true
}
)
.sheet(isPresented: $showModal, content: { ModalView() })
}
You can even move it outside the NavigationView closure.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") { self.showModal = true }
)
}
.sheet(isPresented: $showModal, content: { ModalView() })
Notice you can also simplify the Button call if you have a simple text button.

The solution is not readily apparent in the documentation and most tutorials opt for simple solutions. But I really wanted a button in the NavigationBar of the sheet that would dismiss the sheet. Here is the solution in six steps:
Set the DetailView to not show.
Add a button to set the DetailView to show.
Call the .sheet(isPresented modifier to display the sheet.
Wrap the view that will appear in the sheet in a NavigationView because we want to display a .navigationBarItem button.
PresentationMode is required to dismiss the sheet view.
Add a button to the NavBar and call the dismiss method.
import SwiftUI
struct ContentView: View {
// 1
#State private var showingDetail = false
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button("Show Detail") {
showingDetail = true // 2
}
// 3
.sheet(isPresented: $showingDetail) {
// 4
NavigationView {
DetailView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
// 5
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Detail View!")
// 6
.navigationBarItems(leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "x.circle")
.font(.headline)
.foregroundColor(.accentColor)
})
}
}

Related

SwiftUI: Sheet gets dismissed immediately after being presented

I want to have a fullscreen SwiftUI View with a button in the Navigation Bar, which presents a SwiftUI Sheet above.
Unfortunately, the Compiler says: "Currently, only presenting a single sheet is supported.
The next sheet will be presented when the currently presented sheet gets dismissed."
This is my Code:
struct ContentView: View {
var body: some View {
EmptyView().fullScreenCover(isPresented: .constant(true), content: {
FullScreenView.init()
})
}
}
struct FullScreenView: View{
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
#State private var showingSheet = false
var body: some View {
Form {
Section(header: Text("Header")) {
NavigationLink(destination: UIKitView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
AddContentView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: AddContentView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}
}
The Problem
I want to show the SwiftUI View in Fullscreen, so I use fullScreenCover(...). With this first "Sheet", I cannot present a second sheet, my AddContentView() View. Is there any way how I can fix this? I really want to have this sheet above :(
Thanks for any help!!
Feel free to ask for other code or if there are ambiguities. :)
The error message says that the sheet cannot be displayed at the same time(Do not overlap sheets), so if you want to go to view and again to another view, you have to use a NavigationLink and only at the end .sheet()
.sheet(isPresented: $showingSheet){
AddContentView()
}
or fullScreenCover()
.fullScreenCover(isPresented: $showingSheet){
AddContentView()
}
Edited: Sheet is not overlapped twice in this code.
import SwiftUI
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView{
Form {
Section(header: Text("Header")) {
NavigationLink(destination: EmptyView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
EmptyView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: EmptyView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
p.s. fullScreenCover() belongs to the sheet

How to change "Cancel" button text in modal sheet view in SwiftUI on watchOS 7?

I have 2 simple views:
import SwiftUI
struct ContentView: View {
#State private var showingModalView = false
var body: some View {
Button(action: {
self.showingModalView.toggle()
}) {
Text("Show Modal View")
}.sheet(isPresented: $showingModalView) {
ModalView()
}
}
}
struct ModalView: View {
var body: some View {
Text("Modal View")
}
}
When "Show Modal" button pressed, ModalView is show.
How to change text "Cancel" when ModalView is active to something else?
This Cancel is actually a navigation bar item. You can replace it with own button using toolbar, like
struct ContentView: View {
#State private var showingModalView = false
var body: some View {
Button(action: {
self.showingModalView.toggle()
}) {
Text("Show Modal View")
}.sheet(isPresented: $showingModalView) {
ModalView()
.toolbar(content: {
ToolbarItem(placement: .cancellationAction) {
Button("Close") { self.showingModalView = false }
}
})
}
}
}
also you can hide it at all (and make your custom approach to close, eg. with button in sheet view, etc.)
}.sheet(isPresented: $showingModalView) {
ModalView()
.navigationBarHidden(true)

I'm having problem dismissing a modal view when calling it inside a navigation bar with SwiftUI

At tap "Show Modal" button in navigation bar, the modal is shown.
But, when "Dismiss" button is tapped, the modal never is showed.
If the modal is swiped, the error don't occurr.
import SwiftUI
struct ContentView: View {
#State var showModal = false
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarItems(trailing: Button("Show Modal") {
self.showModal = true
})
}.sheet(isPresented: self.$showModal) {
Modal()
}
}
}
struct Modal: View {
#Environment(\.presentationMode) var mode
var body: some View {
NavigationView {
Text("Modal")
.navigationBarItems(leading: Button("Dismiss") {
self.mode.wrappedValue.dismiss()
})
}
}
}
You need to toggle off the showModal state. Something like this will work. I merged both views to demonstrate quickly
import SwiftUI
struct ContentView: View {
#State var showModal = false
#Environment(\.presentationMode) var mode
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarItems(trailing: Button("Show Modal") {
self.showModal.toggle()
})
}.sheet(isPresented: self.$showModal) {
NavigationView {
Text("Modal")
.navigationBarItems(leading: Button("Dismiss") {
self.showModal.toggle()
self.mode.wrappedValue.dismiss()
})
}
}
}
}

Button, how to open a new View in swiftUI embedded in navigation bar

I embedded a button on on the NavigationBar.
I'm try to make button to open a new View called DetailView
I try to use NavigationLink but it does't work inside a button.
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State var isAddPresented = false
var body: some View {
NavigationView {
HStack {
List () {
ForEach (dm.storage) { data in
StileCella(dm2: data)
}
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(trailing: Button(action: {
self.isAddPresented = true
// Load here the DetailView??? How??
DetailView()
}) {
Text("Button")
})
}
}
}
}
struct DetailView: View {
var body: some View {
VStack(alignment: .center) {
Text("CIAO").bold()
Spacer()
Image(systemName: "star")
.resizable()
}
}
}
You just need to add a sheet modifier to your view, which presents your view depending on the value of isAddPresented, just like this:
struct ContentView: View {
#State var isAddPresented = false
var body: some View {
NavigationView {
List(dm.storage){ data in
StileCella(dm2: data)
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(trailing: Button("Button") {
self.isAddPresented = true
})
} .sheet(isPresented: $isAddPresented,
onDismiss: { self.isAddPresented = false }) {
DetailView()
}
}
}
The important bit is to remember to set isAddPresented back to false in on dismiss to prevent it form presenting again.
If you want to open a new view just like we used to open through storyboard other than sheet, you can update the code in the following way:
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State var isAddPresented = false
var body: some View {
NavigationView {
HStack {
List () {
ForEach (dm.storage) { data in
StileCella(dm2: data)
}
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(leading:
NavigationLink(destination: DetailView()) {
Text("Button")
})
}
}
}
}
struct DetailView: View {
var body: some View {
VStack(alignment: .center) {
Text("CIAO").bold()
Spacer()
Image(systemName: "star")
.resizable()
}
}
}
Instead of button, simply add NavigationLink inside navigationBarItems. This would do the trick! I wrote the complete for guidance but main change point is, I used
.navigationBarItems(leading:
NavigationLink(destination: DetailView()) {
Text("Button")
})
instead of:
.navigationBarItems(trailing: Button(action: {
self.isAddPresented = true
// Load here the DetailView??? How??
DetailView()
}) {
Text("Button")
})

Custom back button for NavigationView's navigation bar in SwiftUI

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")
}
}
}