I want to dismiss the keyboard when tapping out of a textfield, in order to give space to a picker below that textfield.
struct ContentView: View {
#State private var date = Date()
#State private var text = "write something...."
var body: some View {
Form {
Section {
TextField(text, text: $text)
}
Section {
DatePicker(selection: $date, displayedComponents: .date, label: { Text("select a date")})
.onTapGesture {
self.dismissKeyboard()
}
}
}
}
func dismissKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
The problem is the .onTapGesture does dismiss the keyboard but it doesn't show the picker wheel. So, is there a way to call the hidden wheel after dismiss the keyboard?
Use .onAppear instead of .onTapGesture
DatePicker(selection: $date, displayedComponents: .date, label: { Text("select a date")})
.onAppear {
self.dismissKeyboard()
}
Related
In a View with a textfield I have a popover, activated by a button.
In that popover I want to listen to a keyboardShortcut without modifiers.
These keypresses arrive in the textfild of the parent view instead of the popover.
What can I do, to react on these in the popover?
struct ContentView: View {
#State var showPopOver = false
#State var text = ""
var body: some View {
VStack{
TextField("enter text here", text: $text)
Button("show popover"){ showPopOver = true }
.popover(isPresented: $showPopOver) {PopOverView(text: $text)} }
.keyboardShortcut("p")
.padding()
}
}
struct PopOverView: View {
#Environment(\.dismiss) var dismiss
#Binding var text: String
var body: some View {
VStack{
Text("my Popover")
Button("set Tom"){
text = "Tom"
dismiss()
}
.keyboardShortcut("t",modifiers: [])
//.keyboardShortcut("t") // like this it works
Button("set Frank"){
text = "Frank"
dismiss()
}
.keyboardShortcut("f",modifiers: [])
}
.padding()
}
}
KeyboardShortcuts with a modifier in the popover do work (commented out).
Background
I have two TextFields, one of which has a keyboard type of .decimalPad.
Given that there is no 'Done' button when using a decimal pad keyboard to close it, rather like the return key of the standard keyboard, I would like to add a 'Done' button within a toolbar above they keypad only for the decimal keyboard in SwiftUI.
Problem
Adding a .toolbar to any TextField for some reason adds it to all of the TextFields instead! I have tried conditional modifiers, using focussed states and checking for the Field value (but for some reason it is not set when checking, maybe an ordering thing?) and it still adds the toolbar above the keyboard for both TextFields.
How can I only have a .toolbar for my single TextField that accepts digits, and not for the other TextField that accepts a string?
Code
Please note that I've tried to make a minimal example that you can just copy and paste into Xcode and run it for yourself. With Xcode 13.2 there are some issues with displaying a keyboard for TextFields for me, especially within a sheet, so maybe simulator is required to run it properly and bring up the keyboard with cmd+K.
import SwiftUI
struct TestKeyboard: View {
#State var str: String = ""
#State var num: Float = 1.2
#FocusState private var focusedField: Field?
private enum Field: Int, CaseIterable {
case amount
case str
}
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
// I'm only adding .toolbar here, but it still shows for the one above..
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
Spacer()
}
}
}
class FloatNumberFormatter: NumberFormatter {
override init() {
super.init()
self.numberStyle = .currency
self.currencySymbol = "€"
self.minimumFractionDigits = 2
self.maximumFractionDigits = 2
self.locale = Locale.current
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
// So you can preview it quickly
struct TestKeyboard_Previews: PreviewProvider {
static var previews: some View {
TestKeyboard()
}
}
Try to make toolbar content conditional and move toolbar outside, like below. (No possibility to test now - just idea)
Note: test on real device
var body: some View {
VStack {
Spacer()
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
TextField("", value: $num, formatter: FloatNumberFormatter())
.focused($focusedField, equals: .amount)
.keyboardType(.decimalPad)
Spacer()
}
.toolbar { // << here !!
ToolbarItem(placement: .keyboard) {
if field == .amount { // << here !!
Button("Done") {
focusedField = nil
}
}
}
}
}
Using introspect you can do something like this in any part in your View:
.introspectTextField { textField in
textField.inputAccessoryView = UIView.getKeyboardToolbar {
textField.resignFirstResponder()
}
}
and for the getKeyboardToolbar:
extension UIView {
static func getKeyboardToolbar( _ callback: #escaping (()->()) ) -> UIToolbar {
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
let doneButton = CustomBarButtonItem(title: "Done".localized, style: .done) { _ in
callback()
}
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.items = [space, doneButton]
return toolBar
}
}
and for the CustomBarButtonItem this is a bar button item that takes a closure
import UIKit
class CustomBarButtonItem: UIBarButtonItem {
typealias ActionHandler = (UIBarButtonItem) -> Void
private var actionHandler: ActionHandler?
convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
#objc func barButtonItemPressed(sender: UIBarButtonItem) {
actionHandler?(sender)
}
}
I tried it a lot but I ended up in the below one.
.focused($focusedField, equals: .zip)
.toolbar{
ToolbarItem(placement: .keyboard) {
switch focusedField{
case .zip:
HStack{
Spacer()
Button("Done"){
focusedField = nil
}
}
default:
Text("")
}
}
}
This is my solution:
func textFieldSection(title: String,
text: Binding<String>,
keyboardType: UIKeyboardType,
focused: FocusState<Bool>.Binding,
required: Bool) -> some View {
TextField(
vm.placeholderText(isRequired: required),
text: text
)
.focused(focused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focused.wrappedValue {
Spacer()
Button {
focused.wrappedValue = false
} label: {
Text("Done")
}
}
}
}
}
For my project I have five TextField views on one View, so I created this method in the View's extension.
I pass the unique FocusState<Bool>.Binding value and use it in the ToolbarItemGroup closure to determine if we should display the content (Spacer, Button). If the particular TextField is focused, we display the toolbar content (all other unfocused TextFields won't).
Number Pad return solution in SwiftUI,
Tool bar button over keyboard,
Focused Field
struct NumberOfBagsView:View{
#FocusState var isInputActive: Bool
#State var phoneNumber:String = ""
TextField("Place holder",
text: $phoneNumber,
onEditingChanged: { _ in
//do actions while writing something in text field like text limit
})
.keyboardType(.numberPad)
.focused($isInputActive)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
print("done clicked")
isInputActive = false
}
}
}
}
I've found wrapping each TextField in its own NavigationView gives each its own context and thus a unique toolbar. It feels not right and I've seen constraint warnings in the console. Use something like this:
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
NavigationView {
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
}
// I'm only adding .toolbar here, but it still shows for the one above..
NavigationView {
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
Spacer()
}
}
There is work. But the other TextField will still display toolbar.
--- update ---
Hi, I updated the code to use ViewModifier to make the code easier to use and this time the code does compile and run >_<
struct ToolbarItemWithShow<Toolbar>: ViewModifier where Toolbar: View {
var show: Bool
let toolbar: Toolbar
let placement: ToolbarItemPlacement
func body(content: Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: placement) {
ZStack(alignment: .leading) {
if show {
HStack { toolbar }
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
extension View {
func keyboardToolbar<ToolBar>(_ show: Bool, #ViewBuilder toolbar: () -> ToolBar) -> some View where ToolBar: View {
modifier(ToolbarItemWithShow(show: show, toolbar: toolbar(), placement: .keyboard))
}
}
struct ContentView: View {
private enum Field: Hashable {
case name
case age
case gender
}
#State var name = "Ye"
#State var age = "14"
#State var gender = "man"
#FocusState private var focused: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focused, equals: .name)
.keyboardToolbar(focused == .name) {
Text("Input Name")
}
TextField("Age", text: $age)
.focused($focused, equals: .age)
.keyboardToolbar(focused == .age) {
Text("Input Age")
}
TextField("Gender", text: $gender)
.focused($focused, equals: .gender)
.keyboardToolbar(focused == .gender) {
Text("Input Sex")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
--- old ---
struct TextFieldWithToolBar<Label, Toolbar>: View where Label: View, Toolbar: View {
#Binding public var text: String
public let toolbar: Toolbar?
#FocusState private var focus: Bool
var body: some View {
TextField(text: $text, label: { label })
.focused($focus)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
ZStack(alignment: .leading) {
if focus {
HStack {
toolbar
Spacer()
Button("Done") {
focus = false
}
}
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
TextFieldWithToolBar("Name", text: $name)
TextFieldWithToolBar("Name", text: $name){
Text("Only Brand")
}
TextField("Name", "Set The Name", text: $name)
with Done with Toolbar without
I want to hide keyboard when tapped the list background, but onTapGesture will cover NavigationLink. Is this a bug or have a better solution?
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
NavigationLink("NextPage", destination: Text("Page"))
TextField("Placeholder", text: $text)
}
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
Thanks!
update
Thanks to asperi, and I found an alternative way: just put it in section header. As for style, we should create a custom ButtonStyle for NavigationLink.
Here is an example of using InsetGroupedListStyle.
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section(
header: NavigationLink(destination: Text("Page")) {
HStack {
Text("NextPage")
.font(.body)
.foregroundColor(Color.primary)
Spacer()
Image(systemName: "chevron.forward")
.imageScale(.large)
.font(Font.caption2.bold())
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.padding(.vertical, 12)
.padding(.horizontal)
}
.textCase(nil)
.buttonStyle(CellButtonStyle())
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal, -16)
) {}
TextField("Placeholder", text: $text)
}.listStyle(InsetGroupedListStyle())
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
struct CellButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
configuration.isPressed
? Color(UIColor.systemGray5)
: Color(UIColor.secondarySystemGroupedBackground)
)
}
}
Here is a possible direction to solve this - by making all taps handled simultaneously and navigate programmatically. Tested with Xcode 12 / iOS 14.
truct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
MyRowView()
TextField("Placeholder", text: $text)
}
.simultaneousGesture(TapGesture().onEnded {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
})
}
}
}
struct MyRowView: View {
#State private var isActive = false
var body: some View {
NavigationLink("NextPage", destination: Text("Page"), isActive: $isActive)
.contentShape(Rectangle())
.onTapGesture {
DispatchQueue.main.async { // maybe even with some delay
self.isActive = true
}
}
}
}
Is there a simple means of dismissing the keyboard or any other active control when another control such as a date picker becomes active? Or vice versa.
There are some solutions for dismissing the keyboard, but which also disable the use of a picker.
Possibly there is an event other than onTapGesture to use.
Follows is some sample code that illustrates the problem.
struct TestView: View {
#State private var firstname = ""
#State private var surname = ""
#State private var birthdate = Date()
#State private var activeField: Int = 0
var body: some View {
Form {
Section{
TextField("Firstname", text: self.$firstname)
TextField("Surname", text: self.$surname)
TextField("Surname", text: self.$surname, onEditingChanged: { (editingChanged) in
if editingChanged {
print("TextField focused")
} else {
print("TextField focus removed")
self.endEditing()
}
})
}
Section(header: Text("Time: ")){
DatePicker(selection: self.$birthdate, in: ...Date(), displayedComponents: .date) {
Text("Date of Birth")
}
.onTapGesture {
self.endEditing()
}
DatePicker(selection: self.$birthdate, in: ...Date(), displayedComponents: .hourAndMinute) {
Text("Date of Birth")
}
}
}
}
func endEditing() {
//UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
Besides onTapGesture, you can add onAppear. That solves your concerns
DatePicker(selection: self.$birthdate, in: ...Date(), displayedComponents: .date) {
Text("Date of Birth")
}
.onAppear{self.endEditing()}
.onTapGesture{self.endEditing()}
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")
}
}
}