How To Get Detail Api In Swift UI with Alamofire And Combine? - ios

My Network Manager With Alamofire is Like this for get all API (NetworkManager.swift)
#Published var games = GamesDataList(results: [])
#Published var loading = false
private let api_url_base = "https://api.rawg.io/api/games"
And I want to go to navigation when I retrieve data based on ID LIst sent: (HomeView.swift)
List(networkManager.games.results) { game in
NavigationLink(destination: GameDetailView(game: game)){
GamesRowView(game: game)
}
}
And if I want to switch pages, NetworkDetail.swift has to be like that so that the Alamofire takes away from the ID details dynamic.
private func loadDetailData() {
let parameters = "12"
AF.request("\(api_url_detail)/\(id)",)
.responseJSON{ response in
guard let data = response.data else { return }
let gamesDetail = try! JSONDecoder().decode(GamesDetail.self, from: data)
print(gamesDetail)
DispatchQueue.main.async {
self.gamesDetail = gamesDetail
self.loading = false
}
}
And this my GameDetailView.swift
var game: Games
#ObservedObject var networkDetail = NetworkDetail()
var body: some View {
VStack {
URLImage(URL(string: "\(networkDetail.gamesDetail.background_image)")!, delay: 0.25) {proxy in
proxy.image.resizable()
.frame(width: 120, height: 80)
.aspectRatio(contentMode: .fill)
.clipped()
}
HStack {
Text("Description").foregroundColor(.gray)
Spacer()
}
Text(networkDetail.gamesDetail.description).lineLimit(nil)
Spacer()
}.navigationBarTitle(Text(game.slug), displayMode: .inline)
.padding()
}
I want when to click Item in List it will passing ID and make Request from Alamofire:
Please help me thanks. Right now the detailed API is still static.

Related

Delay SwiftUI view rendering until Published object finishes loading [duplicate]

This question already has answers here:
Displaying activity indicator while loading async request in SwiftUI
(2 answers)
Closed 8 months ago.
I am trying to build an app using SwiftUI, and I've created a class responsible for performing an API request and decoding the response into a weatherResponse object, and have annotated this object as #Published within this class.
In my view, I have instantiated this class using the #StateObject annotation. According to this post, this should be the correct way of observing the weatherResponse object within my view so that it automatically updates.
However, when my view loads the instance of WeatherNetworking hasn't even been initialized, and so the first reference to the object published within that class results in a nil value access.
struct MainWeatherView: View {
#StateObject var weatherNetworking = WeatherNetworking()
var body: some View {
VStack {
Image(systemName: weatherNetworking.getConditionName(weatherID: (weatherNetworking.weatherResponse!.current.weather[0].id))) // weatherResponse is nil
.resizable()
.aspectRatio( contentMode: .fit)
.scaleEffect(0.75)
.padding()
HStack {
VStack{
Text("Temperature")
.fontWeight(.bold)
.font(.system(size: 24))
Text("Humidity")
.fontWeight(.bold)
.font(.system(size: 24))
...
}
VStack {
Text("\(weatherNetworking.weatherResponse!.current.temp, specifier: "%.2f") °F")
.fontWeight(.bold)
.font(.system(size: 24))
Text("\(weatherNetworking.weatherResponse!.current.humidity, specifier: "%.0f") %")
.fontWeight(.bold)
.font(.system(size: 24))
...
}
}
}
.onAppear {
self.weatherNetworking.getMainWeather()
}
}
}
class WeatherNetworking: ObservableObject {
#StateObject var locationManager = LocationManager()
#Published var weatherResponse: WeatherResponse?
func getMainWeather() {
print("Location:", locationManager.lastLocation?.coordinate.latitude, locationManager.lastLocation?.coordinate.longitude)
if let loc = URL(string: "https://api.openweathermap.org/data/2.5/onecall?appid=redacted&exclude=minutely&units=imperial&lat=\(locationManager.lastLocation?.coordinate.latitude ?? 0)&lon=\(locationManager.lastLocation?.coordinate.longitude ?? 0)") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: loc) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(WeatherResponse.self, from: safeData)
DispatchQueue.main.async {
self.weatherResponse = results
}
print("weatherResponse was succesfully updated")
} catch {
print(error)
}
}
} else {
print(error!)
}
}
task.resume()
}
}
}
None of the print statements within getMainWeather() execute which leads me to believe that the view is attempting to assign values before the function is called. How can I delay the assignment of these values within my view until after the onAppear() method finishes? it is worth noting that just as MainWeatherView depends upon the instantiation and asynchronous calls in WeatherNetworking, so too WeatherNetworking depends upon an instance of LocationManager.
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#Published var locationStatus: CLAuthorizationStatus?
#Published var lastLocation: CLLocation?
...
}
Make dependent views conditional, like
VStack {
if let response = weatherNetworking.weatherResponse {
Image(systemName: weatherNetworking.getConditionName(weatherID: (response.current.weather[0].id))) // weatherResponse is nil
.resizable()
.aspectRatio( contentMode: .fit)
.scaleEffect(0.75)
.padding()
}
// .. other code

I can't transfer data to each index

I am trying to transfer the same API data from the first view and display the rest of the details in the second view
The problem is that the same first data is displayed in all index
struct model: Codable, Identifiable {
let id = UUID()
var details : String
var title : String
}
and fetch data here
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
request.setValue("Bearer \(APIgetURL.Token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
Here I present the data in the first view
ForEach(modelname.models) { item in
NavigationLink(destination: DetilesTap1(modelname: modelname)) {
Rectangle()
.cornerRadius(15)
.frame(width: .infinity, height: 40)
.foregroundColor(.white)
.shadow(color: .black.opacity(0.3), radius: 20)
.padding(.trailing).padding(.leading)
.overlay(
HStack {
Text(item.title)
.padding(.trailing,30).padding(.leading,30)
Spacer()
Image(systemName: "arrow.down.doc.fill")
.padding(.trailing,30).padding(.leading,30)
}
)
}
}
Here is the second view
ForEach(modelname.models) { item in
VStack(alignment: .center) {
VStack(spacing: 10) {
Text(item.title)
Text(item.details)
Spacer()
}
}
}
I tried to put the id and the same problem
Despite all the explanations and methods available, they all have the same problem
You haven't posted a Minimal Reproducible Example (MRE), so this is an educated guess. Assuming that
#StateObject var modelname = Api()
Then your first view should be:
// keep you naming consistent. item is really of type Model, so call it model.
ForEach(modelname.models) { model in
// Pass just the model data you want to display, not everything.
NavigationLink(destination: DetilesTap1(model: model)) {
...
}
in your second view:
struct DetailView: View {
let model: Model
var body: some View {
// the two VStack were redundant.
VStack(alignment: .center, spacing: 10) {
Text(model.title)
Text(model.details)
Spacer()
}
}
}
You just need to pass the data you want to display, not everything all over again. That is the point of the ForEach. It allows you to take each element and deal with it individually no indexes needed. Please study Apple’s SwiftUI Tutorials & Stanford’s CS193P. You need a better base of knowledge on how SwiftUI works.

Swift change state and reload after API call with #binding

I currently have a list of recipes that I fetch from a local API upon screen load. This page also has a search field on it that I want to hook up to pull certain data from this API. This API call stores the results in the state of the view. My goal is, when somebody searches, to pull those records and update the state, and reloading the view with new data.
I have seen different ways of doing this with #binding and #state, however, it seems from the examples I've been looking at the binding is in a different struct within another file. In my iteration, however, I just have some functions that pull data from this API. I'd like to avoid using reloadData() as I'd like to use #binding and #state wherever possible in my application.
struct RecipesView: View {
#State var recipes = [Recipe]()
#State var searchText = ""
var body: some View {
VStack {
HStack {
TextField("Search ...", text: $searchText)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal, 10)
}
VStack{
HStack {
Text("Categories")
.bold()
.multilineTextAlignment(.trailing)
.padding(10)
Spacer()
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(SPIRITS, id:\.self){ spirit in
Button("\(spirit)", action: {
queryBySpirit(spirit: spirit)
})
.frame(width: 80, height: 40, alignment: .center)
.background(Color("Tope"))
.foregroundColor(Color("Onyx"))
.cornerRadius(15)
.font(.subheadline.bold())
}
}.padding([.leading, .trailing])
}
}
ScrollView {
VStack {
ForEach(recipes) { recipe in
NavigationLink(destination: RecipeView(recipe: recipe)) {
RecipeCard(recipe: recipe)
}
}.padding([.trailing])
}
}
}
.onAppear(perform: loadData)
.navigationBarTitle("Recipes")
.listStyle(InsetGroupedListStyle())
Color.gray.ignoresSafeArea()
}
func loadData() {
AF.request("http://localhost:3000/recipes").responseJSON { response in
guard let data = response.data else { return }
if let response = try? JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
return
}
}
}
func queryBySpirit(spirit: String) {
AF.request("http://localhost:3000/recipes?spirit=\(spirit)").responseJSON { response in
guard let data = response.data else { return }
if let response = try? JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
return
}
}
}
}
How can I use #binding within this file to take advantage of the hot reloading? Or, based on my iteration will I need to leverage reloadData() instead?
Spefically, I'd like to avoid using reloadData() as seen here: Swift CollectionView reload data after API call
I'd love to be able to leverage Changing #State variable does not update the View in SwiftUI
However I'm unsure of how to do that with what I have currently.

