Get Live Text when press button - swift

UIAction.captureTextFromCamera(responder: context.coordinator, identifier: nil)
This method must be placed in a Menu before it can be used. What if I want to call this method when I press the Button?
And I wanna use it in the SwiftUI, like this
#State var text = ""
Text(text)
Menu {
Button(action: {
// TODO: support live text
}) {
Label("Live Text", systemImage: "camera.viewfinder")
}
} label: {
Text("Hi")
}

import SwiftUI
#available(iOS 15.0, *)
struct ScanTextView: View {
#State var text: String = ""
#State var message: String = ""
var body: some View {
VStack {
Text(message)
HStack{
TextField("Enter text",text: $text)
ScanTextView_UI(text: $text, type: .buttonMenu,imageSystemName: "ellipsis.circle", additionalActions: [
//https://developer.apple.com/documentation/uikit/uiaction
UIAction(title: "Refresh") { (action) in
//Just sample
self.message = "refresh"
},
UIAction(title: "Anything", image: UIImage(systemName: "checkmark")) { (action) in
//Just sample
self.message = "anything"
},
UIAction(title: "Clear message") { (action) in
//Just sample
self.message = ""
}
])
}.border(Color.red)
TextField("Enter text",text: $text)
.border(Color.yellow)
.toolbar(content: {
ToolbarItem(placement: .keyboard, content: {
ScanTextView_UI(text: $text, type: .buttonMenu)
})
})
ScanTextView_UI(text: $text, type: .textField, additionalActions: [
UIAction(title: "Anything", image: UIImage(systemName: "checkmark")) { (action) in
//Just sample
self.message = "anything"
}
]).border(Color.orange)
HStack{
TextField("Enter text",text: $text)
ScanTextView_UI(text: $text, type: .button)
}.border(Color.green)
}
}
}
#available(iOS 15.0, *)
struct ScanTextView_UI: UIViewRepresentable {
#Binding var text: String
let type: Types
var imageSystemName: String = "text.viewfinder"
var additionalActions: [UIAction] = []
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> some UIView {
let textFromCamera = UIAction.captureTextFromCamera(responder: context.coordinator, identifier: nil)
let image = UIImage(systemName: imageSystemName)
//button
let button = UIButton(frame: .zero, primaryAction: textFromCamera)
button.setImage(image, for: .normal)
//buttonMenu
// A UIButton can hold the menu, it is a long press to get it to come up
let buttonMenu = UIButton()
var temp: [UIAction] = [textFromCamera]
for elem in additionalActions{
temp.append(elem)
}
let cameraMenu = UIMenu(children: temp)
buttonMenu.setImage(image, for: .normal)
buttonMenu.menu = cameraMenu
//TextField
// Or a textField
let toolbarItem = UIBarButtonItem(title: nil, image: image, primaryAction: textFromCamera, menu: nil)
var temp2: [UIBarButtonItem] = [toolbarItem]
for elem in additionalActions{
temp2.append(UIBarButtonItem(title: elem.title, image: elem.image, primaryAction: elem, menu: nil))
}
let textField = UITextField()
let bar = UIToolbar()
bar.items = temp2
bar.sizeToFit()
textField.inputAccessoryView = bar
textField.placeholder = "Enter text"
textField.delegate = context.coordinator
textField.text = text
var result: UIView = UIView()
switch type {
case .textField:
result = textField
case .buttonMenu:
result = buttonMenu
case .button:
if !additionalActions.isEmpty{
result = buttonMenu
}else{
result = button
}
}
return result
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if uiView is UITextField{
(uiView as! UITextField).text = text
}
}
//Making the Coordinator a UIResponder as! UIKeyInput gives access to the text
class Coordinator: UIResponder, UIKeyInput, UITextFieldDelegate{
var hasText: Bool{
!parent.text.isEmpty
}
let parent: ScanTextView_UI
init(_ parent: ScanTextView_UI){
self.parent = parent
}
func insertText(_ text: String) {
//Update the #Binding
parent.text = text
}
func deleteBackward() { }
// MARK: UITextFieldDelegate
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
enum Types{
case textField
case buttonMenu
case button
}
}
#available(iOS 15.0, *)
struct ActionMenuView_Previews: PreviewProvider {
static var previews: some View {
ScanTextView()
}
}

Related

#FetchRequest predicate is ignored if the context changes

I have a simple SwiftUI application with CoreData and two views. One view displays all "Place" objects. You can create new places and you can show the details for the place.
Inside the second view you can add "PlaceItem"s to a place.
The problem is that, once a new "PlaceItem" is added to the viewContext, the #NSFetchRequest seems to forget about its additional predicates, which I set in onAppear. Then every place item is shown inside the details view. Once I update the predicate manually (the refresh button), only the items from the selected place are visible again.
Any idea how this can be fixed? Here's the code for my two views:
struct PlaceView: View {
#FetchRequest(sortDescriptors: []) private var places: FetchedResults<Place>
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
List(places) { place in
NavigationLink {
PlaceItemsView(place: place)
} label: {
Text(place.name ?? "")
}
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let place = Place(context: viewContext)
place.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
}
.navigationTitle("Places")
}
}
struct PlaceItemsView: View {
#ObservedObject var place: Place
#State var searchText = ""
#FetchRequest(sortDescriptors: []) private var items: FetchedResults<PlaceItem>
#Environment(\.managedObjectContext) private var viewContext
func updatePredicate() {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %#", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %#", searchText))
}
items.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
NavigationView {
List(items) { item in
Text(item.name ?? "");
}
}
.onAppear(perform: updatePredicate)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.onAppear(perform: updatePredicate)
.onChange(of: searchText, perform: { _ in
updatePredicate()
})
.navigationTitle(place.name ?? "")
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
PlaceView()
}
}
}
Thanks!
Rather than relying on onAppear, you might have more luck defining your fetchRequest in PlaceItemView’s initializer, e.g.:
struct PlaceItemsView: View {
#ObservedObject private var place: Place
#FetchRequest private var items: FetchedResults<PlaceItem>
init(place: Place) {
self.place = place
self._items = FetchRequest(
entity: PlaceItem.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "place == %#", place)
)
}
It's best to break Views up by the data they need, its called having a tighter invalidation and solves your problem of updating the fetch request too! e.g.
struct PlaceItemsView {
#FetchRequest private var items: FetchedResults<PlaceItem>
var body: some View {
List(items) { item in
Text(item.name ?? "")
}
}
}
// body called when either searchText is different or its a different place (not when the place's properties change because no need to)
struct SearchPlaceView {
let searchText: String
let place: Place
var predicate: NSPredicate {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %#", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %#", searchText))
}
return NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
PlaceItemsView(items:FetchRequest<PlaceItem>(sortDescriptors:[], predicate:predicate))
}
}
// body called when either the place has a change or search text changes
struct PlaceItemsView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State var searchText = ""
var body: some View {
NavigationView {
SearchPlaceView(searchTest:searchText, place: place)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.navigationTitle(place.name ?? "")
}
}
Always try to break the Views up by the smallest data their body needs, try to not think about breaking them up by screens or areas of a screen which is a very common mistake.
import UIKit
import CoreData
class LoginViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
//MARK: - Outlets
#IBOutlet weak var LogInDataTableView: UITableView!
#IBOutlet weak var btnLogIn: UIButton!
#IBOutlet weak var btnAdd: UIButton!
//MARK: - Variables
var fetchedCoreData = [NSManagedObject]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var name = String()
let placeHolderArray = ["username", "password"]
var isLogin = false
//MARK: - Methods and other functionalities
override func viewDidLoad() {
super.viewDidLoad()
btnLogIn.layer.cornerRadius = 20.0
LogInDataTableView.layer.cornerRadius = 20.0
LogInDataTableView.layer.borderWidth = 2.0
LogInDataTableView.layer.borderColor = UIColor.blue.cgColor
let tap = UITapGestureRecognizer(target: self, action: #selector(UIInputViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
//MARK: - dismiss keyboard when tap on screen
#objc func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
fetchRequest()
}
//MARK: - fetch saved data from coredata
func fetchRequest() {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "EntityName")
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
fetchedCoreData = result as! [NSManagedObject]
} catch {
print("error while fetch data from database")
}
}
//MARK: - login button click with validation
#IBAction func btnLogInClick(_ sender: Any) {
if loginData[0].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter username", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[1].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter password", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[2].isEmpty{
let alert = UIAlertController(title: "Alert", message: "Please enter id", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else {
let registeredUser = clinicData.contains { objc in
objc.value(forKey: "username") as! String == loginData[0] && objc.value(forKey: "password") as! String == loginData[1]
}
if registeredUser == true {
for data in clinicData {
if data.value(forKey: "username") as! String == loginData[0] && data.value(forKey: "password") as! String == loginData[1] {
clinicName = data.value(forKey: "clinic") as! String
let addClinicVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HomeScreenViewController") as! HomeScreenViewController
addClinicVC.clinicName = clinicName
self.navigationController?.pushViewController(addClinicVC, animated: true)
}
}
} else {
//MARK: - alert
let alert = UIAlertController(title: "Alert", message: "username and password mismatch", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
}
}
#IBAction func didTapAddClinic(_ sender: UIButton) {
let addVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NewScreenViewController") as! NewScreenViewController
self.navigationController?.pushViewController(addVC, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return placeHolderArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LoginTableViewCell", for: indexPath) as! LoginTableViewCell
cell.txtLoginData.tag = indexPath.row
cell.txtLoginData.placeholder = placeHolderArray[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}

SwiftUI hide a view by touching outside of its bounds

I'm trying to implement pop up alert, which basicly are view that appears on another view, how can i dismiss this alert view on top, when i tap outside of white box? screen
My logic of calling this alert: I have my main view, where i have facebook signIn button, which should call this alert view, my alert view is just another swiftui view, which has UIViewRepresentable object inside to make hyperlinks inside my text.
MAIN VIEW:
//
// WelcomeView.swift
//
//
//
//
import SwiftUI
import FacebookLogin
import SDWebImageSwiftUI
import NavigationStack
import AVKit
struct WelcomeView: View {
#State var showTermsAndConditionsAlert: Bool = false
ZStack { // 1 outer
ZStack { // 2 inner
VStack() {
ZStack(alignment: .top) {
// my video player here
HStack {
// some other ui elements
}
VStack {
// logo
}
}
}
HStack(alignment: .bottom) {
VStack {
Button(action: {
appLog.debug("Continue with Facebook")
showTermsAndConditionsAlert = true
}) {
AuthButtonView(imageIdentifier: "ic_facebook", buttonTitle: "welcome_page_sign_up_btn_fb".localized)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(5)
}
// email button sign up
}
// email button sign in
}) {
}
// push views
}
.ignoresSafeArea()
}
}
.padding(.bottom, Theme.pageBottomPadding)
if showTermsAndConditionsAlert {
TermsConditionsAlertView() {
self.showTermsAndConditionsAlert = false
self.viewModel.facebookLogin()
}
}
}
.onTapGesture(perform: {
// TODO: close dialog only when it is opened and press outside of dialog bound
self.showTermsAndConditionsAlert = false
})
}
.navigationBarTitle("", displayMode: .automatic)
.onAppear {
self.player.isMuted = true
self.player.play()
}
.onDisappear {
self.player.pause()
}
.onReceive(viewModel.$state.dropFirst(), perform: { state in
// switch state
}
})
}
}
ALERT VIEW: HyperLinkTextView - uirepresantable uiview
//
// TermsConditionsAlertView.swift
//
//
//
//
import SwiftUI
import NavigationStack
struct TermsConditionsAlertView: View {
var callback: (() -> ())?
var body: some View {
ZStack {
Color.black.opacity(0.8).edgesIgnoringSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
// Text here
HyperLinkTextView(text: "terms_conditions_alert_view_description".localized,
links: [Hyperlink(word: "terms_conditions".localized, url: NSURL(string: Constants.termsURL)!),
Hyperlink(word: "privacy_policy".localized, url: NSURL(string: Constants.privacyPolicyURL)!)])
Button(action: {
appLog.debug("Agree and sign up pressed")
callback?()
}) {
// button struct
}
}
}
}
}
HYPERLINK UI VIEW:
//
// HyperLinkTextView.swift
//
//
//
//
import SwiftUI
struct Hyperlink {
var word: String
var url: NSURL
}
struct HyperLinkTextView: UIViewRepresentable {
private var text: String
private var links: [Hyperlink]
init(text: String, links: [Hyperlink]) {
self.text = text
self.links = links
}
func makeUIView(context: Self.Context) -> UITextView {
let attributedString = NSMutableAttributedString(string: text)
links.forEach { hyperlink in
let linkAttributes = [NSAttributedString.Key.link: hyperlink.url]
var nsRange = NSMakeRange(0, 0)
if let range = text.range(of: hyperlink.word) {
nsRange = NSRange(range, in: text)
}
attributedString.setAttributes(linkAttributes, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSNumber(value: 1), range: nsRange)
}
let textView = UITextView()
textView.isEditable = false
textView.delegate = context.coordinator
textView.attributedText = attributedString
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue]
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = UIFont(name: "ArialMT", size: 18)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
public class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
weak var textView: UITextView?
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction, replacementText text: String) -> Bool {
return true
}
}
}
Make a full screen view with a ZStack containing a view that is translucent, and put your alert on top of that. Add a tap gesture to the translucent view that dismisses the entire view.

