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
CodePudding user response:
.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
CodePudding user response:
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! :)