Retrieve and Update data stored in Firestore

I am developing a mobile app on iOS and need to track some data when a user clicks a button. However, nothing is shown when I try to get the data according to the official doc. Here is my snippet:
import SwiftUI
import FirebaseStorage
import FirebaseCore
import FirebaseFirestore
struct Station_View: View {
#State private var showingAlert = false
var ref: Firestore!
var station_ : station
var food : [food] = []
var body: some View {
VStack(alignment: .leading, spacing: 10, content: {
VStack {
ForEach(station_.menu_items, id: \.self) { i in
Divider()
.frame(width: 400, height: 1)
.background(Color("Black"))
.padding(.vertical,0)
HStack {
VStack (alignment: .leading) {
Text(i.name + ", " + i.calories + "cal, protein: " + i.protein)
.font(.headline)
.foregroundColor(Color("Black"))
}.padding(.leading, 8)
Spacer()
if (Int(i.protein)! > 10) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "p.circle")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a protein meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
if (i.is_vegan) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "leaf")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a vegan meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
}
.padding(.init(top: 12, leading: 0, bottom: 12, trailing: 0))
}
}
} )
}
}
What can I do to make it come true? I am expecting to update only one key-value pair while the others remain the same when the data is collected back.
Firstly, when working with SwiftUI you should always use a ViewModel. This is a weird transition at first but it will make your code infinitely easier to understand and keep track of. Here's the basic structure.
View Model
class YourViewModel: ObservableObject {
#Published var isTrue = false
func isValueTrue(){
print(isTrue.description)
}
}
Notice that there are a few things going on here, the ObservableObject and #Published essentially this means that the object YourViewModel can be observed with published properties, or the ones that can be bound to a view. To use it in a view you can do this.
View
struct YourView: View {
//This is your ViewModel reference.
//Use is to bind all your details to the view with
//something like yourViewModel.firstName or $yourViewModel.firstName
#observedObject var yourViewModel = YourViewModel()
var body: some View {
Button("Change Bool") {
yourViewModel.isTrue.toggle()
yourViewModel.isValueTrue()
}
}
}
This is the basic structure for an MVVM pattern and will save you tons of space in your view, making it much much easier to read and maintain. Typically you'll have a separate .swift file for the View and for the ViewModel try not to combine them, and abstract as much as you can.
To answer the ultimate question, how do you retrieve data from Firebase and update that same data? Well, the answer is as follows, I will demonstrate using a function and a property within a ViewModel that you can Bind to your views to update them.
Getting Firebase Data
//These properties are a part of the VIEWMODEL and can be bound to the view
//Using yourViewModel.someProperty
#Published var firstName = ""
#Published var lastName = ""
#Published var email = ""
func fetchFirebaseData() {
guard let uid = Auth.auth().currentUser?.uid else {
print("Handle Error")
return
}
//Create Database Reference
let db = Firestore.firestore()
//Reference the collection and document. In this example
//I'm accessing users/someUserId
let fsUserProfile = db.collection("users").document(uid)
//Request the document
fsUserProfile.getDocument { (snapshot, err) in
if err != nil { return }
self.fetchImageFromURL(url: URL(string: snapshot?.get("profile_image_url") as? String ?? "")!)
self.firstName = snapshot?.get("first_name") as? String ?? ""
self.lastName = snapshot?.get("last_name") as? String ?? ""
self.email = snapshot?.get("email") as? String ?? ""
}
}
Updating Firebase Data
This is a simple way of updating your firebase data. This is handled by passing a dictionary with a key which is the field and a value associated with it. WARNING: DO NOT use setData(...) it will clear everything else that you had in there. setData(...) is useful for first time data creation such as registering an account, creating a new entry, etc..
func updateFirebaseData(firstName: String) {
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).updateData(["first_name": firstName])
}
}
Usage
struct YourView: View {
#observedObject var yourViewModel = YourViewModel()
var body: some View {
VStack {
//Fetching Example
VStack {
Button("Fetch Data") {
yourViewModel.fetchFirebaseData()
}
Text(yourViewModel.firstName)
}
//Setting Example
VStack {
Button("Update Data") {
//You could have this "John" value, a property
//of your ViewModel as well, or a text input, or whatever
//you want.
yourViewModel.updateFirebaseData(firstName: "John")
}
}
}
}
}
Notice how much cleaner the MVVM structure is when working in SwiftUI, once you do it for a few days, it will become second nature.

