SwiftUI change TextBox placeholder based on HttpResponse - swift

I am learning SwiftUI and I am trying to implement a Forgot Password Functionality . The text field will say by default Enter your email then an http call takes places if the email is found in our system then I would like the Text PlaceHolder to Say "Enter Verification Code" . I already have everything else working . This is my code below. They enter their email then the HTTP call handles the rest and returns either a 0 or 1 in a closure depending on if the email is found . In the code below if the foundEmail is 1 then the Text placeholder should change to Enter Verification Code
struct ForgotPassWordView: View {
#State private var textResponse = ""
var body: some View {
ZStack
{
Color.black
VStack {
NavigationView {
Form {
Section {
TextField("Enter Email", text: $textResponse)
// change to Verification Code if foundEmail is 1
}
Section {
HStack {
Spacer()
Button(action: {
if !self.textResponse.isEmpty {
_ = ForgotPasswordRequest(email: self.textResponse, section: 1) {(foundEmail) in
if foundEmail == 0 {
// not found do nothing
} else if foundEmail == 1
{
// found email change to : Enter Verification Code
}
}
}
}) {
Spacer()
Text("Submit").fontWeight(.bold).frame(width: 70.0)
Spacer()
}
Spacer()
}
}
}
.navigationBarTitle(Text(""))
}
.padding(.horizontal, 15.0)
Text("Error Response").foregroundColor(.white)
}
.frame(height: 400.0)
}.edgesIgnoringSafeArea(.all)
}
}

