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

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! :)

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.

NavigationLink moving to intended view then reverting back to previous view

Im working on a new social media app and Im having and issue with my navigation code.
Once a user fills out the registration form I want them to be prompted to upload the profile picture. The issue I am having is that it shows the intended view for a half second then moves right back to the registration view.
I have a RegistrationView that handles the UI and and a AuthViewModel that is taking care of the server side communications. Essentially when the user finishes entering the information and hits the button. The AuthViewModel takes over and send the info to firebase then triggers a Bool to be true.
I then had a NagivationLink on the RegistrationView that listens for that bool and when true, changes the view on the UI. Here is the code for that.
NavigationLink(destination: ProfilePhotoSelectorView(), isActive: $viewModel.didAuthenticateUser, label:{} )
XCode is spitting out that its been deprecated in iOS 16 and to move to the NavigationStack system they developed. But, with every guide I can see I cant get this to work. The only time I can get it to work is though the code above and returns this UI glitch.
Here is the full code for the RegistrationView
import SwiftUI
struct RegistrationView: View {
#State private var email = ""
#State private var username = ""
#State private var fullName = ""
#State private var password = ""
#State private var isVerified = false
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack{
NavigationLink(destination: ProfilePhotoSelectorView(), isActive: $viewModel.didAuthenticateUser, label:{} )
AuthHeaderView(title1: "Get Started.", title2: "Create Your Account.")
VStack(spacing: 40) {
CustomInputFields(imageName: "envelope", placeholderText: "Email", isSecureField: false, text: $email)
CustomInputFields(imageName: "person", placeholderText: "Username", isSecureField: false, text: $username)
CustomInputFields(imageName: "person", placeholderText: "Full Name", isSecureField: false, text: $fullName)
CustomInputFields(imageName: "lock", placeholderText: "Password", isSecureField: true, text: $password)
}
.padding(32)
Button {
viewModel.register(withEmail: email, password: password, fullname: fullName, username: username, isVerified: isVerified)
} label: {
Text("Sign Up")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340, height: 50)
.background(Color("AppGreen"))
.clipShape(Capsule())
.padding()
}
.shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
Spacer()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
HStack {
Text("Already Have And Account?")
.font(.caption)
Text("Sign In")
.font(.footnote)
.fontWeight(.semibold)
}
}
.padding(.bottom, 32)
.foregroundColor(Color("AppGreen"))
}
.ignoresSafeArea()
.preferredColorScheme(.dark)
}
}
struct RegistrationView_Previews: PreviewProvider {
static var previews: some View {
RegistrationView()
}
}
And here is the full code for the AuthViewModel
import SwiftUI
import Firebase
class AuthViewModel: ObservableObject {
#Published var userSession: Firebase.User?
#Published var didAuthenticateUser = false
init() {
self.userSession = Auth.auth().currentUser
print("DEBUG: User session is \(String(describing: self.userSession?.uid))")
}
func login(withEmail email: String, password: String){
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error\(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
print("Did log user in")
}
}
func register(withEmail email: String, password: String, fullname: String, username: String, isVerified: Bool){
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error\(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
print("DEBUG: Registerd User Succesfully")
let data = ["email": email, "username" :username.lowercased(), "fullname": fullname, "isVerified": isVerified, "uid": user.uid]
Firestore.firestore().collection("users")
.document(user.uid)
.setData(data) { _ in
self.didAuthenticateUser = true
}
}
}
func signOut() {
userSession = nil
try? Auth.auth().signOut()
}
}
Here is the code for the ProfilePhotoSelectorView
import SwiftUI
struct ProfilePhotoSelectorView: View {
var body: some View {
VStack {
AuthHeaderView(title1: "Account Creation:", title2: "Add A Profile Picture")
Button {
print("Pick Photo Here")
} label: {
VStack{
Image("PhotoIcon")
.resizable()
.renderingMode(.template)
.frame(width: 180, height: 180)
.scaledToFill()
.padding(.top, 44)
.foregroundColor(Color("AppGreen"))
Text("Tap To Add Photo")
.font(.title3).bold()
.padding(.top, 10)
.foregroundColor(Color("AppGreen"))
}
}
Spacer()
}
.ignoresSafeArea()
.preferredColorScheme(.dark)
}
}
struct ProfilePhotoSelectorView_Previews: PreviewProvider {
static var previews: some View {
ProfilePhotoSelectorView()
}
}
Tried all variations of the new NavigationStack and tried some other button code to see if i could trigger it from there. No Reso
Using NavigationLink in this way is not recommended, so I'm not surprised it would lead to buggy behavior. This is exacerbated by the fact that your RegistrationView is not placed within a NavigationView (deprecated) or NavigationStack, as these views provide most of the functionality of a navigation link.
Like you said, this usage of NavigationLink is deprecated. The isActive property has always been a little ambiguous in my opinion (from what I understand it is not meant as an 'activator' of the navigation link, rather as a way to read whether the link is active). The new way of presenting navigation links (using .navigationDestination) is much better.
Presenting a view using a boolean property
What you essentially want is to present the ProfilePhotoSelectorView when a boolean property is switched to true. This is common paradigm in SwiftUi, and there are many ways to do this, such as .sheet(isPresented:content:) or .popover(isPresented:content:). Note the isPresented parameter in both methods are boolean properties. Using .sheet, for example:
struct RegistrationView: View {
// ...
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack {
// ...
}
// Presents the photo selector view when `didAuthenticateUser` is true
.sheet(isPresented: $viewModel.didAuthenticateUser) {
ProfilePhotoSelectorView()
}
}
}
Adding a new view to the Navigation Tree
If you are adamant on using navigation links (i.e. you really want the ProfilePhotoSelectorView to be a node in the navigation tree), you're going to have to learn to use the new NavigationStack and append the view onto the path. This will require some retooling (and probably some reading on your part; here and here are good starting points). The view model would be the most likely place to control the stack, although you may eventually want to create a dedicated viewmodel. Here's a simple example:
struct ContentView: View {
#StateObject var viewModel = AuthViewModel()
var body: some View {
NavigationStack(path: $viewModel.navigationPath) {
RegistrationView()
.environmentObject(viewModel)
.navigationDestination(for: RegistrationScreen.self) { screen in
switch screen {
case .photoSelection:
ProfilePhotoSelectorView()
}
}
}
}
}
class AuthViewModel: ObservableObject {
// ...
// A new enum that defines the various types of possible views in the navigation stack
enum RegistrationScreen: Hashable {
case photoSelection
}
// The navigation path
#Published var navigationPath: [RegistrationScreen] = []
// Example usages of the navigation path. These functions show how to programmatically control the navigation stack
func showPhotoSelectionScreen() {
self.navigationPath.append(.photoSelection)
}
func goToRootOfNavigation() {
self.navigationPath = []
}
}
If I understand your question correctly, you want to use NavigationStack,
but it is not working for you.
There are many missing parts, but here is my attempt of using NavigationStack
to trigger the destination, given a change in viewModel.didAuthenticateUser.
struct ContentView: View {
#StateObject var viewModel = AuthViewModel()
var body: some View {
NavigationStack(path: $viewModel.didAuthenticateUser) { // <-- here
RegistrationView()
.environmentObject(viewModel)
}
}
}
struct RegistrationView: View {
#State private var email = ""
#State private var username = ""
#State private var fullName = ""
#State private var password = ""
#State private var isVerified = false
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack{
AuthHeaderView(title1: "Get Started.", title2: "Create Your Account.")
VStack(spacing: 40) {
CustomInputFields(imageName: "envelope", placeholderText: "Email", isSecureField: false, text: $email)
CustomInputFields(imageName: "person", placeholderText: "Username", isSecureField: false, text: $username)
CustomInputFields(imageName: "person", placeholderText: "Full Name", isSecureField: false, text: $fullName)
CustomInputFields(imageName: "lock", placeholderText: "Password", isSecureField: true, text: $password)
}
.padding(32)
Button {
viewModel.register(withEmail: email, password: password, fullname: fullName, username: username, isVerified: isVerified)
} label: {
Text("Sign Up")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340, height: 50)
.background(Color("AppGreen"))
.clipShape(Capsule())
.padding()
}
.shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
Spacer()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
HStack {
Text("Already Have And Account?")
.font(.caption)
Text("Sign In")
.font(.footnote)
.fontWeight(.semibold)
}
}
.padding(.bottom, 32)
.foregroundColor(Color("AppGreen"))
}
.navigationDestination(for: Bool.self) { _ in // <-- here
ProfilePhotoSelectorView()
}
.ignoresSafeArea()
.preferredColorScheme(.dark)
}
}
class AuthViewModel: ObservableObject {
#Published var userSession: Firebase.User?
#Published var didAuthenticateUser: [Bool] = [] // <-- here
init() {
self.userSession = Auth.auth().currentUser
print("DEBUG: User session is \(String(describing: self.userSession?.uid))")
}
func login(withEmail email: String, password: String){
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error\(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
print("Did log user in")
}
}
func register(withEmail email: String, password: String, fullname: String, username: String, isVerified: Bool){
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error\(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
print("DEBUG: Registerd User Succesfully")
let data = ["email": email, "username" :username.lowercased(), "fullname": fullname, "isVerified": isVerified, "uid": user.uid]
Firestore.firestore().collection("users")
.document(user.uid)
.setData(data) { _ in
self.didAuthenticateUser = [true] // <-- here
}
}
}
func signOut() {
userSession = nil
try? Auth.auth().signOut()
}
}
struct ProfilePhotoSelectorView: View {
var body: some View {
VStack {
AuthHeaderView(title1: "Account Creation:", title2: "Add A Profile Picture")
Button {
print("Pick Photo Here")
} label: {
VStack{
Image(systemName: "globe")
.resizable()
.renderingMode(.template)
.frame(width: 180, height: 180)
.scaledToFill()
.padding(.top, 44)
.foregroundColor(Color("AppGreen"))
Text("Tap To Add Photo")
.font(.title3).bold()
.padding(.top, 10)
.foregroundColor(Color("AppGreen"))
}
}
Spacer()
}
.ignoresSafeArea()
.preferredColorScheme(.dark)
}
}
You can only have one level of NavigationLinks unless you mark the links as isDetail(false) or use .navigationStyle(.stack).
The reason is because in landscape it uses a split view and the link replaces the large detail pane on the right.

