Creating an iOS passcode view with SwiftUI, how to hide a TextView? - ios

I am trying to imitate a lock screen of iOS in my own way with some basic code. However I do not understand how to properly hide an input textview. Now I am using an opacity modifier, but it does not seem to be the right solution. Could you please recommend me better options?
import SwiftUI
public struct PasscodeView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var viewModel: ContentView.ViewModel
private let maxDigits: Int = 4
private let userPasscode = "1234"
#State var enteredPasscode: String = ""
#FocusState var keyboardFocused: Bool
#State private var showAlert = false
#State private var alertMessage = "Passcode is wrong, try again!"
public var body: some View {
VStack {
HStack {
ForEach(0 ..< maxDigits) {
($0 + 1) > enteredPasscode.count ?
Image(systemName: "circle") :
Image(systemName: "circle.fill")
}
}
.alert("Wrong Passcode", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
TextField("Enter your passcode", text: $enteredPasscode)
.opacity(0)
.keyboardType(.decimalPad)
.focused($keyboardFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
keyboardFocused = true
}
}
}
.padding()
.onChange(of: enteredPasscode) { _ in
guard enteredPasscode.count == maxDigits else { return }
passcodeValidation()
}
}
func passcodeValidation() {
if enteredPasscode == userPasscode {
viewModel.isUnlocked = true
dismiss()
} else {
enteredPasscode = ""
showAlert = true
}
}
}

Related

Swiftui view doesn't refresh when navigated to from a different view

I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!

Checking a toggled Button for a Quiz Game in SwiftUI

