DatePicker using Time-Interval in SwiftUI - swift

I want to use a DatePicker in SwiftUI, it is working fine and as expected. I want to add an Time-interval, like explained: UIDatePicker 15 Minute Increments Swift
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
Is there a Modifier for that in SwifUI?

I don't believe there is a modifier for this. However it's possible to "do it yourself" by using UIViewRepresentable to wrap a UIDatePicker:
The basic structure for this code is based on the Interfacing with UIKit tutorial.
struct MyDatePicker: UIViewRepresentable {
#Binding var selection: Date
let minuteInterval: Int
let displayedComponents: DatePickerComponents
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MyDatePicker>) -> UIDatePicker {
let picker = UIDatePicker()
// listen to changes coming from the date picker, and use them to update the state variable
picker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged), for: .valueChanged)
return picker
}
func updateUIView(_ picker: UIDatePicker, context: UIViewRepresentableContext<MyDatePicker>) {
picker.minuteInterval = minuteInterval
picker.date = selection
switch displayedComponents {
case .hourAndMinute:
picker.datePickerMode = .time
case .date:
picker.datePickerMode = .date
case [.hourAndMinute, .date]:
picker.datePickerMode = .dateAndTime
default:
break
}
}
class Coordinator {
let datePicker: MyDatePicker
init(_ datePicker: MyDatePicker) {
self.datePicker = datePicker
}
#objc func dateChanged(_ sender: UIDatePicker) {
datePicker.selection = sender.date
}
}
}
struct DatePickerDemo: View {
#State var wakeUp: Date = Date()
#State var minterval: Int = 1
var body: some View {
VStack {
Stepper(value: $minterval) {
Text("Minute interval: \(minterval)")
}
MyDatePicker(selection: $wakeUp, minuteInterval: minterval, displayedComponents: .hourAndMinute)
Text("\(wakeUp)")
}
}
}
struct DatePickerDemo_Previews: PreviewProvider {
static var previews: some View {
DatePickerDemo()
}
}

Not fully a SwiftUI solution, but much simpler than the existing answer. Attach this to your main body View that contains the picker
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
The one downside is this will apply to all pickers in the view and can affect pickers in other views in the app, but you can always do the same thing in those views (set minute interval back to 1).