Hello there I think you can use two TextField and switch views using a boolean because the placeHolder of text field not Binding so it cannot be edit... let me show what I mean in code:
struct ContentView: View {
#State var email: String = ""
#State var verificationCode: String = ""
#State var showCodeField: Bool = false
var body: some View {
NavigationView {
VStack {
if (!showCodeField) {
TextField("Enter Valid Email", text: $email)
} else {
TextField("Enter Verification Code", text: $verificationCode)
}
Button(action: {
self.showCodeField.toggle()
}) {
Text("Verify Email")
}
}
.padding()
}
}
}
also if you have a complex view you can extract them as variables and control which one is visible or not like this:
var someField: some View {
ZStack(alignment: .center) {
RoundedRectangle(cornerRadius: 8).foregroundColor(Color.white)
TextField("Enter Email", text: $email)
}
}
}
and in body you just call it like this
var body: some View {
VStack {
if(isVisible) {
someField
}
}

Related

How can I give a swiftUI button multiple functions when pressed?

I created a page for users to register new accounts. I created a "continue" button that is meant to push the new data to firebase and simultaneously move the users to the next view which is my mapView(). Right now the signup fucntion is working, but I cant figure out how to implement the mapView()
Button(action:{
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
} , label: {
Text("Continue")
.foregroundColor(.white)
.frame(width: 350, height: 35)
.background(Color.blue)
.cornerRadius(20)
})
}
Ive tried adding map view inside of the function but Xcode returns a warning that says "Result of 'mapView' initializer is unused".
Button(action:{
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
mapView()
} , label: {
Text("Continue")
.foregroundColor(.white)
.frame(width: 350, height: 35)
.background(Color.blue)
.cornerRadius(20)
})
There are a few ways to pull this off, but using a NavigationStack works really well. Something like this:
//
// ContentView.swift
// animation-example
//
// Created by Andrew Carter on 12/15/22.
//
import SwiftUI
enum SignUpFlow {
case name
case age
case welcome
}
struct SignUpInfo {
var name: String
var age: String
}
struct NameView: View {
#Binding var name: String
var body: some View {
VStack {
TextField("Name", text: $name)
NavigationLink("Next", value: SignUpFlow.age)
.disabled(name.isEmpty)
}
}
}
struct AgeView: View {
#Binding var age: String
var body: some View {
VStack {
TextField("Age", text: $age)
NavigationLink("Next", value: SignUpFlow.welcome)
.disabled(age.isEmpty)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome")
}
}
struct ContentView: View {
#State private var path: [SignUpFlow] = []
#State private var signUpInfo = SignUpInfo(name: "", age: "")
var body: some View {
NavigationStack(path: $path) {
NavigationLink("Create Account", value: SignUpFlow.name)
.navigationDestination(for: SignUpFlow.self) { flow in
switch flow {
case .age:
AgeView(age: $signUpInfo.age)
case .name:
NameView(name: $signUpInfo.name)
case .welcome:
WelcomeView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In the example above with mapView() just placed in the buttons action, it's unused because, well, it's unused. The Button closure expects a Void return, you're just making a map view than instantly throwing it away- it's not used in any way.

Why does the value change between views? SwiftUi

I am creating an app in which there is a list of users, with an associated button for each user that leads to another screen. This screen should display all the data of the selected user.
struct UserList: View {
#StateObject var administratorManager = AdministratorManager()
#State var ricerca = ""
#State var isPresented: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ForEach(administratorManager.users.sorted(using: [
KeyPathComparator(\.surname, order: .forward)
])) { user in
if user.toCheck.contains(where: {$0.range(of: ricerca.lowercased()) != nil}) || user.toCheck.contains(where: {$0.range(of: ricerca.capitalized) != nil}) || ricerca.isEmpty {
VStack(alignment: .leading) {
Text(user.number).font(.subheadline)
Text(user.name).font(.subheadline)
Text(user.surname).font(.subheadline)
}.background(Color("Purple"))
.cornerRadius(5)
// Button to change screen ---------------------------------------------------------------------------------------------------
Button {
isPresented.toggle()
administratorManager.infoUtente[2] = user.number
} label: {
Text("Tap me!")
.padding()
.background(Color.white)
.cornerRadius(20)
}.padding(1)
// ---------------------------------------------------------------------------------------------------------------------------
}
}.navigationBarTitle("User List")
.searchable(text: $ricerca, prompt: "Search")
.onAppear() {
administratorManager.fetchData(collection: "Users")
administratorManager.checkPermission()
}
.alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
.sheetWithDetents(
isPresented: $isPresented,
detents: [.medium(),.large()]
) {
print("The sheet has been dismissed")
} content: {
Group {
userInfo()
}
.font(.title)
}
}
}
}
This is the screen where the user information will be displayed:
struct userInfo: View {
// Oggetti.
#StateObject var administratorManager = AdministratorManager() // Oggetto riguardante i comandi riservati agli admin.
var body: some View {
HStack {
Text("Nome: \(administratorManager.infoUtente[0])")
.padding(.horizontal, 5)
Text("Cogome: \(administratorManager.infoUtente[1])")
.padding(.horizontal, 5)
}.padding(1)
Text("Numero: \(administratorManager.infoUtente[2])")
.padding(.horizontal, 5)
Button("print", action: {
print(administratorManager.infoUtente[2])
}).padding(1)
}
}
If I print in the console the value of administratorManager.infoUser [2] is empty.
I've recently been using Swift, and I haven't found anything to fix it. How can I solve?
You have two different AdministratorManager instances. One in UserList, second in userInfo. They are not connected and have different data.
Change to this in userInfo
#EnvironmentObject var administratorManager: AdministratorManager
And in UserList pass environment object to next screen
Group {
userInfo()
.environmentObject(administratorManager)
}

SWIFTUI/ How to delay .task and let it work only if the data from textFields are passed

I just tried to make an API app in SwiftUI with loveCalculator from rapidapi.com
The problem is that API first needs names from me before it gives me the results.
My program works but fetching data form API earlier that I want (when I click to show my data in next view, first show default data, then show the data that should be displayed when I click).
Also Is it possible to initialize #Published var loveData (in LoveViewModel) without passing any default data or empty String?
Something like make the data from LoveData optional ?
Can You tell me when I make mistake?
MY CODE IS :
LoveData (for api)
struct LoveData: Codable {
let percentage: String
let result: String
}
LoveViewModel
class LoveViewModel: ObservableObject {
#Published var loveData = LoveData(percentage: "50", result: "aaa")
let baseURL = "https://love-calculator.p.rapidapi.com/getPercentage?"
let myApi = "c6c134a7f0msh980729b528fe273p1f337fjsnd17137cb2f24"
func loveCal (first: String, second: String) async {
let completedurl = "\(baseURL)&rapidapi-key=\(myApi)&sname=\(first)&fname=\(second)"
guard let url = URL(string: completedurl) else {return }
do {
let decoder = JSONDecoder()
let (data, _) = try await URLSession.shared.data(from: url)
if let safeData = try? decoder.decode(LoveData.self, from: data) {
print("succesfully saved data")
self.loveData = safeData
}
} catch {
print(error)
}
}
}
LoveCalculator View
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onEditingChanged: { isBegin in
if isBegin == false {
print("finish get names")
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage, description: loveViewModel.loveData.result)
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
.task {
await loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
LoveResults View
struct LoveResults: View {
var percentage: String
var description: String
var body: some View {
ZStack{
Color(.green).ignoresSafeArea()
VStack{
Text("RESULTS :")
.padding()
Text(percentage)
.font(.system(size: 80, weight: .heavy))
.foregroundColor(.red)
.padding()
Text(description)
}
}
}
}
Thanks for help!
Regards,
Michal
.task is the same modifier as .onAppear in terms of view lifecycle, meaning it will fire immediately when the view appears. You have to move your API call to a more controlled place, where you can call it once you have all the required data.
If you want to fire the API request only after both names are entered, you can create a computed variable, that checks for desired state of TextFields and when that variable turns to true, then you call the API.
Like in this example:
struct SomeView: View {
#State var firstName: String = ""
#State var secondName: String = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty // << validation logic here
}
var body: some View {
Form {
TextField("Enter first name", text: $firstName)
TextField("Enter second name", text: $secondName)
}
.onChange(of: namesAreValid) { isValid in
if isValid {
// Call API
}
}
}
}
You can also set your loveData to optional using #Published var loveData: LoveData? and disable/hide the navigation link, until your data is not nil. In that case, you might need to provide default values with ?? to handle optional string errors in LoveResults view initializer
It was very helpful but still not enough for me, .onChange work even if I was type 1 letter in my Form.
I find out how to use Task { } and .onCommit on my TextField, now everything working well !
My code now looks like that :
LoveCalculator View :
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty
}
func makeApiCall() {
if namesAreValid {
DispatchQueue.main.async {
Task {
await self.loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName, onCommit: {makeApiCall()})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onCommit: {
makeApiCall()
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage ?? "", description: loveViewModel.loveData.result ?? "")
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
}
}
}
Thank You one more time for very helpful tip!
Peace!
MichaƂ ;)
Editing :
Just look at Apple documentation and I see that they say that .onCommit is deprecated whatever it means.
So instead of this I use .onSubmit and works the same !
TextField("second name", text: $secondName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
.onSubmit {
makeApiCall()
}
Peace! :)