First, sorry for my bad English! I'm absolutely new to SwiftUI and I tried to create a Quiz App with multiple Choices and multiple Answers. I created a Button with a ForEach to Display the possible answers. Now I want to select the correct Answer and tap the check Button to validate the chosen Answer. There can be more then 1 correct Answer.
I tried this function but its only return, if there are one or two
//MARK:- Funktionen
func checkAnswer() {
if validateAnswer == quiz.correctAnswer {
print("Richtig")
} else {
print("Falsch")
}
}
I have no idea how to validate the chosen Answers with the correct answers. Can anyone help me?
Here is my Code:
QuizModel
struct Quiz: Identifiable {
var id: String = UUID().uuidString
var question: String
var howManyAnswers: String
var options: [PossibleAnswer]
var correctAnswer: [String]
var explain: String
}
extension Quiz: Equatable {}
struct PossibleAnswer : Identifiable, Equatable {
let id = UUID()
let text : String
}
ContentView
struct ContentView: View {
var quiz: Quiz
#State var isChecked:Bool = false
#State private var showAlert: Bool = false
#State var validateAnswer: [String] = ["Antwort 3", "Antwort 4"]
//MARK:- Answers
VStack {
ForEach(quiz.options) { answerOption in
QuizButtonView(isChecked: isChecked, title: answerOption.text)
.foregroundColor(.black)
.padding(2.0)
}
Spacer()
Divider()
HStack {
//MARK:- Button Überprüfen & Zurück
Button(action: {
print("Ich gehe zurück")
}, label: {
Text("Zurück")
})
Button(action: {
checkAnswer()
print("Ich überprüfe...")
self.showAlert.toggle()
}, label: {
Text("Überprüfen")
})
.padding(.leading, 200)
And my CheckButtonView
struct QuizButtonView: View {
#State var isChecked:Bool = false
var title:String
func toggle(){
isChecked.toggle()
if self.isChecked == true {
print("Antwort wurde ausgewählt")
} else if self.isChecked == false {
print("Antwort wurde wieder abgewählt")
}
}
var body: some View {
VStack {
Button(action: toggle) {
HStack{
Text(title)
.font(.system(size: 16))
.foregroundColor(.black)
.lineLimit(3)
Spacer()
Image(systemName: isChecked ? "checkmark.square.fill": "square")
}
}
Thank you!
You just need to create a state variable of an array of booleans:
struct ContentView: View {
private let quiz: Quiz
#State private var userSelections: [Bool]
init(quiz: Quiz) {
self.quiz = quiz
_userSelections = State(initialValue: Array(repeating: false, count: quiz.options.count))
}
var body: some View {
VStack {
ForEach(0..<quiz.options.count) { index in
QuizButtonView(isChecked: userSelections[index], title: quiz.options[index].text)
.foregroundColor(.black)
.padding(2.0)
}
}
}
func checkAnswer() {
let userSelectionTexts = Set(userSelections.enumerated().map({ quiz.options[$0.offset].text }))
let correctAnswers = Set(quiz.correctAnswer)
let isAllSelectionsTrue = userSelectionTexts == correctAnswers
let isAllSelectionsFalse = userSelectionTexts.intersection(correctAnswers).isEmpty
let isAnySelectionsTrue = !isAllSelectionsFalse
}
}

Displaying multiple overlays with SwiftUI

I'm trying to show multiple sheets with SwiftUI.
Overlay.swift
import SwiftUI
struct OverlayWith<Content: View>: View {
let content: () -> Content
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.compact.down")
.resizable()
.frame(width: 40, height: 15)
.accentColor(Color.gray)
.padding()
}
content()
Spacer()
}
}
}
struct OverlayView_Button1: View {
var body: some View{
Text("Button 1 triggered this overlay")
}
}
struct OverlayView_Button2: View {
var body: some View{
Text("Button 2 triggered this overlay")
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var overlayVisible: Bool = false
// Button States
#State var view1_visible: Bool = false
#State var view2_visible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if self.view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if self.view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
self.view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
self.view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
self.view1_visible = false
self.view2_visible = false
}
}
Compiling this code for iOS 13 works as expected. For iOS 14 on the other hand showSheetX() opens a sheet with an empty view. Dismissing the sheet and opening it again shows OverlayView_ButtonX. Debugging shows that viewX_visible is false when showSheetX() was called for the first time.
Is this a bug with iOS itself or am I missing something here?
Thank you in advance!
I think I found the solution for this issue:
ContentView.swift
import SwiftUI
var view1_visible: Bool = false // <- view1_visible is not a state anymore
var view2_visible: Bool = false // <- view2_visible is not a state anymore
struct ContentView: View {
#State var overlayVisible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
view1_visible = false
view2_visible = false
}
}
Feel free to explain to me why this code works. I'm really confused :S

SwiftUI/Combine no updates to #ObservedObject from #Published

I have a logon screen followed by an overview screen. When the user is successful at logon, the logon response sends back a list of items, which I want to display on the subsequent overview screen.
I can see the response being successfully mapped but the overview view is not receiving any update to the #ObservedObject. I could be missing something obvious but I've been through a bunch of articles and haven't managed to get anything working. Any help appreciated!
Logon view
import SwiftUI
struct LogonView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var inputError: Bool = false
#State private var errorMessage: String = ""
#State private var loading: Bool? = false
#State private var helpShown: Bool = false
#State private var successful: Bool = false
//MARK:- UIView
var body: some View {
NavigationView {
VStack {
VStack {
TextField("Username", text: $username)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 80)
VStack {
SecureField("Password", text: $password)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 40)
if (inputError) {
HStack {
Text(errorMessage)
.padding(.top)
.padding(.horizontal)
.foregroundColor(Color("SharesaveError"))
.animation(.easeInOut)
.lineLimit(nil)
.font(.footnote)
Spacer()
}
}
SharesaveButton(action: {self.submit(user: self.username, pass: self.password)},
label: "Log on",
loading: $loading,
style: .primary)
.padding(.top, 40)
.animation(.interactiveSpring())
NavigationLink(destination: OverviewView(), isActive: $successful) {
Text("")
}
Spacer()
}
.navigationBarTitle("Hello.")
.navigationBarItems(
trailing: Button(action: { self.helpShown = true }) {
Text("Need help?").foregroundColor(.gray)
})
.sheet(isPresented: $helpShown) {
SafariView( url: URL(string: "http://google.com")! )
}
}
}
//MARK:- Functions
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel()
resultsVM.getGrants(user: user, pass: pass,
successful: { response in
self.loading = false
if ((response) != nil) { self.successful = true }
},
error: { error in
self.inputError = true
self.loading = false
self.successful = false
switch error {
case 401:
self.errorMessage = "Your credentials were incorrect"
default:
self.errorMessage = "Something went wrong, please try again"
}
},
failure: { fail in
self.inputError = true
self.loading = false
self.successful = false
self.errorMessage = "Check your internet connection"
})
}
}
Results View Model
import Foundation
import Moya
import Combine
import SwiftUI
class ResultsViewModel: ObservableObject {
#Published var results: Results = Results()
func getGrants(
user: String,
pass: String,
successful successCallback: #escaping (Results?) -> Void,
error errorCallback: #escaping (Int) -> Void,
failure failureCallback: #escaping (MoyaError?) -> Void
)
{
let provider = MoyaProvider<sharesaveAPI>()
provider.request(.getSharesave(username: user, password: pass)) { response in
switch response.result {
case .success(let value):
do {
let data = try JSONDecoder().decode(Results.self, from: value.data)
self.results = data
successCallback(data)
} catch {
let errorCode = value.statusCode
errorCallback(errorCode)
}
case .failure(let error):
failureCallback(error)
}
}
}
}
Overview View
import SwiftUI
import Combine
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel()
var body: some View {
let text = "\(self.viewModel.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}
You submitted request to one instance of ResultsViewModel
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel() // << here
by try to read data from another instance of ResultsViewModel
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel() // << here
but it must be the one instance, so modify as follows
1) In OverviewView
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel // expects injection
2) In LogonView
struct LogonView: View {
#ObservedObject var resultsViewModel = ResultsViewModel() // created once
and inject same instance for OverviewView
NavigationLink(destination: OverviewView(viewModel: self.resultsViewModel), isActive: $successful) {
Text("")
}
and in submit
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = self.resultsViewModel // use created
Please try after change in initialise OverviewView like below in NavigationLink
NavigationLink(destination: OverviewView(viewModel: self.resultsVM),
isActive: $successful) {
Text("")
}
OR
pass results in OverviewView as argument like below
NavigationLink(destination: OverviewView(results: self.resultsVM.results),
isActive: $successful) {
Text("")
}
.....
struct OverviewView: View {
let results: Results
var body: some View {
let text = "\(self.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}

Focus on the next TextField/SecureField in SwiftUI

I've built a login screen in SwiftUI. I want to focus on the password SecureField when the user is finished entering their email. How can I do this?
struct LoginView: View {
#State var username: String = ""
#State var password: String = ""
var body: some View {
ScrollView {
VStack {
TextField("Email", text: $username)
.padding()
.frame(width: 300)
.background(Color(UIColor.systemGray5))
.cornerRadius(5.0)
.padding(.bottom, 20)
.keyboardType(.emailAddress)
SecureField("Password", text: $password)
.padding()
.frame(width: 300)
.background(Color(UIColor.systemGray5))
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
}, label: {
Text("Login")
.padding()
.frame(width: 300)
.background((username.isEmpty || password.isEmpty) ? Color.gray : Color(UIColor.cricHQOrangeColor()))
.foregroundColor(.white)
.cornerRadius(5.0)
.padding(.bottom, 20)
}).disabled(username.isEmpty || password.isEmpty)
iOS 15+
In iOS 15 we can now use #FocusState to control which field should be focused.
Here is an example how to add buttons above the keyboard to focus the previous/next field:
struct ContentView: View {
#State private var email: String = ""
#State private var username: String = ""
#State private var password: String = ""
#FocusState private var focusedField: Field?
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button(action: focusPreviousField) {
Image(systemName: "chevron.up")
}
.disabled(!canFocusPreviousField()) // remove this to loop through fields
}
ToolbarItem(placement: .keyboard) {
Button(action: focusNextField) {
Image(systemName: "chevron.down")
}
.disabled(!canFocusNextField()) // remove this to loop through fields
}
}
}
}
}
extension ContentView {
private enum Field: Int, CaseIterable {
case email, username, password
}
private func focusPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1) ?? .password
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1) ?? .email
}
}
private func canFocusPreviousField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue > 0
}
private func canFocusNextField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue < Field.allCases.count - 1
}
}
When using UIKit, one would accomplish this by setting up the responder chain. This isn't available in SwiftUI, so until there is a more sophisticated focus and responder system, you can make use of the onEditingChanged changed of TextField
You will then need to manage the state of each field based on stored State variables. It may end up being more work than you want to do.
Fortunately, you can fall back to UIKit in SwiftUI by using UIViewRepresentable.
Here is some code that manages the focus of text fields using the UIKit responder system:
import SwiftUI
struct KeyboardTypeView: View {
#State var firstName = ""
#State var lastName = ""
#State var focused: [Bool] = [true, false]
var body: some View {
Form {
Section(header: Text("Your Info")) {
TextFieldTyped(keyboardType: .default, returnVal: .next, tag: 0, text: self.$firstName, isfocusAble: self.$focused)
TextFieldTyped(keyboardType: .default, returnVal: .done, tag: 1, text: self.$lastName, isfocusAble: self.$focused)
Text("Full Name :" + self.firstName + " " + self.lastName)
}
}
}
}
struct TextFieldTyped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
#Binding var text: String
#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
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.text = textField.text ?? ""
} else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.text = textField.text ?? ""
}
return true
}
}
}
You can refer to this question to get more information about this particular approach.
Hope this helps!
I've improved on the answer from Gene Z. Ragan and Razib Mollick. Fixes a crash, this allows for any amount of textfields, supports passwords and made it into its own class.
struct UITextFieldView: UIViewRepresentable {
let contentType: UITextContentType
let returnVal: UIReturnKeyType
let placeholder: String
let tag: Int
#Binding var text: String
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.textContentType = contentType
textField.returnKeyType = returnVal
textField.tag = tag
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.clearButtonMode = UITextField.ViewMode.whileEditing
if textField.textContentType == .password || textField.textContentType == .newPassword {
textField.isSecureTextEntry = true
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if uiView.window != nil {
if isfocusAble[tag] {
if !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
} else {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UITextFieldView
init(_ textField: UITextFieldView) {
self.parent = textField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.text = textField.text ?? ""
}
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag)
return true
}
func setFocus(tag: Int) {
let reset = tag >= parent.isfocusAble.count || tag < 0
if reset || !parent.isfocusAble[tag] {
var newFocus = [Bool](repeatElement(false, count: parent.isfocusAble.count))
if !reset {
newFocus[tag] = true
}
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.isfocusAble = newFocus
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag + 1)
return true
}
}
}
struct UITextFieldView_Previews: PreviewProvider {
static var previews: some View {
UITextFieldView(contentType: .emailAddress,
returnVal: .next,
placeholder: "Email",
tag: 0,
text: .constant(""),
isfocusAble: .constant([false]))
}
}
Here you go - Native SwiftUI solution. Thanks Gene Z. Ragan for the link to SwiftUI Documentation in an earlier answer
struct TextFieldTest: View {
#FocusState private var emailFocused: Bool
#FocusState private var passwordFocused: Bool
#State private var username: String = ""
#State private var password: String = ""
var body: some View {
TextField("User name (email address)", text: $username)
.focused($emailFocused)
.onSubmit {
passwordFocused = true
}
TextField("Enter Password", text: $password)
.focused($passwordFocused)
}
}
I created a view Modifier that takes in a binding of #FocusState. This will automatically handle the next progression and clear the keyboard.
import SwiftUI
struct KeyboardToolsView<Content: View, T: Hashable & CaseIterable & RawRepresentable>: View where T.RawValue == Int {
var focusedField: FocusState<T?>.Binding
let content: Content
init(focusedField: FocusState<T?>.Binding, #ViewBuilder content: () -> Content) {
self.focusedField = focusedField
self.content = content()
}
var body: some View {
content
.toolbar {
ToolbarItem(placement: .keyboard) {
HStack {
Button(action: previousFocus) {
Image(systemName: "chevron.up")
}
.disabled(!canSelectPreviousField)
Button(action: nextFocus) {
Image(systemName: "chevron.down")
}
.disabled(!canSelectNextField)
Spacer()
Button("Done") {
focusedField.wrappedValue = nil
}
}
}
}
}
var canSelectPreviousField: Bool {
if let currentFocus = focusedField.wrappedValue {
return currentFocus.rawValue > 0
} else {
return false
}
}
var canSelectNextField:Bool {
if let currentFocus = focusedField.wrappedValue {
return currentFocus.rawValue < T.allCases.count - 1
} else {
return false
}
}
func previousFocus() {
if canSelectPreviousField {
selectPreviousField()
}
}
func nextFocus() {
if canSelectNextField {
selectNextField()
}
}
func selectPreviousField() {
focusedField.wrappedValue = focusedField.wrappedValue.map {
T(rawValue: $0.rawValue - 1)!
}
}
func selectNextField() {
focusedField.wrappedValue = focusedField.wrappedValue.map {
T(rawValue: $0.rawValue + 1)!
}
}
}
struct KeyboardToolsViewModifier<T: Hashable & CaseIterable & RawRepresentable>: ViewModifier where T.RawValue == Int {
var focusedField: FocusState<T?>.Binding
func body(content: Content) -> some View {
KeyboardToolsView(focusedField: focusedField) {
content
}
}
}
extension View {
func keyboardTools<T: Hashable & CaseIterable & RawRepresentable>(focusedField: FocusState<T?>.Binding) -> some View where T.RawValue == Int {
self.modifier(KeyboardToolsViewModifier<T>(focusedField: focusedField))
}
}
Example of how it can be used:
struct TransactionForm: View {
#State private var price: Double?
#State private var titleText: String = ""
#State private var date: Date = .now
#FocusState private var focusedField: Field?
// Having an enum that is Int and CaseIterable is important. As it will allow the view modifier to properly choose the next focus item.
private enum Field: Int, CaseIterable {
case title, price
}
var body: some View {
NavigationView {
Form {
TextField("Title", text: $titleText)
.focused($focusedField, equals: .title)
TextField("$0.00", value: $price, format: .currency(code: "USD"))
.focused($focusedField, equals: .price)
.keyboardType(.decimalPad)
DatePicker("Date", selection: $date, displayedComponents: [.date])
Section {
Button(action: {...}) {
Text("Add")
}
}
.listRowBackground(Color.clear)
}
.navigationTitle("Add Transaction")
.keyboardTools(focusedField: $focusedField)
}
}
}

Resources