Update search results from api when value changes

I have a search bar that will perform a search function based off user input. When the user searches a list is populated (ie. this is a meal app, so if they search for "eggs" a list will be populated showing results of search)
My issue is when the user completes there first search and wants to type again to find a new value (food), the list does not populate again. The API still makes the call, but I'm having trouble updating the list. I tried adding removeAll() to the array onSubmit but it didn't work as expected.
struct FoodSearchResultsView: View {
//calls API
#EnvironmentObject private var foodApi: FoodApiSearch
//textfield input
#State private var searchResultsItem = ""
//if toggled, will display, binded to search bar
#Binding var userSearch: Bool
//var holds if textfield typing is complete by user
#Binding var textComplete: Bool
//triggers select breakfast, lunch, dinner optins
//when false, api results will not display
#State private var isViewSearching = false
var body: some View {
if userSearch{
VStack{
Text(isViewSearching ? "Results" : "Searching..")
Spacer()
// delays showing api call
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
self.isViewSearching = true
}
}
//if user has completed searching for a food
if isViewSearching{
List(foodApi.userSearchResults){meal in
VStack{
HStack{
VStack(alignment: .leading){
Text(meal.mealName)
HStack{
Text(meal.calories + " cals, ")
.font(.caption)
.offset(y:8)
Text(meal.brand)
.font(.caption)
.offset(y:8)
}
}
.foregroundColor(.black)
Spacer()
Image(systemName: "plus.app")
.font(.title2)
.foregroundColor(.blue)
.offset(x: 30)
}
.frame(width:200, height:40) //width of background
.padding([.leading, .trailing], 60)
.padding([.top, .bottom], 10)
.background(RoundedRectangle(
cornerRadius:20).fill(Color("LightWhite")))
.foregroundColor(.black)
Spacer()
}
}
.frame(height:800)
}
}
}
}
}
SearchBar
struct MealSearchBar: View {
//TEXTFIELD
#State var userFoodInput = ""
#State private var didtextComplete = false
//if user seached for a meal
#State private var didUserSearch = false
//calls search API
#StateObject private var foodApi = FoodApiSearch()
var body: some View {
VStack{
ZStack{
Rectangle()
.foregroundColor(Color("LightWhite"))
HStack{
Image(systemName: "magnifyingglass")
TextField("Enter Food", text: $userFoodInput)
.onSubmit {
foodApi.searchFood(userItem: userFoodInput)
didUserSearch = true
userFoodInput = ""
}
//Text(foodApi.foodDescription)
}
.foregroundColor(.black)
.padding(.leading, 13)
}
.frame(height:40)
.cornerRadius(15)
.padding(12)
}
FoodSearchResultsView(userSearch: $didUserSearch, textComplete: $didtextComplete)
.environmentObject(foodApi)
}
}
I only attached my results view and searchbar that calls the view. I believe the issue is happening onSubmit of the textfield, if you need the api call as well, will be happy to supply it, but to confirm again for clarity, despite the list not refreshing, the API is still updating, despite the list not updating.
Update: Added API Call
class FoodApiSearch: ObservableObject{
var userSearchResults: [Meal] = Array()
#Published var foodUnit = ""
#Published var calories = ""
#Published var brand = ""
//will search for user Input
func searchFood(userItem: String){
///IMPROVE API FUNCTION LATER ON DURING LAUNCH
///
let urlEncoded = userItem.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
guard
let url = URL(string: "https://api.nal.usda.gov/fdc/v1/foods/search?&api_key=****GWtDvDZVOy8cqG&query=\(urlEncoded!)") else {return}
URLSession.shared.dataTask(with: url) { (data, _,_) in
let searchResults = try! JSONDecoder().decode(APISearchResults.self, from: data!)
DispatchQueue.main.async { [self] in
var counter = 0
for item in searchResults.foods ?? []{
if (counter < 5){
self.userSearchResults.append(Meal(
id: UUID(),
brand: item.brandOwner?.lowercased().firstCapitalized ?? "Brand Unavailable",
mealName: item.lowercaseDescription?.firstCapitalized ?? "food invalid",
calories: String(Double(round(item.foodNutrients?[3].value! ?? 0.00)).removeZerosFromEnd()),
quantity: 2,
amount: "test",
protein: 2,
carbs: 2,
fat: 2)
)
counter += 1
}
else{return}
}
}
}
.resume()
}
}
Thank you everyone for the advice and tips. So the actual issue was actually one of relative simplicity. I was just missing emptying the array upon ending the search.
if didtextComplete{
print("complete")
foodApi.userSearchResults = [] //emptys list
userFoodInput = ""
}
I also marked a var didtextComplete = true when the search was finished. Not the answer I was looking for, but overall it works so I'll remain satisfied for the time being

How to setup NavigationLink in SwiftUI sheet to redirect to new view

I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}

SwiftUI change TextBox placeholder based on HttpResponse

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