Keyboard Calls OnAppear of Other Views in TabBar SwiftUI 2.0

I am using UITabBarController in SwiftUI 2.0 and Xcode 12 but seems like Keyboard cases some unexpected behavior. As you can see from the below GIF, OnAppear of the other 2 tab's view called when the keyboard appears in the first tab. That is causing the issue as I have an API call written on appear.
Also, is there any way I can turn off the default view offset behavior of Xcode 12.
Here is my code of Content View.
struct ContentView: View {
#State private var index:Int = 0
var menuItems:[String] = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView(content: {
ZStack{
MyTabView(selectedIndex: self.$index)
.view(item: self.item1) {
NewView(title: "Hello1").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item2) {
NewView(title: "Hello2").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item3) {
NewView(title: "Hello3").navigationBarTitle("")
.navigationBarHidden(true)
}
}.navigationBarHidden(true)
.navigationBarTitle("")
})
}
var item1:MyTabItem {
var item = MyTabItem()
item.imageName = "pencil.circle"
item.selectedImageName = "pencil.circle.fill"
return item
}
var item2:MyTabItem {
var item = MyTabItem()
item.imageName = "pencil.circle"
item.selectedImageName = "pencil.circle.fill"
return item
}
var item3:MyTabItem {
var item = MyTabItem()
item.imageName = "pencil.circle"
item.selectedImageName = "pencil.circle.fill"
return item
}
}
struct NewView:View {
#State var text:String = ""
var title:String
var body: some View {
VStack {
Spacer()
Text("Hello")
TextField(title, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.onAppear {
debugPrint("OnApper \(self.title)")
}
}
}
and here is the code for CustomTabView.
class MyTabViewViewModel:ObservableObject {
var controllers: [UIViewController] = []
var tabItems:[MyTabItem] = []
}
struct MyTabItem {
var imageName:String = ""
var selectedImageName:String = ""
var hasDarkModeSupport:Bool = true
var image:UIImage?
var selectedImage:UIImage?
}
struct MyTabView: UIViewControllerRepresentable {
var viewModel:MyTabViewViewModel = MyTabViewViewModel()
#Binding var selectedIndex: Int
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = self.viewModel.controllers
tabBarController.delegate = context.coordinator
tabBarController.selectedIndex = 0
let appearance = tabBarController.tabBar.standardAppearance
appearance.shadowImage = nil
appearance.shadowColor = nil
appearance.backgroundEffect = nil
tabBarController.tabBar.standardAppearance = appearance
tabBarController.tabBar.shadowImage = UIImage()
tabBarController.tabBar.backgroundImage = UIImage()
tabBarController.tabBar.layer.shadowPath = UIBezierPath(rect: tabBarController.tabBar.bounds).cgPath
tabBarController.tabBar.layer.shadowOffset = CGSize.init(width: 0, height: -3)
tabBarController.tabBar.layer.shadowRadius = 5
tabBarController.tabBar.layer.shadowColor = UIColor.black.cgColor
tabBarController.tabBar.layer.shadowOpacity = 0.25
tabBarController.tabBar.backgroundColor = UIColor.white
tabBarController.tabBar.barTintColor = UIColor.white
self.updateTabItems(forTabBarController: tabBarController)
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) {
tabBarController.selectedIndex = selectedIndex
self.updateTabItems(forTabBarController: tabBarController)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateTabItems(forTabBarController tabBarController:UITabBarController) {
let isDarkModeEnable:Bool = tabBarController.traitCollection.userInterfaceStyle == .dark
for (index, tabItem) in self.viewModel.tabItems.enumerated() {
tabBarController.tabBar.items?[index].title = ""
if let image = tabItem.image {
tabBarController.tabBar.items?[index].image = image
if let selectedImage = tabItem.selectedImage {
tabBarController.tabBar.items?[index].selectedImage = selectedImage
}
} else {
if tabItem.hasDarkModeSupport && isDarkModeEnable {
if let image = UIImage.init(systemName: "\(tabItem.imageName)-dark") {
tabBarController.tabBar.items?[index].image = image
} else if let image = UIImage.init(systemName: tabItem.imageName) {
tabBarController.tabBar.items?[index].image = image
}
if let selectedImage = UIImage.init(systemName: "\(tabItem.selectedImageName)-dark") {
tabBarController.tabBar.items?[index].selectedImage = selectedImage
} else if let selectedImage = UIImage.init(systemName: tabItem.selectedImageName) {
tabBarController.tabBar.items?[index].selectedImage = selectedImage
}
} else {
if let image = UIImage.init(systemName: tabItem.imageName) {
tabBarController.tabBar.items?[index].image = image
}
if let selectedImage = UIImage.init(systemName: tabItem.selectedImageName) {
tabBarController.tabBar.items?[index].selectedImage = selectedImage
}
}
}
}
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: MyTabView
init(_ tabBarController: MyTabView) {
self.parent = tabBarController
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
parent.selectedIndex = tabBarController.selectedIndex
}
}
func view<HostedView:View>(item:MyTabItem, #ViewBuilder sheet: #escaping () -> HostedView) -> MyTabView {
self.viewModel.controllers.append(UIHostingController.init(rootView: sheet()))
self.viewModel.tabItems.append(item)
return self
}
}
Having the same issue myself
"Hackish" workaround is to wrap the NewView.body in a List:
#State var text:String = ""
var title:String
var body: some View {
List {
VStack {
Spacer()
Text("Hello")
TextField(title, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.onAppear {
debugPrint("OnApper \(self.title)")
}
}
}
}
Could also work to use a LazyVStack, but haven't gotten to test it as my project targets 13.x
Same issue here OnAppear calls unexpectedly when Keyboard Appears in SwiftUI

Pass from a Binding<String> TextField to a Binding<Int> in SwiftUI

I'm developing a SwiftUI app and I want to recreate the effect of passing from a textField to the following one and, to do that, I need to wrap a TextField from UIKit which has the keyboard option "next" and "done".
I've this situation: 1 TextField (SwiftUI style) which accepts only a String and two TextFields (wrapped from UIKit) which accepts only Ints.
My aim is to make the TextField Binding the first responder of the View and connect the key "next" pressed by the user to the first TextField Binding and, thereafter, the second textField Binding.
I've tried to implement the Binding inside the wrapper but I've got some problems because I don't know how to fill the init inside the View with the proper bindings.
I would really appreciate some help!
struct LinkedTextFields: View {
#State var name : String = ""
#State var number_1 : Int = 100
#State var number_2 : Int = 10
#State var focused: [Bool] = [true, false, false]
var body: some View {
NavigationView{
VStack {
VStack {
Form {
TextField("Name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
HStack {
Text("Number 1:")
Spacer()
TextFieldWrapped(keyboardType: .namePhonePad, returnVal: .next, tag: 0, number: self.$number_1, isfocusAble: self.$focused)
}
HStack {
Text("Number 2:")
Spacer()
TextFieldWrapped(keyboardType: .namePhonePad, returnVal: .done, tag: 1, number: self.$number_2, isfocusAble: self.$focused)
}
}
}
}
.navigationBarTitle("Add product")
}
}
}
struct TextFieldWrapped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
#Binding var number: Int
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.keyboardType = self.keyboardType
textField.returnKeyType = self.returnVal
textField.tag = self.tag
textField.delegate = context.coordinator
textField.autocorrectionType = .no
textField.textAlignment = .center
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
uiView.text = String(number)
}
func makeCoordinator() -> Coordinator {
Coordinator(self, $number)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapped
var number: Binding<Int>
init(_ textField: TextFieldWrapped,_ number: Binding<Int>) {
self.parent = textField
self.number = number
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.number = Int(textField.text ?? "") ?? 0
}
else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.number = Int(textField.text ?? "") ?? 0
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
if reason == .committed {
textField.resignFirstResponder()
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
let text = textField.text as NSString?
let newValue = text?.replacingCharacters(in: range, with: string)
if let number = Int(newValue ?? "0") {
self.number.wrappedValue = number
return true
} else {
if nil == newValue || newValue!.isEmpty {
self.number.wrappedValue = 1
}
return false
}
}
}
}

Autofocus TextField programmatically in SwiftUI

I'm using a modal to add names to a list. When the modal is shown, I want to focus the TextField automatically, like this:
I've not found any suitable solutions yet.
Is there anything implemented into SwiftUI already in order to do this?
Thanks for your help.
var modal: some View {
NavigationView{
VStack{
HStack{
Spacer()
TextField("Name", text: $inputText) // autofocus this!
.textFieldStyle(DefaultTextFieldStyle())
.padding()
.font(.system(size: 25))
// something like .focus() ??
Spacer()
}
Button(action: {
if self.inputText != ""{
self.players.append(Player(name: self.inputText))
self.inputText = ""
self.isModal = false
}
}, label: {
HStack{
Text("Add \(inputText)")
Image(systemName: "plus")
}
.font(.system(size: 20))
})
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
Spacer()
}
.navigationBarTitle("New Player")
.navigationBarItems(trailing: Button(action: {self.isModal=false}, label: {Text("Cancel").font(.system(size: 20))}))
.padding()
}
}
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, for example, you can set the focusedField property in the code to make the binded textField become active:
Resign first responder ( Dismiss keyboard )
or dismiss the keyboard by setting the variable to nil:
Don't forget to watch the Direct and reflect focus in SwiftUI session from WWDC2021
iOS 13 and 14 (and 15)
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.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
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
}
Since Responder Chain is not presented to be consumed via SwiftUI, so we have to consume it using UIViewRepresentable.
I have made a workaround that can work similarly to the way we use to do using UIKit.
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
#Binding var nextResponder : Bool?
#Binding var isResponder : Bool?
init(text: Binding<String>,nextResponder : Binding<Bool?> , isResponder : Binding<Bool?>) {
_text = text
_isResponder = isResponder
_nextResponder = nextResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder = false
if self.nextResponder != nil {
self.nextResponder = true
}
}
}
}
#Binding var text: String
#Binding var nextResponder : Bool?
#Binding var isResponder : Bool?
var isSecured : Bool = false
var keyboard : UIKeyboardType
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.isSecureTextEntry = isSecured
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.keyboardType = keyboard
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text, nextResponder: $nextResponder, isResponder: $isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isResponder ?? false {
uiView.becomeFirstResponder()
}
}
}
You can use this component like this...
struct ContentView : View {
#State private var username = ""
#State private var password = ""
// set true , if you want to focus it initially, and set false if you want to focus it by tapping on it.
#State private var isUsernameFirstResponder : Bool? = true
#State private var isPasswordFirstResponder : Bool? = false
var body : some View {
VStack(alignment: .center) {
CustomTextField(text: $username,
nextResponder: $isPasswordFirstResponder,
isResponder: $isUsernameFirstResponder,
isSecured: false,
keyboard: .default)
// assigning the next responder to nil , as this will be last textfield on the view.
CustomTextField(text: $password,
nextResponder: .constant(nil),
isResponder: $isPasswordFirstResponder,
isSecured: true,
keyboard: .default)
}
.padding(.horizontal, 50)
}
}
Here isResponder is to assigning responder to the current textfield, and nextResponder is to make the first response , as the current textfield resigns it.
SwiftUIX Solution
It's super easy with SwiftUIX and I am surprised more people are not aware about this.
Install SwiftUIX through Swift Package Manager.
In your code, import SwiftUIX.
Now you can use CocoaTextField instead of TextField to use the function .isFirstResponder(true).
CocoaTextField("Confirmation Code", text: $confirmationCode)
.isFirstResponder(true)
I think SwiftUIX has many handy stuff, but that is still the code outside of your control area and who knows what happens to that sugar magic when SwiftUI 3.0 comes out.
Allow me to present the boring UIKit solution slightly upgraded with reasonable checks and upgraded timing DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
// AutoFocusTextField.swift
struct AutoFocusTextField: UIViewRepresentable {
private let placeholder: String
#Binding private var text: String
private let onEditingChanged: ((_ focused: Bool) -> Void)?
private let onCommit: (() -> Void)?
init(_ placeholder: String, text: Binding<String>, onEditingChanged: ((_ focused: Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
_text = text
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UITextField, context:
UIViewRepresentableContext<AutoFocusTextField>) {
uiView.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // needed for modal view to show completely before aufo-focus to avoid crashes
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 ?? ""
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.onEditingChanged?(false)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.onEditingChanged?(true)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
parent.onCommit?()
return true
}
}
}
// SearchBarView.swift
struct SearchBarView: View {
#Binding private var searchText: String
#State private var showCancelButton = false
private var shouldShowOwnCancelButton = true
private let onEditingChanged: ((Bool) -> Void)?
private let onCommit: (() -> Void)?
#Binding private var shouldAutoFocus: Bool
init(searchText: Binding<String>,
shouldShowOwnCancelButton: Bool = true,
shouldAutofocus: Binding<Bool> = .constant(false),
onEditingChanged: ((Bool) -> Void)? = nil,
onCommit: (() -> Void)? = nil) {
_searchText = searchText
self.shouldShowOwnCancelButton = shouldShowOwnCancelButton
self.onEditingChanged = onEditingChanged
_shouldAutoFocus = shouldAutofocus
self.onCommit = onCommit
}
var body: some View {
HStack {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray500)
.font(Font.subHeadline)
.opacity(1)
if shouldAutoFocus {
AutoFocusTextField("Search", text: $searchText) { focused in
self.onEditingChanged?(focused)
self.showCancelButton.toggle()
}
.foregroundColor(.gray600)
.font(Font.body)
} else {
TextField("Search", text: $searchText, onEditingChanged: { focused in
self.onEditingChanged?(focused)
self.showCancelButton.toggle()
}, onCommit: {
print("onCommit")
}).foregroundColor(.gray600)
.font(Font.body)
}
Button(action: {
self.searchText = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray500)
.opacity(searchText == "" ? 0 : 1)
}.padding(4)
}.padding([.leading, .trailing], 8)
.frame(height: 36)
.background(Color.gray300.opacity(0.6))
.cornerRadius(5)
if shouldShowOwnCancelButton && showCancelButton {
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
}
}
#if DEBUG
struct SearchBarView_Previews: PreviewProvider {
static var previews: some View {
Group {
SearchBarView(searchText: .constant("Art"))
.environment(\.colorScheme, .light)
SearchBarView(searchText: .constant("Test"))
.environment(\.colorScheme, .dark)
}
}
}
#endif
// MARK: Helpers
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
// ContentView.swift
class SearchVM: ObservableObject {
#Published var searchQuery: String = ""
...
}
struct ContentView: View {
#State private var shouldAutofocus = true
#StateObject private var viewModel = SearchVM()
var body: some View {
VStack {
SearchBarView(searchText: $query, shouldShowOwnCancelButton: false, shouldAutofocus: $shouldAutofocus)
}
}
}
For macOS 13, there is a new modifier that does not require a delay. Currently, does not work on iOS 16.
VStack {
TextField(...)
.focused($focusedField, equals: .firstField)
TextField(...)
.focused($focusedField, equals: .secondField)
}.defaultFocus($focusedField, .secondField) // <== Here
Apple Documentation: defaultFocus()