Login Screen SwiftUI

I have TextFields for Login and SecureField for Password.
How can I go to another view when my Login and Password are correct?
struct LoginBoard: View {
#State private var login = "Tony"
#State private var password = "1234"
var body: some View {
ZStack {
VStack {
HStack {
Text("Enter Login and Password")
}
HStack {
Image(systemName: "person")
TextField("Login", text: $login)
}
HStack {
SecureField("Password", text: $password)
}
Button("Login") {
}
}
}
}
}
You should use NavigationView that's an equivalent to navigation controller in UIKit and use NavigationLink as the segue or trigger for navigation.
struct LoginBoard: View {
#State private var login = "Tony"
#State private var password = "1234"
#State isLoginSuccess = false
var body: some View {
// like navigation controller
// that handles the navigation of views
NavigationView {
// DestinationView is the view will go
// to if credentials is correct
NavigationLink(destination: DestinationView(),
isActive: $isLoginSuccess) { }
ZStack {
VStack {
HStack {
Text("Enter Login and Password")
}
HStack {
Image(systemName: "person")
TextField("Login", text: $login)
}
HStack {
SecureField("Password", text: $password)
}
Button("Login") {
// if user and password are correct change
// isLoginSuccess to true and will navigate
// to the next View
isLoginSuccess = true
}
}
}
}
}
}
Wrap your Stacks inside a NavigationView{} and use a NavigationLink{} to direct to another view. Sample code is below:
import SwiftUI
struct LoginBoard: View {
#State private var login = "Tony"
#State private var password = "1234"
var body: some View {
NavigationView {
ZStack {
VStack {
HStack {
Text("Enter Login and Password")
}
HStack {
Image(systemName: "person")
TextField("Login", text: $login)
}
HStack {
SecureField("Password", text: $password)
}
NavigationLink {
WelcomeView()
} label: {
Text("Login")
.foregroundColor(.blue)
}
.disabled((login == "Tony" &&
password == "1234") ? false : true)
}
}
}
}
}
struct WelcomeView: View {
var body: some View {
Text("welcome!")
}
}

Remove padding on TextEditor

My custom text editor below once you click on the pen to edit, a new space appears so the text from before is not on the same line as the new one. How can I fix this? Here's a simple reproducible example:
struct SwiftUIView: View {
#State var name: String = "test"
#State var showEdit: Bool = true
var body: some View {
HStack {
HStack {
if(showEdit) {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
} else {
Text(name)
.font(.headline)
}
}
Spacer()
Button(action: {
showEdit.toggle()
}) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
}
}
}
}
struct CustomTextEditor: View {
let placeholder: String
#Binding var text: String
var body: some View {
ZStack {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color.primary.opacity(0.25))
}
TextEditor(text: $text)
}.onAppear() {
UITextView.appearance().backgroundColor = .clear
}.onDisappear() {
UITextView.appearance().backgroundColor = nil
}
}
}
I want it to have the same padding properies as inserting a simple Text("") so when I switch between Text("xyz") and TextEditor(text: $xyz) it has the same padding alignment. Right now TextEditor has a weird padding.
You will drive yourself insane trying to line up a Text and a TextEditor (or a TextField, for that matter), so don't try. Use another, disabled, TextEditor instead, and control the .opacity() on the top one depending upon whether the bound variable is empty or not. Like this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
}
}
}
This view will show the placeholder if there is no text, and hide the placeholder as soon as there is text.
Edit:
You don't need the button, etc. in your other view. It becomes simply:
struct SwiftUIView: View {
#State var name: String = ""
var body: some View {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
.padding()
}
}
and if you need a "Done" button on the keyboard, change your CustomTextEditor() to this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
#FocusState var isFocused: Bool
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
.focused($isFocused)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
isFocused = false
} label: {
Text("Done")
.foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
}
}