When opening a context menu, the keyboard disappears and the active textfield will become unfocused. Is there a way to keep the keyboard open and the active textfield focused?
This short example demonstrates the issue:
struct ContentView: View {
#State private var text: String = ""
var body: some View {
TextField(
"Enter text",
text: $text
)
.contextMenu {
Button(action: {
print("Hello")
}) {
Text("Hello")
}
}
}
}
Before opening the context menu the keyboard is open and the text field is focused:
After opening the context menu the keyboard is closed and the textfield becomes unfocused:
You need to put FocusState in a onAppear inside of a DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {}
This solves the problem:
struct ContentView: View {
#FocusState private var focusOn: Bool
#State private var text = ""
var body: some View {
TextField("Enter text", text: $text)
.disableAutocorrection(true)
.keyboardType(.default)
.focused($focusOn)
.padding()
.contextMenu {
Button {
print("Hello")
} label: { Text("Hello") }
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
focusOn = true
}
}
.submitLabel(.go)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button { hideKeyboard() } label: {
Image(systemName: "keyboard.chevron.compact.down")
.padding()
}
}
}
}
public func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
Related
I have a basic window with an input field (page 1), on top of it appears a popup window (page 2), inside of which there is also an input fields and buttons, which, when clicked, will bring up a small window with an input field (page 3). If there is no "Done" on the keyboard, the interface functions normally. If you add a "Done" button, it turns out that its color changes from system color blue to gray when moving from page 2 to page 3. Experimenting and wondering why this is so, I found that the toolbar on page 1 is responsible for the color of the button on page 3... If you change the color of the button on the toolbar on page 1 - it will change on the toolbar on page 3, and page 2 will not be affected. Also, adding buttons causes error: "[LayoutConstraints] Unable to simultaneously satisfy constraints." I want to understand why setting the button on the keyboard for Page 1, I also get a button when I type on Page 3? And why is it grayed out and not working? Why if I change the color for the button on Page 1, does it also change for that gray button on Page 3?
A small representative sample:
ContentView
import SwiftUI
struct ContentView: View {
#State private var bloodClucoseLvl: String = ""
#State private var isSheetShown: Bool = false
#FocusState private var focusField: Bool
var body: some View {
NavigationView {
List {
Section("Add your current blood glucose lvl") {
TextField("5,0 mmol/l", text: $bloodClucoseLvl)
.focused($focusField)
}
Section("Add food or drink") {
Button(action:{
isSheetShown.toggle()
}, label:{
HStack{
Text("Add")
Image(systemName: "folder.badge.plus")
}
})
.sheet(isPresented: $isSheetShown) {
addFoodButton()
}
}
}
.navigationTitle("Page 1 - General")
.toolbar{
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button(action: {
focusField = false
}) {
Text("Done")
}
}
}
}
.ignoresSafeArea(.keyboard)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
addFoodButton
import SwiftUI
struct addFoodButton: View {
#State private var selectedFood: String = ""
#State public var addScreen: Bool = false
var body: some View {
ZStack {
NavigationView {
List {
Section("or choose from category"){
NavigationLink(destination: Alcohol(addScreen: $addScreen)){
Text("Alcohol")
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $selectedFood, prompt: "Search by word")
.navigationTitle("Page 2 - Search in DB")
}
if addScreen{
addScreenView(addScreen: $addScreen)
}
}
}
}
struct Alcohol: View {
#State private var searchInsideCategory: String = ""
#Binding var addScreen: Bool
var body: some View {
List {
Button(action: {addScreen.toggle()}){
Text("Light beer")
}
}
.navigationTitle("Page 2 - Choose beer")
.searchable(text: $searchInsideCategory, prompt: "Search inside a category")
}
}
struct addFoodButton_Previews: PreviewProvider {
static var previews: some View {
addFoodButton()
}
}
addScreenView
import SwiftUI
struct addScreenView: View {
#Binding var addScreen: Bool
#State private var gram: String = ""
var body: some View {
ZStack{
Color.black.opacity(0.2).ignoresSafeArea()
VStack(spacing:0){
Text("Page 3 - Add an item")
.bold()
.padding()
Divider()
VStack(){
TextField("gram", text: $gram)
.padding(.leading, 16)
.padding(.trailing, 16)
.keyboardType(.numberPad)
Rectangle()
.frame(height: 1)
.padding(.leading, 16)
.padding(.trailing, 16)
}.padding()
Divider()
HStack(){
Button(action: {
addScreen.toggle()
}){
Text("Cancel").frame(minWidth:0 , maxWidth: .infinity)
}
Divider()
Button(action: {
addScreen.toggle()
}){
Text("Save").frame(minWidth:0 , maxWidth: .infinity)
}
}.frame(height: 50)
}
.background(Color.white.cornerRadius(10))
.padding([.leading, .trailing], 15)
}
}
}
struct addScreenView_Previews: PreviewProvider {
static var previews: some View {
addScreenView(addScreen: .constant(true))
}
}
If the user clicks on connexionBtnView I want to redirect them to an AdminView or UserView
import SwiftUI
struct ConnexionView: View {
#State var loginId: String = ""
#State var pwd: String = ""
#StateObject private var keyboardHander = KeyBoardHandler()
var body: some View {
NavigationView{
ZStack{
Image("background")
.ignoresSafeArea()
VStack (spacing: 15){
Spacer()
logoView
Spacer()
titleView
loginIdView
loginPwdView
connexionBtnView
Spacer()
NavigationLink {
LostPwdView()
} label: {
lostPwd
}
Spacer()
}.frame(maxHeight: .infinity)
.padding(.bottom,keyboardHander.keyboardHeight)
.animation(.default)
}
}
}
The NavigationLink has the isActive parameter. You can pass it in the init of NavigationLink and when this state variable has the true value you will redirect to another view. Details here.
struct ConnexionView: View {
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive) {
LostPwdView()
} label: {
Text("Some Label")
}
Button("Tap me!") {
isActive = true
}
}
}
}
}
struct LostPwdView: View {
var body: some View {
Text("Hello")
}
}
What you need to do is having a #State variable that would trigger the navigation:
.fullScreenCover(
isPresented: $viewShown
) {
print("View dismissed")
} content: {
NextView()
}
Where NextView() is the View you want to show and the viewShown is your State variable, below a full example:
struct ExampleView: View {
#State var isNextPageOpen = false
var body: some View {
Button("Tap Here To Navigate") {
isNextPageOpen = true
}
.fullScreenCover(
isPresented: $isNextPageOpen
) {
print("View dismissed")
} content: {
NextView()
}
}
}
I have a SwiftUI view that consists of a list with some items. Some of these are links to other screens (so I use NavigationLink to do this) and others are actions I want to perform on the current screen (E.g. button to show action sheet).
I am looking for a way for a Button in a SwiftUI List to show with a disclosure indicator (the chevron at the right hand sign that is shown for NavigationLink).
Is this possible?
E.g.
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
Button("Action Sheet") {
self.showingActionSheet = true
}
.foregroundColor(.black)
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
Current Behaviour:
Wanted Behaviour:
My answer uses the SwiftUI-Introspect framework, used to:
Introspect underlying UIKit components from SwiftUI
In this case, it is used to deselect the row after the NavigationLink is pressed.
I would think a button with the normal accent color and without the NavigationLink be more intuitive to a user, but if this is what you need, here it is. The following answer should work for you:
import Introspect
import SwiftUI
struct ExampleView: View {
#State private var showingActionSheet = false
#State private var tableView: UITableView?
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
NavigationLink(
destination: EmptyView(),
isActive: Binding<Bool>(
get: { false },
set: { _ in
showingActionSheet = true
DispatchQueue.main.async {
deselectRows()
}
}
)
) {
Text("Action Sheet")
}
}
.introspectTableView { tableView = $0 }
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
private func deselectRows() {
if let tableView = tableView, let selectedRow = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRow, animated: true)
}
}
}
The possible approach is to make row with custom chevron, like in demo below (tested with Xcode 12.1 / iOS 14.1)
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
HStack {
Text("Navigation Link")
// need to hide navigation link to use same chevrons
// because default one is different
NavigationLink(destination: Text("xx")) { EmptyView() }
Image(systemName: "chevron.right")
.foregroundColor(Color.gray)
}
HStack {
Button("Action Sheet") {
self.showingActionSheet = true
}
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.gray)
}
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
I used a ZStack to put a NavigationLink with an empty label behind the actual label content. This way you get the correct chevron symbol.
For the isActive Binding, you can just use a .constant(false) Binding that will always return false and cannot be changed.
This will result in a row that looks exactly like a NavigationLink, but behaves like a Button. This unfortunately also includes the drawback that the user has to click on the label-content (the text) to activate the button and cannot just click on empty space of the cell.
struct ExampleView: View {
#State private var showingActionSheet = false
var body: some View {
NavigationView {
List {
NavigationLink("Navigation Link", destination: Text("xx"))
Button {
self.showingActionSheet = true
} label: {
// Put a NavigationLink behind the actual label for the chevron
ZStack(alignment: .leading) {
// NavigationLink that can never be activated
NavigationLink(
isActive: .constant(false),
destination: { EmptyView() },
label: { EmptyView() }
)
// Actual label content
Text("Action Sheet")
}
}
// Prevent the blue button tint
.buttonStyle(.plain)
}
.listStyle(GroupedListStyle())
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Title"), buttons: [
.default(Text("Do Something")) { },
.cancel()
])
}
}
}
}
struct NavigationButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack(alignment: .leading) {
NavigationLink(
isActive: .constant(false),
destination: { EmptyView() },
label: { EmptyView() }
)
configuration.label
}
}
}
Example:
NavigationView {
List {
Button(action: {
openURL(URL(string: "https://www.apple.com")!)
}) {
Text("Visit Apple")
}
.buttonStyle(NavigationButton())
}
}
I have a view that can be shown either as a modal, or simply pushed onto a navigation stack. When it's pushed, it has the back button in the top left, and when it's shown as a modal, I want to add a close button (many of my testers were not easily able to figure out that they could slide down the modal and really expected an explicit close button).
Now, I have multiple problems.
How do I figure out if a View is shown modally or not? Or alternatively, if it's not the first view on a navigation stack? In UIKit there are multiple ways to easily do this. Adding a presentationMode #Environment variable doesn't help, because its isPresented value is also true for pushed screens. I could of course pass in a isModal variable myself but it seems weird that's the only way?
How do I conditionally add a leading navigationBarItem? The problem is that if you give nil, even the default back button is hidden.
Code to copy and paste into Xcode and play around with:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(leading: closeButton, trailing: deleteButton)
}
private var closeButton: some View {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}
}
private var deleteButton: some View {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If I change closeButton to return an optional AnyView? and then return nil when isModal is false, I don't get a back button at all. I also can't call navigationBarItems twice, once with a leading and once with a trailing button, because the latter call overrides the first call. I'm kinda stuck here.
Okay, I managed it. It's not pretty and I am very much open to different suggestions, but it works 😅
import SwiftUI
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
public func conditionalNavigationBarItems(_ condition: Bool, leading: AnyView, trailing: AnyView) -> some View {
Group {
if condition {
self.navigationBarItems(leading: leading, trailing: trailing)
} else {
self
}
}
}
}
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(trailing: deleteButton)
.conditionalNavigationBarItems(isModal, leading: closeButton, trailing: deleteButton)
}
private var closeButton: AnyView {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}.eraseToAnyView()
}
private var deleteButton: AnyView {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}.eraseToAnyView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I don't see any trouble, just add Dismiss button to your navigation bar. You only have to rearrange your View hierarchy and there is no need to pass any binding to your DetailView
import SwiftUI
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
struct ContentView: View {
#State var sheet = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.sheet = true
}
NavigationLink("Push", destination: DetailView())
}.navigationBarTitle("Home")
}
.sheet(isPresented: $sheet) {
NavigationView {
DetailView().navigationBarTitle("Title").navigationBarItems(leading: Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss")
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You still can dismiss it with swipe down, you can add some buttons (as part of DetailView declaration) ... etc.
When pushed, you have default back button, if shown modaly, you have dismiss
button indeed.
UPDATE (based od discussion)
.sheet(isPresented: $sheet) {
NavigationView {
GeometryReader { proxy in
DetailView().navigationBarTitle("Title")
.navigationBarItems(leading:
HStack {
Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss").padding(.horizontal)
})
Color.clear
Button(action: {
}, label: {
Image(systemName: "trash")
.imageScale(.large)
.padding(.horizontal)
})
}.frame(width: proxy.size.width)
)
}
}
}
finally I suggest you to use
extension View {
#available(watchOS, unavailable)
public func navigationBarItems<L, T>(leading: L?, trailing: T) -> some View where L : View, T : View {
Group {
if leading != nil {
self.navigationBarItems(leading: leading!, trailing: trailing)
} else {
self.navigationBarItems(trailing: trailing)
}
}
}
}
Whenever we provide .navigationBarItems(leading: _anything_), ie anything, the standard back button has gone, so you have to provide your own back button conditionally.
The following approach works (tested with Xcode 11.2 / iOS 13.2)
.navigationBarItems(leading: Group {
if isModal {
closeButton
} else {
// custom back button here calling same dismiss
}
}, trailing: deleteButton)
Update: alternate approach might be as follows (tested in same)
var body: some View {
VStack {
if isModal {
Text("Hello")
.navigationBarItems(leading: closeButton, trailing: deleteButton)
} else {
Text("Hello")
.navigationBarItems(trailing: deleteButton)
}
}
.navigationBarTitle("Test", displayMode: .inline)
}
How can I dismiss the keyboard after the user clicks outside the TextField using SwiftUI?
I created a TextField using SwiftUI, but I couldn't find any solution for dismissing the keyboard if the user clicks outside the TextField. I took a look at all attributes of TextField and also the SwiftUI TextField documentation and I couldn't find anything related with dismissing keyboard.
This is my view's code:
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
}
}
}
This can be done with a view modifier.
Code
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
#if os(macOS)
return content
#else
return content.gesture(tapGesture)
#endif
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
Usage
backgroundView()
.dismissKeyboardOnTap()
Check out the demo here: https://github.com/youjinp/SwiftUIKit
here is the solution using DragGesture it's working.
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("My Text", text: $text)
.keyboardType(.decimalPad)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.gesture(
TapGesture()
.onEnded { _ in
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
)
}
}
Add tap gesture to most outer view and call extension method inside tap gesture closure.
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
} .onTapGesture(perform: {
self.endTextEditing()
})
}
}
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
TextField("Phone Number", text: $no)
.keyboardType(.numbersAndPunctuation)
.padding()
.background(Color("4"))
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(y:-self.value).animation(.spring()).onAppear() {
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillShowNotification, object: nil, queue: .main){ (notif)in
let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
let height = value.height
self.value = height
}
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillHideNotification, object: nil, queue: .main){ (notification)in
self.value = 0
}
}
In SwiftUI 3 #FocusState wrapper can be used to remove or switch focus from editable fields. When the focus is removed from field, keyboard dismisses. So in your case it is just a matter of giving space and gesture to the surrounding space of TextView.
struct ContentView: View {
#State var inputValue : String = ""
#FocusState private var inputIsFocused: Bool
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
.border(Color.green)
.focused($inputIsFocused)
}
.frame(maxHeight: .infinity) // If input is supposed to be in the center
.background(.yellow)
.onTapGesture {
inputIsFocused = false
}
}
}
But we can do more interesting things with #FocusState. How about switching from field to field in a form. And if you tap away, keyboard also dismisses.
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
#State private var firstName = ""
#State private var lastName = ""
#State private var emailAddress = ""
#FocusState private var focusedField: Field?
var body: some View {
ZStack {
VStack {
TextField("Enter first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.givenName)
.submitLabel(.next)
TextField("Enter last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.familyName)
.submitLabel(.next)
TextField("Enter email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.textContentType(.emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
print("Creating account…")
}
}
}
.textFieldStyle(.roundedBorder)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle()) // So ZStack becomes clickable
.onTapGesture {
focusedField = nil
}
}
}