I have two viewmodels, one contains the necessary data for the user, a classic api fetch, and the second viewmodel is also an api fetch, but I want to insert the data from the first into the second, whenever I try, it always outputs nil, although before that I ensure that it cannot output it because I know it exists because the fetch function was called first.
class UserPostViewModel: ObservableObject {
#Published var user: UserResponse?
#Published var phoneNumber:String = ""
#Published var firstName:String = ""
#Published var lastName:String = ""
#Published var selectedImage: UIImage?
#Published var normalizedMobileNumberField:String = ""
func createUser() {
guard let url = URL(string: "privateapi") else {
return
}
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
let task = URLSession.shared.dataTask(with: request) {data, response, error in
guard let data = data, error == nil else{
return
}
do {
let response = try? JSONDecoder().decode(UserResponse.self, from: data)
if self.normalizedMobileNumberField != "The number you enter is invalid." {
DispatchQueue.main.async {
self.user = response
if let userData = self.user {
print(userData.data.id)
}
}
} else {
print("You can't register application")
}
}
catch {
print(error)
}
}
task.resume()
}
}
This is the second one:
class GetTokenViewModel: ObservableObject {
#Published var tokenData: DataToken?
var userVM = UserPostViewModel()
func fetchData() {
let urlString = "privateapi/\(userVM.user.token)"
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url:url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return
}
do {
let decodedData = try JSONDecoder().decode(ResponseToken.self, from: data)
DispatchQueue.main.async{
self.tokenData = decodedData.data
print("TOKEN: \(self.tokenData?.token)")
}
} catch {
print("Error decoding JSON in gettoken: \(error)")
}
}.resume()
}
}
How to pass data from First ViewModel to the second?
I know that these codes have some error but I deleted every private information, but I believe that there is minimal code for understanding my question.
In SwiftUI, the View struct is equivalent to a view model but it is an immutable value type instead of an object that helps prevent consistency bugs. The property wrappers help it behave like an object. You pass data down the View struct hierarchy either with let if you want read access or #Binding var if you want write access. SwiftUI will track that these lets/vars are read in body and then body will be called whenever changes are detected. Transform the data as you pass it along from complex model types to simple value types.
#State declares a source of truth and #StateObject is when you need a reference type in an #State because you are doing something asynchronous or need to work around the limitations of closures in structs. Because these states are sources of truth you can't pass anything in because they won't be able to detect changes the same way the View struct can with let or #Binding var.
The best way to call web API in SwiftUI now is .task and .task(id:). The task will start when the view appears, cancelled if it disappears, or restarted if the id changes. This can completely remove the need for an object.
Related
Language:Swift
Hello, I'd like some help in resolving an error being thrown when I try to retrieve data from an Apollo GraphQL request that I'm making. The API in use is the AniList API utilizing GraphQL.
Here's what I've tried:
In my model I'm making the Apollo GraphQL query inside of a search() function. I want to then use the Codable protocol to fill an array of anime objects. Currently it's setup to return just for 1 anime object. I was planning on using this anime list as a data set for TableView later. I wanted to take small steps so my current goal is to at least get the Codable protocol to work and return the response data to an anime Struct object.
The documentation for Apollo shows how to get individual fields but when I try to get the corresponding fields from my response , I don't even have the option.
func search(){
Network.shared.apollo.fetch(query: AnisearchQuery()){ result in
guard let data = try? result.get().data else { return }
var topData:APIResponse?
do{
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
}catch{
}
}
}
Here are the data structures that I've set up as a representation of the JSON data I expect to receive with respect to the hierarchy it is laid out in the response.
struct APIResponse:Codable{
let data:data
}
struct data:Codable{
let Page:page
let media:media
}
struct media:Codable{
let animeResults:anime
}
struct anime:Codable{
var romaji:String
var english: String
var native:String
var episodes:Int
var duration:Int
var medium:String
}
Here is the error in question.
"Cannot convert value of type 'AnisearchQuery.Data' to expected argument type 'Data'". This is generated by this line of code
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
For further context , AnisearchQuery.Data is generated in response to the query I created for the codgen.
Here's what the data would look like in JSON format
This is the setup of the query:
query anisearch($page:Int, $perPage:Int, $search:String){
Page (page:$page, perPage:$perPage){
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media(search:$search){
title{
romaji
english
native
}
episodes
duration
coverImage{
medium
}
}
}
}
Here's the Data object in the API.swift file:
public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["Query"]
public static var selections: [GraphQLSelection] {
return [
GraphQLField("Page", arguments: ["page": GraphQLVariable("page"), "perPage": GraphQLVariable("perPage")], type: .object(Page.selections)),
]
}
I'd be open to any alternative methods as to getting this task done or perhaps fixes to the error being thrown.
Many thanks in advance.
Inefficient Workaround
var animeCollection:SearchAnimeQuery.Data?
var media:[SearchAnimeQuery.Data.Page.Medium]?
var filteredData:[SearchAnimeQuery.Data.Page.Medium] = []
func loadData(search:String = "") {
if !search.isEmpty{
Network.shared.apollo.fetch(query: SearchAnimeQuery(search: search)){
[weak self] result in
//Make Sure ViewController Has not been deallocated
guard let self = self else{
return
}
/*defer {
}*/
switch result {
case .success(let graphQLResult):
if let animeData = graphQLResult.data {
DispatchQueue.main.async {
self.animeCollection = animeData
self.media = self.animeCollection?.page?.media as! [SearchAnimeQuery.Data.Page.Medium]
self.filteredData = self.media!
self.tableView.reloadData()
}
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
print(message)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
I have an asynchronous function that performs an SQL query and updates the #Published variables. I can read these new values into the Picker as the Picker's default value but when I change the Picker value - and then pass it to another function that runs an SQL Update function (through a button), it just passes the original default #Published values, not the updated ones chosen by the user interaction with the Picker. The findSingleProduct function is called by passing a Barcode to it that comes from a preceeding view using user input.
class ProductsModel: ObservableObject{
#Published var Comp1Mat: Int = 0
#Published var code: String = ""
func findSingleProduct(code: String){
// Completes an SQL query on the Products database to find a product by it's barcode number.
// prepare json data
let insertProductURL = URL(string: "http://recyclingmadesimple.xyz/service.php")!
var urlRequest = URLRequest(url: insertProductURL)
urlRequest.httpMethod = "POST"
let postString = "code=\(code)"
urlRequest.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
DispatchQueue.main.async {
self.Comp1Mat = responseJSON["Comp1Mat"] as! Int
}
}
}
task.resume()
}
This is my view that displays Pickers about the object. I have removed a lot of the Pickers as they're all suffering from the same problem so the solution should be transferable.
struct ProductDetail: View {
#ObservedObject private var viewModel = ProductsModel()
init(ad: [String], material: [String], code: String){
self.viewModel.findSingleProduct(code: code)
}
var body: some View {
//This is the main stack that contains everything
VStack(alignment: .leading){
Picker(selection: $viewModel.Comp1Mat, label: Text("Material")){
ForEach(0 ..< material.count){
Text(self.material[$0])
}
}
Button(action: {viewModel.updateProduct(brands: "\(viewModel.Comp1Mat)", **Omitted variables for simplicity, they also run on similar Pickers**); showingAlert = true}, label: {
Text("Update Product")
}).alert(isPresented: $showingAlert) {
Alert(title: Text("Product Updated"), message: Text("Thanks for helping the community!"), dismissButton: .default(Text("Dismiss")))
}
}
I have confirmed the PHP and SQL of the update function work by hardcoding some values into the Button like: Button(action: {viewModel.updateProduct(Comp1Mat: "(5)"}... and they are indeed correctly passed through and the database is updated.
I thought this similar question might be of use but I believe I am already using the $ correctly, which was the suggested solution for this person so I am unsure why it doesn't work for me. Change a #Published var via Picker
Thanks.
You just need to add a tag.
Text(self.material[$0]).tag($0)
I have this small project where a user can post an Image together with a quote, I would then like to display the Image and the quote togehter in their profile, as well as somewhere else so other users can see the post.
If I have this Cloud Firestore setup
where all of the Image Docs have the same 3 fields, but with different values.
How can I then iterate over all of the Image Docs and get the the Url and the quote? So I later can display the url together with the correct Quote?
And if this is for some reason not possible, is it then possible to get the number of Documents in a Collection?
BTW, I am not very experienced so I would appreciate a "kid friendly" answer if possible
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
let quote = documentData["Quote"] as? String
let url = documentData["Url"] as? String
print("Quote: \(quote ?? "(unknown)")")
print("Url: \(url ?? "(unknown)")")
})
}
You can get all of the documents in a collection by calling getDocuments.
Inside that, snapshot will be an optional -- it'll return data if the query succeeds. You can see I upwrap snapshot and check for error in the guard statement.
Once you have the snapshot, you can iterate over the documents with documents.forEach. On each document, calling data() will get you a Dictionary of type [String:Any].
Then, you can ask for keys from the dictionary and try casting them to String.
You can wee that right now, I'm printing all the data to the console.
Keep in mind that getDocuments is an asynchronous function. That means that it runs and then returns at an unspecified time in the future. This means you can just return values out of this function and expect them to be available right after the calls. Instead, you'll have to rely on things like setting properties and maybe using callback functions or Combine to tell other parts of your program that this data has been received.
If this were in SwiftUI, you might do this by having a view model and then displaying the data that is fetched:
struct ImageModel {
var id = UUID()
var quote : String
var url: String
}
class ViewModel {
#Published var images : [ImageModel] = []
func fetchData() {
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
self.images = snapshot.documents.compactMap { documentSnapshot -> ImageModel? in
let documentData = documentSnapshot.data()
if let quote = documentData["Quote"] as? String, let url = documentData["Url"] as? String {
return ImageModel(quote: quote, url: url)
} else {
return nil
}
}
}
}
}
struct ContentView {
#ObservedObject var viewModel = ViewModel()
var body : some View {
VStack {
ForEach(viewModel.images, id: \.id) { item in
Text("URL: \(item.url)")
Text("Quote: \(item.quote)")
}
}.onAppear { viewModel.fetchData() }
}
}
Note: there are now fancier ways to get objects decoded out of Firestore using FirebaseFirestoreSwift and Combine, but that's a little outside the scope of this answer, which shows the basics
I have an Observable Object class to store a forecast object for my app
The class looks like this:
final class ForecastData: ObservableObject {
#Published var forecast: DarkSkyResponse
public func getForecast(at location: CLLocation) {
let request = DarkSkyRequest(key: "KEYGOESHERE")
let point = DarkSkyRequest.Point(location.coordinate.latitude, location.coordinate.longitude)
guard let url = request.buildURL(point: point) else {
//Handle this better
preconditionFailure("Failed to construct URL")
}
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
DispatchQueue.main.async {
guard let data = data else {
//Handle this better
fatalError("No Data Recieved")
}
guard let forecast = DarkSkyResponse(data: data) else {
//Handle this better
fatalError("Decoding Failed")
}
self.forecast = forecast
}
}
task.resume()
}
init() {
self.getForecast(at: CLLocation(latitude: 37.334987, longitude: -122.009066))
}
}
The first part simply generates a URL to access the API by. Then I start a URLSession which downloads the data and then parses it into a DarkSkyResponse object. Finally I set the #Published variable to the forecast object.
My problem is when I call the function in the initialiser, I get an error because the forecast property is not initialised. What is the best way to get around this? Where should I call the function?
By the way I am using this class in my SwiftUI View using an #ObservedObject property wrapper
Case1: use optional (and you need to handle it in View)
#Published var forecast: DarkSkyResponse?
Case2: use some default instance
#Published var forecast = DarkSkyResponse()
Both variants are equivalent and acceptable, so just by your preference.
Try this changes:
#Published var forecast: DarkSkyResponse!
and
init() {
super.init()
self.getForecast(at: CLLocation(latitude: 37.334987, longitude: -122.009066))
}
I'm Learning Swift development for IOS and encountered a design problem in my simple Project. I have a pickerView set up so that everytime user selects a value, different information from the Json is displayed and it works just fine.
However, in my current design the data gets parsed/fetched again everytime the user selects a new value from the pickerview, what I want to do is to collect the data once and then just loop through the same data based on the users selection. My guess is that i need to separate the function to load the data and the function/code to actually do the looping and populate the labels. But I can't seem to find any way to solve it, when i try to return something from my loadData function I get problems with the returns already used inside the closure statements inside the function.
Hopefully you guys understand my question!
The selectedName variable equals the users selected value from the pickerView.
The function loadData gets run inside the pickerView "didselectrow" function.
func loadData() {
let jsonUrlString = "Here I have my Json URL"
guard let url = URL(string: jsonUrlString) else
{ return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do { let myPlayerInfos = try
JSONDecoder().decode(Stats.self, from: data)
DispatchQueue.main.async {
for item in myPlayerInfos.elements! {
if item.web_name == self.selectedName{
self.nameLabel.text = "Name:\t \t \(item.first_name!) \(item.second_name!)"
} else {}
}
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}//end function loaddata
And for reference, the Stats struct:
struct Stats: Decodable {
let phases: [playerPhases]?
let elements: [playerElements]?
}
struct playerPhases: Decodable{
let id: Int?
}
struct playerElements: Decodable {
let id: Int?
let photo: String?
let first_name: String?
let second_name: String?
}