Segue to a new view in SwftUI after a successful authentication from API without a NavigationButton

I am trying to segue to an entirely new view after I get a 200 OK response code from my API. The response code is successful sent back to the app and is printed into the console and the view is loaded. Except when the view loads in loads on top of the previous view. Photo of what I'm talking about. I can't use a navigation button because you can't have them preform functions and I don't want back button at the top left of the screen after a login. Here is the code I have now;
#State var username: String = ""
#State var password: String = ""
#State var sayHello = false
#State private var showingAreaView = false
#State private var showingAlert = false
#State private var alertTitle = ""
#State private var alertMessage = ""
Button(action: {
self.login()
})//end of login function
{
Image("StartNow 3").resizable()
.scaledToFit()
.padding()
}
if showingAreaView == true {
AreaView()
.animation(.easeIn)
}
//actual Login func
func login(){
let login = self.username
let passwordstring = self.password
guard let url = URL(string: "http://localhost:8000/account/auth/") else {return}
let headers = [
"Content-Type": "application/x-www-form-urlencoded",
"cache-control": "no-cache",
"Postman-Token": "89a81b3d-d5f3-4f82-8b7f-47edc39bb201"
]
let postData = NSMutableData(data: "username=\(login)".data(using: String.Encoding.utf8)!)
postData.append("&password=\(passwordstring)".data(using: String.Encoding.utf8)!)
let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:8000/account/auth/")! as URL,
cachePolicy:.useProtocolCachePolicy, timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
guard let data = data else {return}
print(data)
if httpResponse.statusCode == 200{
DispatchQueue.main.async {
//Segue to new view goes here
print(httpResponse.statusCode)
self.showingAreaView = true
}
}else{
if httpResponse.statusCode == 400{
DispatchQueue.main.async {
self.alertTitle = "Oops"
self.alertMessage = "Username or Password Incorrect"
self.showingAlert = true
print(httpResponse.statusCode)
}
}else{
DispatchQueue.main.async {
self.alertTitle = "Well Damn"
self.alertMessage = "Ay chief we have no idea what just happened but it didn't work"
self.showingAlert = true
}
}
}
do{
let JSONFromServer = try JSONSerialization.jsonObject(with: data, options: [])
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let tokenArray = try decoder.decode(token.self, from: data)
print(tokenArray.token)
UserDefaults.standard.set(tokenArray.token, forKey: "savedToken")
let savedToken = UserDefaults.standard.object(forKey: "savedToken")
print(savedToken)
}catch{
if httpResponse.statusCode == 400{
self.alertTitle = "Oops"
self.alertMessage = "Username or Password Incorrect"
self.showingAlert = true
}
print(error)
print(httpResponse.statusCode)
}
} else if let error = error {
self.alertTitle = "Well Damn"
self.alertMessage = "Ay chief we have no idea what just happened but it didn't work"
self.showingAlert = true
print(error)
}
})
dataTask.resume()
}
I've tired to google this issue but haven't had any luck. All I need is a basic segue to a new view without the need of a NavigationButton.
Any help is appreciated!
Assuming you've got two basic views (e.g., a LoginView and a MainView), you can transition between them in a couple ways. What you'll need is:
Some sort of state that determines which is being shown
Some wrapping view that will transition between two layouts when #1 changes
Some way of communicating data between the views
In this answer, I'll combine #1 & #3 in a model object, and show two examples for #2. There are lots of ways you could make this happen, so play around and see what works best for you.
Note that there is a lot of code just to style the views, so you can see what's going on. I've commented the critical bits.
Pictures (opacity method on left, offset method on right)
The model (this satisfies #1 & #3)
class LoginStateModel: ObservableObject {
// changing this will change the main view
#Published var loggedIn = false
// will store the username typed on the LoginView
#Published var username = ""
func login() {
// simulating successful API call
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// when we log in, animate the result of setting loggedIn to true
// (i.e., animate showing MainView)
withAnimation(.default) {
self.loggedIn = true
}
}
}
}
The top-level view (this satisfies #2)
struct ContentView: View {
#ObservedObject var model = LoginStateModel()
var body: some View {
ZStack {
// just here for background
Color(UIColor.cyan).opacity(0.3)
.edgesIgnoringSafeArea(.all)
// we show either LoginView or MainView depending on our model
if model.loggedIn {
MainView()
} else {
LoginView()
}
}
// this passes the model down to descendant views
.environmentObject(model)
}
}
The default transition for adding and removing views from the view hierarchy is to change their opacity. Since we wrapped our changes to model.loggedIn in withAnimation(.default), this opacity change will happen slowly (its better on a real device than the compressed GIFs below).
Alternatively, instead of having the views fade in/out, we could have them move on/off screen using an offset. For the second example, replace the if/else block above (including the if itself) with
MainView()
.offset(x: model.loggedIn ? 0 : UIScreen.main.bounds.width, y: 0)
LoginView()
.offset(x: model.loggedIn ? -UIScreen.main.bounds.width : 0, y: 0)
The login view
struct LoginView: View {
#EnvironmentObject var model: LoginStateModel
#State private var usernameString = ""
#State private var passwordString = ""
var body: some View {
VStack(spacing: 15) {
HStack {
Text("Username")
Spacer()
TextField("Username", text: $usernameString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
HStack {
Text("Password")
Spacer()
SecureField("Password", text: $passwordString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
// save the entered username, and try to log in
self.model.username = self.usernameString
self.model.login()
}, label: {
Text("Login")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
}
.padding()
.inExpandingRectangle(Color.gray)
.frame(width: 300, height: 200)
}
}
Note that in a real functional login form, you'd want to do some basic input sanitation and disable/rate limit the login button so you don't get a billion server requests if someone spams the button.
For inspiration, see:
Introducing Combine (WWDC Session)
Combine in Practice (WWDC Session)
Using Combine (UIKit example, but shows how to throttle network requests)
The main view
struct MainView: View {
#EnvironmentObject var model: LoginStateModel
var body: some View {
VStack(spacing: 15) {
ZStack {
Text("Hello \(model.username)!")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
.frame(height: 60)
HStack {
Spacer()
Button(action: {
// when we log out, animate the result of setting loggedIn to false
// (i.e., animate showing LoginView)
withAnimation(.default) {
self.model.loggedIn = false
}
}, label: {
Text("Logout")
.inFittedRectangle(Color.green.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
.padding()
}
}
Text("Content")
.inExpandingRectangle(.gray)
}
.padding()
}
}
Some convenience extensions
extension View {
func inExpandingRectangle(_ color: Color) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(color)
self
}
}
func inFittedRectangle(_ color: Color) -> some View {
self
.padding(5)
.background(RoundedRectangle(cornerRadius: 15)
.fill(color))
}
}
You could use a flag to trigger a navigation, curently it is not possible to navigate without a NavigationLink
struct ContentView: View {
#State var logedIn = false
var body: some View {
return NavigationView {
VStack {
NavigationLink(destination: DestinationView(), isActive: $logedIn, label: {
EmptyView()
})// It won't appear on screen because the label is EmptyView
Button("log in") {
// log in logic
if succesful login {
logedIn = true // this will trigger the NavigationLink
}
}
}
}
}
}

Resources