SwiftUI - Dismiss Keyboard when Picker selected - datepicker

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

Related

SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields

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

SwiftUI List onTapGesture covered NavigationLink

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

SwiftUI Glitches in FormView

I am facing some weird glitches in my Form implementation. I am not sure whether this is some implementation error or a bug in SwiftUI itself.
So basically what I want to do is the typical "ToDo/TimeTracking" app. I want to create a new task entity referenced to a project entity. Therefore I created a Form where I can select a project, set the title and notes of the task as well as the start and end date.
But now I am experiencing some visual glitches.
When I select a project with the Picker I am getting navigated to a separate view (as expected) but the list from which I can select the the project is moving a little bit upwards after the page transition animation as finished.
When selecting "Already finished" and then selecting a date from the DatePicker and closing the picker, the picker glitches around inside the view before it gets hidden.
A minimal viable example can be achieved with the following code snippet:
mport SwiftUI
struct Project {
let id: Int
let title: String
}
let projects = [Project(id: 0, title: "Hello"),Project(id: 1, title: "World")]
struct ContentView: View {
#State private var showingCreateTask = false
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Test")
.navigationBarItems(trailing: Button(action: { self.showingCreateTask.toggle() }) {
Image(systemName: "plus").imageScale(.large)
})
.sheet(isPresented: self.$showingCreateTask) {
CreateTaskView(projects: projects)
}
}
}
}
struct CreateTaskView: View {
let projects: [Project]
#State private var selectedProject = 0
#State private var taskTitle: String = ""
#State private var taskNotes: String = ""
#State private var alreadyFinished = false
#State private var startDate = Date()
#State private var endDate = Date()
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedProject, label: Text("Project")) {
ForEach(0..<self.projects.count) { index in
Text(self.projects[index].title)
}
}
}
Section {
TextField("Title", text: $taskTitle)
TextField("Notes", text: $taskNotes)
Toggle(isOn: $alreadyFinished) {
Text("Already finished ?")
}
}
if alreadyFinished {
Section {
DatePicker(selection: $startDate, displayedComponents: [.date, .hourAndMinute]) {
Text("Start Date")
}
DatePicker(selection: $endDate, in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("End Date")
}
}
}
Button(action: {
}) {
Text("Save changes")
}
}
.navigationBarTitle("Create a new task")
}
}
}
Maybe someone has experienced something similar and knows whether this is an SwiftUI Bug or some error in my code. Any help is appreciated.
You may improve it like this:
NavigationView {
Form {
Section {
Picker(selection: $selectedProject, label: Text("Project")) {
ForEach(0..<self.projects.count) { index in
Text(self.projects[index].title).tag(index)
}
}
}
Section {
TextField("Title", text: $taskTitle)
TextField("Notes", text: $taskNotes)
Toggle(isOn: $alreadyFinished) {
Text("Already finished ?")
}
}
Button(action: {
}) {
Text("Save changes")
}
if alreadyFinished {
Section {
DatePicker(selection: $startDate,in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("Start Date")
}}
Section {
DatePicker(selection: $endDate, in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("End Date")
}
}.transition(AnyTransition.identity.animation(nil))
}
}
.navigationBarTitle("Create a new task", displayMode: .inline)
}

SwiftUI: Is there any solution to clear value in DatePicker?

I am finding a solution to clear value of a DatePicker in SwiftUI. I tried and it's not success. Please check:
struct EditProfileView: View {
#State var birthDate = Date()
var body: some View {
Form {
Section (header: Text("Birth day")) {
DatePicker(selection: $birthDate, in: ...Date(), displayedComponents: .date) {
Text("Select a date")
}
HStack {
Text("You were born in \(birthDate, formatter: dateFormatter)")
.font(.subheadline)
.foregroundColor(Color.gray)
Spacer()
Button(action: {
self.clearDate()
}) {
Text("Clear")
}
}
}
}
}
func clearDate () {
self.$birthDate = nil
}
}
This line of code is not work:
self.$birthDate = nil
I think because of Date type cant not leave nil but I cant find a solution to handle with it
Set the birthDate (not $birthDate) to the following, which will default back to the current date:
self.birthDate = Date()
You can create a Binding from Date?
struct EditProfileView: View {
#State var birthDate : Date?
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
var body: some View {
Form {
Section (header: Text("Birth day")) {
DatePicker(selection: Binding<Date>(get: { () -> Date in
self.birthDate ?? Date()
}, set: { (Date) in
self.birthDate = Date
}), in: ...Date(), displayedComponents: .date) {
Text("Select a date")
}
HStack {
Text( "You were born in :") + (birthDate == nil ? Text("") : Text("\(birthDate!, formatter: dateFormatter)"))
.font(.subheadline)
.foregroundColor(Color.gray)
Spacer()
Button(action: {
self.clearDate()
}) {
Text("Clear")
}
}
}
}
}
func clearDate () {
self.birthDate = nil
}
}

Show/hide DatePicker wheel manually in SwiftUI

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