I think, the below will help..
struct DatePickerView: View {
#Binding var dateSelected: Date
var type: DatePickerComponents? = .date
var body: some View {
DatePicker("", selection: $dateSelected, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.onAppear {
if type == .hourAndMinute {
UIDatePicker.appearance().minuteInterval = 15
} else {
UIDatePicker.appearance().minimumDate = dateSelected
}
}
}
}

It is also possible with Introspect lib
DatePicker(...)
.introspectDatePicker {
$0.minuteInterval = 30
$0.roundsToMinuteInterval = true
}

Related

SwiftUI: #Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:
struct JobsTab: View {
#State private var jobFilter: JobFilter = JobFilter()
var filter: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView($jobFilter, categoriesViewModel, jobsViewModel)
}
}
However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var categoriesViewModel: CategoriesViewModel
#ObservedObject var jobsViewModel: JobsViewModel
let filterViewModel: FilterViewModel
#Binding var jobFilter: JobFilter
#State private var draft: JobFilter
#State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
_jobFilter = jobFilter
_draft = State(wrappedValue: jobFilter.wrappedValue)
self.categoriesViewModel = categoriesViewModel
self.jobsViewModel = jobsViewModel
self.filterViewModel = FilterViewModel()
}
...
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("FilterView.Button.Done.Text".capitalizedLocalization) {
let request = Job.defaultRequest()
request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
jobsViewModel.filteredJobsFetchRequest = request
self.jobFilter = self.draft
presentationMode.wrappedValue.dismiss()
}
}
}
I have also tried with a #Binding like Paul says here but there's no luck.
Is there any workaround, or am I doing something wrong?
Thanks in advance!
EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft.
What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a #State) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false? I don't know to be honest.
EDIT2: I have also tried
``
if #available(iOS 15.0, *) {
let _ = Self._printChanges()
} else {
// Fallback on earlier versions
}
in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged
According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own.
Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create #Binding or #State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works.
I don't have your full code, but I created a similar situation to your problem as below code:
import SwiftUI
struct Main: View {
#State private var showFilter = false
var body: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView()
}
}
}
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VStack {
Text("Filter View")
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("cancel")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("okay")
}
}
}
}
}
}
the parameter onDismiss is missing:
.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }
View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an #State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:
struct SheetConfig {
var isPresented = false
var data: [String] = []
mutating func show(initialData: [String] ) {
isPresented = true
data = initialData
}
mutating func hide() {
isPresented = false
}
}
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
Button {
config.show(intialData: ...)
} label: {
}
.sheet(isPresented: $config.isPresented) {
FilterView($config)
}
Here is what I did to handle this issue.
Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
/// A method which should be triggered whenever dismiss is needed.
/// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
func dismissHostedView(presentationMode: Binding<PresentationMode>)
}
public extension UIViewControllerHostedView {
func dismissHostedView(presentationMode: Binding<PresentationMode>) {
// Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
if #available(iOS 15, *) {
presentationMode.wrappedValue.dismiss()
} else {
self.topViewController?.dismisOrPopBack()
}
}
}
Extend UIWindow and UIApplication
import UIKit
public extension UIWindow {
// Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
default:
if let presentedViewController = vc.presentedViewController {
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
}
}
}
#objc public extension UIApplication {
/// LCUIComponents: Returns the current visible top most window of the app.
var topWindow: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
var topViewController: UIViewController? {
return topWindow?.visibleViewController()
}
}
Extend View to handle UIHostingController presented View`
public extension View {
weak var topViewController: UIViewController? {
UIApplication.shared.topViewController
}
}
Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {
// MARK: - Properties
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body some View {
Button {
// Note: this part trigger a method in a created protocol above.
dismissHostedView(presentationMode: presentationMode)
} label: {
Text("Tap to dismiss")
}
}
In the sheet try adding this instead.
#Environment(\.dismiss) var dismiss // EnvironmentValue
Button("Some text") {
// Code
dismiss()
}

View parameter only passed to view when application first loads - SwiftUI

I have a navigation view (SettingsView) as shown below. When I go to PayScheduleForm the first time, the date from UserDefaults is passed properly. If I change the payDate value in PayScheduleForm, I can see that it updates the UserDefaults key properly. However, if I go back to SettingsView, and then go to PayScheduleForm again, the original value is still shown in the picker.
It's kind of an odd scenario so maybe it's better explained step by step:
Start App
Go to Settings -> Pay Schedule
Last UserDefaults payDate value is in DatePicker (10/08/2020)
Change value to 10/14/2020 - console shows that string of UserDefaults.standard.object(forKey: "payDate") = 10/14/2020
Go back to settings (using back button)
Go back to Pay Schedule and see that DatePicker has its original value (10/08/2020)
Of course if I restart the app again, I see 10/14/2020 in the DatePicker
struct SettingsView: View {
var body: some View {
NavigationView{
if #available(iOS 14.0, *) {
List{
NavigationLink(destination: AccountsList()) {
Text("Accounts")
}
NavigationLink(destination: CategoriesList()) {
Text("Categories")
}
NavigationLink(destination: PayScheduleForm(date: getPayDate()).onAppear(){
getPayDate()
}) {
Text("Pay Schedule")
}
}.navigationTitle("Settings")
} else {
// Fallback on earlier versions
}
}.onAppear(perform: {
getUserDefaults()
})
}
func getPayDate() -> Date{
var date = Date()
if UserDefaults.standard.object(forKey: "payDate") != nil {
date = UserDefaults.standard.object(forKey: "payDate") as! Date
}
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
return date
}
struct PayScheduleForm: View {
var frequencies = ["Bi-Weekly"]
#Environment(\.managedObjectContext) var context
#State var payFrequency: String?
#State var date: Date
var nextPayDay: String{
let nextPaydate = (Calendar.current.date(byAdding: .day, value: 14, to: date ))
return Utils.dateFormatterMed.string(from: nextPaydate ?? Date() )
}
var body: some View {
Form{
Picker(selection: $payFrequency, label: Text("Pay Frequency")) {
if #available(iOS 14.0, *) {
ForEach(0 ..< frequencies.count) {
Text(self.frequencies[$0]).tag(payFrequency)
}.onChange(of: payFrequency, perform: { value in
UserDefaults.standard.set(payFrequency, forKey:"payFrequency")
print(UserDefaults.standard.string(forKey:"payFrequency")!)
})
} else {
// Fallback on earlier versions
}
}
if #available(iOS 14.0, *) {
DatePicker(selection: $date, displayedComponents: .date) {
Text("Last Payday")
}
.onChange(of: date, perform: { value in
UserDefaults.standard.set(date, forKey: "payDate")
let date = UserDefaults.standard.object(forKey: "payDate") as! Date
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
})
} else {
// Fallback on earlier versions
}
}
```
Fixed this. Posting so others with the same issue can find the answer.
iOS 14 allows the #AppStorage property wrapper which easily allows access to UserDefaults, BUT the Date type is not allowed with #AppStorage as of now.
Instead you can use an #Observable object
First create a swift file called UserSettings.swift and create a class in there:
class UserSettings: ObservableObject {
#Published var date: Date {
didSet {
UserDefaults.standard.set(date, forKey: "payDate")
}
}
init() {
self.date = UserDefaults.standard.object(forKey: "payDate") as? Date ?? Date()
}
}
Then add an #ObservableObject to your view
#ObservedObject var userSettings = UserSettings()
...
DatePicker(selection: $userSettings.date, displayedComponents: .date) {
Text("Last Payday")
}

