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
Related
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.
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.
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.
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.
i'm trying to work with swiftUI but i'm having an issue. i want to fetch the data from firebase and show that data to the list. i used mutating function to modify the variable. now when i called that function than it give me error. how i can solve this please help me. below are the code i'm using.
struct UserListView : View {
var body: some View{
VStack{
List{
VStack(alignment:.leading){
HStack{
Text("Favroute").frame(alignment: .leading).padding(.leading, 20)
Spacer()
Text("All").frame(alignment: .trailing)
}
ScrollView(.horizontal, content: {
HStack(spacing: 10) {
ForEach(0..<10) { index in
StatusView(statusImage: "nature")
}
}
.padding(.leading, 10)
})
.frame(height: 190)
}
ForEach(0..<10){_ in
HStack{
Group{
NavigationLink(destination: ChatView()){
UserImageView(imageName: "profileDummy").padding(.leading, 5)
Text("User Name").padding(.leading,10)
}
}
}.frame( height: 40)
}.environment(\.defaultMinListRowHeight, 40)
}
}
.navigationBarTitle("UserList",displayMode: .inline)
.onAppear {
self.userList()
}
}
var userDataList = [UserModel]()
mutating func userList() {
databaseReference.child(DatabaseNode.Users.root.rawValue).observe(.value) { (snapShot) in
if snapShot.exists(){
if let friendsDictionary = snapShot.value{
for each in friendsDictionary as! [String : AnyObject]{
print(each)
if let user = each.value as? [String : Any]{
let data = UserModel(userId: JSON(user["userId"] ?? "").stringValue, name: JSON(user["name"] ?? "").stringValue)
print(data)
self.userDataList.append(data)
}
}
}
}
}
}
}
below are the error message i'm getting:
Cannot use mutating member on immutable value: 'self' is immutable
You should declare your userDataList property like so #State var userDataList = [UserModel](). This way this property will be stored by SwiftUI outside of your struct and can be mutated.