Swift UI DatePicker won't update #ObservedObject value

I am using a class to store a #Published variable but when I try to pass the value chosen on my custom DatePicker to the #ObservedObject in the class I receive the following error:
TimePicker(time: self.$time.**timeSelected**)
Cannot convert value of type 'Binding' (aka
'Binding') to expected argument type 'TimeModel'
How do I update the #ObservedObject with the picker value?
Full code:
struct ContentView: View {
#ObservedObject var time = TimeModel()
var body: some View {
ZStack{
VStack{
TimePicker(time: self.$time.timeSelected)
Text("You chose \(time.timeSelected/60) minutes")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TimeModel: ObservableObject{
#Published var timeSelected: TimeInterval = 1.0
}
struct TimePicker: UIViewRepresentable {
#ObservedObject var time = TimeModel()
func makeUIView(context: Context) -> UIDatePicker {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .countDownTimer
datePicker.addTarget(context.coordinator,
action: #selector(Coordinator.updateTime),
for: .valueChanged)
return datePicker
}
func updateUIView(_ datePicker: UIDatePicker, context: Context) {
datePicker.minuteInterval = 5
datePicker.countDownDuration = time.timeSelected
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
let parent: TimePicker
init(_ parent: TimePicker) {
self.parent = parent
}
#objc func updateTime(datePicker: UIDatePicker) {
parent.time.timeSelected = datePicker.countDownDuration
}
}
}
You need to refer to the same TimeModel instance.
A solution is to pass the TimeModel to the TimePicker:
struct TimePicker: UIViewRepresentable {
#ObservedObject var time: TimeModel // <- declare only
...
}
struct ContentView: View {
#ObservedObject var time = TimeModel()
var body: some View {
ZStack {
VStack {
TimePicker(time: time) // <- pass `TimeModel` here
Text("You chose \(time.timeSelected / 60) minutes")
}
}
}
}
Note that TimeInterval specifies time is in seconds, not minutes.
This line:
#Published var timeSelected: TimeInterval = 1.0
actually sets the default time to 1 second.

How do I change the appearance of the DatePicker in the SwiftUI framework to only months and years?

I want to change the DatePicker's date view. I just want to get a month and year selection. I want to assign ObservedObject to a variable at each selection.
My Code:
#State private var date = Date()
var body: some View {
DatePicker("", selection: $date, in: Date()..., displayedComponents: .date)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.onDisappear(){
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yyyy"
self.cardData.validThru = dateFormatter.string(from: self.date)
}
}
As others have already commented You would need to implement an HStack with two Pickers:
struct ContentView: View {
#State var monthIndex: Int = 0
#State var yearIndex: Int = 0
let monthSymbols = Calendar.current.monthSymbols
let years = Array(Date().year..<Date().year+10)
var body: some View {
GeometryReader{ geometry in
HStack(spacing: 0) {
Picker(selection: self.$monthIndex.onChange(self.monthChanged), label: Text("")) {
ForEach(0..<self.monthSymbols.count) { index in
Text(self.monthSymbols[index])
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
Picker(selection: self.$yearIndex.onChange(self.yearChanged), label: Text("")) {
ForEach(0..<self.years.count) { index in
Text(String(self.years[index]))
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
}
}
}
func monthChanged(_ index: Int) {
print("\(years[yearIndex]), \(index+1)")
print("Month: \(monthSymbols[index])")
}
func yearChanged(_ index: Int) {
print("\(years[index]), \(monthIndex+1)")
print("Month: \(monthSymbols[monthIndex])")
}
}
You would need this helper from this post to monitor the Picker changes
extension Binding {
func onChange(_ completion: #escaping (Value) -> Void) -> Binding<Value> {
.init(get:{ self.wrappedValue }, set:{ self.wrappedValue = $0; completion($0) })
}
}
And this calendar helper
extension Date {
var year: Int { Calendar.current.component(.year, from: self) }
}

SwiftUI: How to make TextField become first responder?

Here's my SwiftUI code:
struct ContentView : View {
#State var showingTextField = false
#State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
What I want is when the text field becomes visible, to make the text field become the first responder (i.e. receive focus & have the keyboard pop up).
Using SwiftUI-Introspect, you can do:
TextField("", text: $value)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
Swift UI 3
As of Xcode 13, you can use the focused modifier to make a view become first responder.
Swift UI 1/2
It doesn't seem to be possible at the moment, but you can implement something similar yourself.
You can create a custom text field and add a value to make it become first responder.
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
Note: didBecomeFirstResponder is needed to make sure the text field becomes first responder only once, not on every refresh by SwiftUI!
You would use it like this...
struct ContentView : View {
#State var text: String = ""
var body: some View {
CustomTextField(text: $text, isFirstResponder: true)
.frame(width: 300, height: 50)
.background(Color.red)
}
}
P.S. I added a frame as it doesn't behave like the stock TextField, meaning there's more stuff going on behind the scenes.
More on Coordinators in this excellent WWDC 19 talk:
Integrating SwiftUI
Tested on Xcode 11.4
iOS 15
There is a new wrapper called #FocusState that controls the state of the keyboard and the focused keyboard ('aka' firstResponder).
Become First Responder ( Focused )
If you use a focused modifier on the text fields, you can make them become focused:
Resign first responder ( Dismiss keyboard )
or dismiss the keyboard by setting the variable to nil:
iOS 13 and above: Old but working!
Simple wrapper struct - Works like a native:
Note that Text binding support added as requested in the comments
struct LegacyTextField: UIViewRepresentable {
#Binding public var isFirstResponder: Bool
#Binding public var text: String
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, configuration: #escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
configuration(uiView)
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
#objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
Usage:
struct ContentView: View {
#State var text = ""
#State var isFirstResponder = false
var body: some View {
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
}
}
🎁 Bonus: Completely customizable
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
$0.textColor = .red
$0.tintColor = .blue
}
This method is fully adaptable. For example, you can see How to add an Activity indicator in SwiftUI with the same method here
iOS 15.0+
macOS 12.0+,
Mac Catalyst 15.0+,
tvOS 15.0+,
watchOS 8.0+
Use focused(_:) if you have a single TextField.
focused(_:)
Modifies this view by binding its focus state to the given Boolean state value.
struct NameForm: View {
#FocusState private var isFocused: Bool
#State private var name = ""
var body: some View {
TextField("Name", text: $name)
.focused($isFocused)
Button("Submit") {
if name.isEmpty {
isFocued = true
}
}
}
}
Use focused(_:equals:) should you have multiple TextFields.
focused(_:equals:)
Modifies this view by binding its focus state to the given state value.
struct LoginForm: View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
handleLogin(username, password)
}
}
}
}
}
SwiftUI Documentation
focused(_:)
focused(_:equals:)
#FocusState
Update
I tested this in Xcode version 13.0 beta 5 (13A5212g). It works
For anyone who ended up here but faced crashed using #Matteo Pacini's answer, please be aware of this change in beta 4: Cannot assign to property: '$text' is immutable about this block:
init(text: Binding<String>) {
$text = text
}
should use:
init(text: Binding<String>) {
_text = text
}
And if you want to make the textfield become first responder in a sheet, please be aware that you cannot call becomeFirstResponder until the textfield is shown. In other words, putting #Matteo Pacini's textfield directly in sheet content causes crash.
To solve the issue, add an additional check uiView.window != nil for textfield's visibility. Only focus after it is in the view hierarchy:
struct AutoFocusTextField: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context:
UIViewRepresentableContext<AutoFocusTextField>) {
uiView.text = text
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: AutoFocusTextField
init(_ autoFocusTextField: AutoFocusTextField) {
self.parent = autoFocusTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
📦 ResponderChain
I made this small package for cross-platform first responder handling without subclassing views or making custom ViewRepresentables in SwiftUI on iOS 13+
https://github.com/Amzd/ResponderChain
How to apply it to your problem
SceneDelegate.swift
...
// Set the ResponderChain as environmentObject
let rootView = ContentView().environmentObject(ResponderChain(forWindow: window))
...
ContentView.swift
struct ContentView: View {
#EnvironmentObject var chain: ResponderChain
#State var showingTextField = false
#State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text).responderTag("field1").onAppear {
DispatchQueue.main.async {
chain.firstResponder = "field1"
}
}
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
In my case I wanted to focused a textfield just right away I used .onappear function
struct MyView: View {
#FocusState private var isTitleTextFieldFocused: Bool
#State private var title = ""
var body: some View {
VStack {
TextField("Title", text: $title)
.focused($isTitleTextFieldFocused)
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isTitleTextFieldFocused = true
}
}
}
}
As others have noted (e.g. #kipelovets comment on the accepted answer, e.g. #Eonil's answer), I have also not found any of the accepted solutions to work on macOS. I've had some luck, however, using NSViewControllerRepresentable to get a NSSearchField to appear as the first responder in a SwiftUI view:
import Cocoa
import SwiftUI
class FirstResponderNSSearchFieldController: NSViewController {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let searchField = NSSearchField()
searchField.delegate = self
self.view = searchField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
}
}
extension FirstResponderNSSearchFieldController: NSSearchFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
}
}
struct FirstResponderNSSearchFieldRepresentable: NSViewControllerRepresentable {
#Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) -> FirstResponderNSSearchFieldController {
return FirstResponderNSSearchFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: FirstResponderNSSearchFieldController,
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) {
}
}
Sample SwiftUI view:
struct ContentView: View {
#State private var text: String = ""
var body: some View {
FirstResponderNSSearchFieldRepresentable(text: $text)
}
}
To fill in this missing functionality, you may install SwiftUIX using Swift Package Manager:
In Xcode, open your project and navigate to File → Swift Packages → Add Package Dependency...
Paste the repository URL (https://github.com/SwiftUIX/SwiftUIX) and click Next.
For Rules, select Branch (with branch set to master).
Click Finish.
Open the Project settings, add SwiftUI.framework to the Linked Frameworks and Libraries, set Status to Optional.
More Info: https://github.com/SwiftUIX/SwiftUIX
import SwiftUI
import SwiftUIX
struct ContentView : View {
#State var showingTextField = false
#State var text = ""
var body: some View {
return VStack {
if showingTextField {
CocoaTextField("Placeholder text", text: $text)
.isFirstResponder(true)
.frame(width: 300, height: 48, alignment: .center)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
SwiftUI
struct ContentView: View {
enum Field {
case firstTextfield
case secondTextfield
case lastTextfield
}
#State private var firstTextfield = ""
#State private var secondTextfield = ""
#State private var lastTextfield = ""
#FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter anything on first textfield", text: $firstTextfield)
.focused($focusedField, equals: .firstTextfield)
.submitLabel(.next)
TextField("Enter anything on second textfield", text: $secondTextfield)
.focused($focusedField, equals: .secondTextfield)
.submitLabel(.next)
TextField("Enter anything on last textfield", text: $lastTextfield)
.focused($focusedField, equals: .lastTextfield)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstTextfield:
focusedField = .secondTextfield
case .secondTextfield:
focusedField = .lastTextfield
default:
focusedField = nil
}
}
}
}
Description: Add an enum with textfields cases, and a property wrapped in a #FocusState with type of that enum. Add focused(_:equals:) modifier to have a binding value, equal to the enum cases. Now, you can change the focusedField to whichever textfield you want to have cursor on, or resign first responder by assigning nil to focusedField.
Not really an answer, just building on Casper's great solution with a convenient modifier -
struct StartInput: ViewModifier {
#EnvironmentObject var chain: ResponderChain
private let tag: String
init(tag: String) {
self.tag = tag
}
func body(content: Content) -> some View {
content.responderTag(tag).onAppear() {
DispatchQueue.main.async {
chain.firstResponder = tag
}
}
}
}
extension TextField {
func startInput(_ tag: String = "field") -> ModifiedContent<TextField<Label>, StartInput> {
self.modifier(StartInput(tag: tag))
}
}
Just use like -
TextField("Enter value:", text: $quantity)
.startInput()
This is a ViewModifier that works with introspect. It works for AppKit MacOS, Xcode 11.5
struct SetFirstResponderTextField: ViewModifier {
#State var isFirstResponderSet = false
func body(content: Content) -> some View {
content
.introspectTextField { textField in
if self.isFirstResponderSet == false {
textField.becomeFirstResponder()
self.isFirstResponderSet = true
}
}
}
}
We have a solution that makes controlling the first responder effortless.
https://github.com/mobilinked/MbSwiftUIFirstResponder
TextField("Name", text: $name)
.firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)
TextEditor(text: $notes)
.firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)
Selected answer causes some infinite loop issue with AppKit. I don't know about UIKit case.
To avoid that issue, I recommend just sharing NSTextField instance directly.
import AppKit
import SwiftUI
struct Sample1: NSViewRepresentable {
var textField: NSTextField
func makeNSView(context:NSViewRepresentableContext<Sample1>) -> NSView { textField }
func updateNSView(_ x:NSView, context:NSViewRepresentableContext<Sample1>) {}
}
You can use that like this.
let win = NSWindow()
let txt = NSTextField()
win.setIsVisible(true)
win.setContentSize(NSSize(width: 256, height: 256))
win.center()
win.contentView = NSHostingView(rootView: Sample1(textField: txt))
win.makeFirstResponder(txt)
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.run()
This breaks pure value semantic, but depending on AppKit means you partially abandon pure value semantic and gonna afford some dirtiness. This is a magic hole we need right now for deal with lack of first-responder control in SwiftUI.
As we access NSTextField directly, setting first-responder is plain AppKit way, therefore no visible source of trouble.
You can download working source code here.
I took a little bit different approach - instead of UIViewRepresentable based on UITextField i made it based on UIView and and plug it in SwiftUI view hierarchy with background modifier. Inside UIView i added logic to find first view that canBecomeFirstResponder in subviews and parent views.
private struct FocusableUIView: UIViewRepresentable {
var isFirstResponder: Bool = false
class Coordinator: NSObject {
var didBecomeFirstResponder = false
}
func makeUIView(context: UIViewRepresentableContext<FocusableUIView>) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}
func makeCoordinator() -> FocusableUIView.Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<FocusableUIView>) {
guard uiView.window != nil, isFirstResponder, !context.coordinator.didBecomeFirstResponder else {
return
}
var foundRepsonder: UIView?
var currentSuperview: UIView? = uiView
repeat {
foundRepsonder = currentSuperview?.subviewFirstPossibleResponder
currentSuperview = currentSuperview?.superview
} while foundRepsonder == nil && currentSuperview != nil
guard let responder = foundRepsonder else {
return
}
DispatchQueue.main.async {
responder.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
private extension UIView {
var subviewFirstPossibleResponder: UIView? {
guard !canBecomeFirstResponder else { return self }
for subview in subviews {
if let firstResponder = subview.subviewFirstPossibleResponder {
return firstResponder
}
}
return nil
}
}
Here is an example how to use it to make TextField to autofocus (+ bonus utilise #FocusState new iOS 15 api).
extension View {
#ViewBuilder
func autofocus() -> some View {
if #available(iOS 15, *) {
modifier(AutofocusedViewModifiers.Modern())
} else {
modifier(AutofocusedViewModifiers.Legacy())
}
}
}
private enum AutofocusedViewModifiers {
struct Legacy: ViewModifier {
func body(content: Content) -> some View {
content
.background(FocusableUIView(isFirstResponder: isFocused))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
#State private var isFocused = false
}
#available(iOS 15, *)
struct Modern: ViewModifier {
func body(content: Content) -> some View {
content
.focused($isFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
#FocusState private var isFocused: Bool
}
}
Content view example:
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
TextField("placeholder", text: $text)
Text("some text")
}
.autofocus()
}
}
Since Responder Chain is not available to be consumed via SwiftUI, so we have to consume it using UIViewRepresentable.
Do have a look at the below link as I have made a workaround that can work similarly to the way we use to do using UIKit.
https://stackoverflow.com/a/61121199/6445871
It is my variant of the implementation, based on #Mojtaba Hosseini and #Matteo Pacini solutions.
I am still new to SwiftUI, so I won't guarantee the absolute correctness of the code, but it works.
I hope it would be helpful to someone.
ResponderView: It is a generic-responder view, that could be used with any UIKit view.
struct ResponderView<View: UIView>: UIViewRepresentable {
#Binding var isFirstResponder: Bool
var configuration = { (view: View) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> View { View() }
func makeCoordinator() -> Coordinator {
Coordinator($isFirstResponder)
}
func updateUIView(_ uiView: View, context: UIViewRepresentableContext<Self>) {
context.coordinator.view = uiView
_ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
configuration(uiView)
}
}
// MARK: - Coordinator
extension ResponderView {
final class Coordinator {
#Binding private var isFirstResponder: Bool
private var anyCancellable: AnyCancellable?
fileprivate weak var view: UIView?
init(_ isFirstResponder: Binding<Bool>) {
_isFirstResponder = isFirstResponder
self.anyCancellable = Publishers.keyboardHeight.sink(receiveValue: { [weak self] keyboardHeight in
guard let view = self?.view else { return }
DispatchQueue.main.async { self?.isFirstResponder = view.isFirstResponder }
})
}
}
}
// MARK: - keyboardHeight
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { ($0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
struct ResponderView_Previews: PreviewProvider {
static var previews: some View {
ResponderView<UITextField>.init(isFirstResponder: .constant(false)) {
$0.placeholder = "Placeholder"
}.previewLayout(.fixed(width: 300, height: 40))
}
}
ResponderTextField - It is a convenient text-field wrapper around ResponderView.
struct ResponderTextField: View {
var placeholder: String
#Binding var text: String
#Binding var isFirstResponder: Bool
private var textFieldDelegate: TextFieldDelegate
init(_ placeholder: String, text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.placeholder = placeholder
self._text = text
self._isFirstResponder = isFirstResponder
self.textFieldDelegate = .init(text: text)
}
var body: some View {
ResponderView<UITextField>(isFirstResponder: $isFirstResponder) {
$0.text = self.text
$0.placeholder = self.placeholder
$0.delegate = self.textFieldDelegate
}
}
}
// MARK: - TextFieldDelegate
private extension ResponderTextField {
final class TextFieldDelegate: NSObject, UITextFieldDelegate {
#Binding private(set) var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
}
struct ResponderTextField_Previews: PreviewProvider {
static var previews: some View {
ResponderTextField("Placeholder",
text: .constant(""),
isFirstResponder: .constant(false))
.previewLayout(.fixed(width: 300, height: 40))
}
}
And way to use that.
struct SomeView: View {
#State private var login: String = ""
#State private var password: String = ""
#State private var isLoginFocused = false
#State private var isPasswordFocused = false
var body: some View {
VStack {
ResponderTextField("Login", text: $login, isFirstResponder: $isLoginFocused)
ResponderTextField("Password", text: $password, isFirstResponder: $isPasswordFocused)
}
}
}
Expanding on #JoshuaKifer answer's above, if you're dealing with the navigation animation being glitchy when using Introspect to make a text field first responder. Use this:
import SchafKit
#State var field: UITextField?
TextField("", text: $value)
.introspectTextField { textField in
field = textField
}
.onDidAppear {
field?.becomeFirstResponder()
}
More details on this solution here.
The correct SwiftUI way is to use #FocusState as mentioned above. However this API is available only for iOS 15. If you are using iOS 14 or iOS 13 you can use the Focuser library which is modelled to follow Apple API.
https://github.com/art-technologies/swift-focuser
Here's an example code. You will notice that API looks almost exactly as Apple, however Focuser also offers to use keyboard to move first responder down the chain which is pretty handy.
If you're having any problem with #JoshuaKifer or #ahaze 's response,
I've solved mine by using the modifier on the parent class, instead of on the TextField itself.
What I was doing:
TextField("Placeholder text...", text: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
How I solved my problem:
YourParentStruct(searchInput: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
I'll put the definition of the parent struct below just for clearness
struct YourParentStruct: View {
#Binding var searchInput: String
var body: some View {
HStack {
TextField("Placeholder text...", text: $searchInput)
.padding()
.background(Color.gray)
.cornerRadius(10)
}
}
}
At its Simplest form in iOS 13 ( without using any third party sdk/repo , or if you havent upgraded to iOS 14 to utilise the focus modifiers)
struct CustomTextField: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
UITextField(frame: .zero)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
}
}
Usage:
struct ContentView : View {
var body: some View {
CustomTextField()
.frame(width: 300, height: 50)
.background(Color.red)
}
}
I know its too late but if it helps anyone this is how I do it.
import SwiftUI
import Introspect
struct MyView: View {
#Binding var text1: String
#Binding var text2: String
#State private var toggleTF: Bool = false
var body: some View {
TextField("TextField 1", text: $text1)
.introspectTextField{ tF in
if toggleTF {
tF.becomeFirstResponder()
}
}
TextField("TextField 2", text: $text1)
.introspectTextField{ tF in
if !toggleTF {
tF.becomeFirstResponder()
}
}
Button("Toggle textfields") {
toggleTF.toggle()
}
}
}
As SwiftUI 2 doesn't support first responders yet I use this solution. It is dirty, but might work for some use cases when you only have 1 UITextField and 1 UIWindow.
import SwiftUI
struct MyView: View {
#Binding var text: String
var body: some View {
TextField("Hello world", text: $text)
.onAppear {
UIApplication.shared.windows.first?.rootViewController?.view.textField?.becomeFirstResponder()
}
}
}
private extension UIView {
var textField: UITextField? {
subviews.compactMap { $0 as? UITextField }.first ??
subviews.compactMap { $0.textField }.first
}
}