I'm am looking for a nice an clean way to assign and get the user/author from my post object.
I could possible loop throw each Post in the posts array and get the user data from Firestore and assign it to the post property from within the viewcontroller, but what I'm looking for is like a computed property or lazy property that gets the data from Firestore and adds it to the object automatically when Post is initialized.
I don't no if it is possible or if it is the right way to do it, been struggling trying different methods with out any success.
This is my current post model - Post.swift
struct Post {
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
}
In my viewController I have a array of post witch get filled by data from the Firestore database - PostViewController.swift
func loadAllPosts() {
database.collection("posts").getDocuments { (querySnapshot, error) in
if let error = error {
print("Error when loading uhuus: \(error.localizedDescription)")
} else {
self.posts = querySnapshot!.documents.flatMap({Post(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
}
}
This is what I was imagining my Post model would look like - Post.swift
(This code does not work and returns a "Unexpected non-void return value in void function" error)
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private var setUser: User {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
return user
} else {
print("Document does not exist")
}
}
}
}
If you don't have a solutions but nows this is bad practice, Then I would like to know why and what would be the best way.
You seem to be on the right track with your second code snippet. You might consider triggering the setUser function when you init a Post as that is the point where you will have a valid authorId.
The reason you are getting that error is because you are trying to return a User in your setUser function, when what you really need to do is just update your user variable on that particular Post.
I'd suggest updating like so:
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
//Immediately fetch the User object here
self.setUser()
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private func setUser() {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
//Set the user on this Post
self.author = user
} else {
print("Document does not exist")
}
}
}
}
On a somewhat unrelated note, you might want to avoid having a database reference inside each Post object. It will still work if you continue the way you have it here, but it's not the Post's job to keep track of this. It'd make more sense to have it in a separate class that is built to deal with database functions.
Related
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.
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 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
How do I go about adding the document ID of a document I just added to my firestore database, to said document?
I want to do this so that when a user retrieves a "ride" object and chooses to book it, I can know which specific ride they've booked.
The problem that i'm facing is that you can't get the document ID until after it's created, so the only way to add it to said document would be to create a document, read its ID, then edit the document to add in the ID. At scale this would create twice as many server calls as desired.
Is there a standard way to do this? Or a simple solution to know which "ride" the user booked and edit it accordingly in the database?
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String // How do I implement this?
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String?, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: [
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": ride.ID,
]) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(ref!.documentID)")
completion(ref?.documentID, nil)
// I'd currently have to use this `ref?.documentID` and edit this document immediately after creating. 2 calls to the database.
}
}
}
While there is a perfectly fine answer, FireStore has the functionality you need built in, and it doesn't require two calls to the database. In fact, it doesn't require any calls to the database.
Here's an example
let testRef = self.db.collection("test_node")
let someData = [
"child_key": "child_value"
]
let aDoc = testRef.document() //this creates a document with a documentID
print(aDoc.documentID) //prints the documentID, no database interaction
//you could add the documentID to an object etc at this point
aDoc.setData(someData) //stores the data at that documentID
See the documentation Add a Document for more info.
In some cases, it can be useful to create a document reference with an
auto-generated ID, then use the reference later. For this use case,
you can call doc():
You may want to consider a slightly different approach. You can obtain the document ID in the closure following the write as well. So let's give you a cool Ride (class)
class RideClass {
var availableSeats: Int
var carType: String
var dateCreated: String
var ID: String
init(seats: Int, car: String, createdDate: String) {
self.availableSeats = seats
self.carType = car
self.dateCreated = createdDate
self.ID = ""
}
func getRideDict() -> [String: Any] {
let dict:[String: Any] = [
"availableSeats": self.availableSeats,
"carType": self.carType,
"dateCreated": self.dateCreated
]
return dict
}
}
and then some code to create a ride, write it out and leverage it's auto-created documentID
var aRide = RideClass(seats: 3, car: "Lincoln", createdDate: "20190122")
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: aRide.getRideDict() ) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
aRide.ID = ref!.documentID
print(aRide.ID) //now you can work with the ride and know it's ID
}
}
I believe that if you use Swift's inbuilt ID generator, called UUID, provided by the Foundation Framework, this will let you do what you want to do. Please see the below code for my recommended changes. Also by doing it this way, when you first initialise your "Ride" struct, you can generate its ID variable then, instead of doing it inside the function. This is the way I generate unique ID's throughout my applications and it works perfectly! Hope this helps!
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
let newDocumentID = UUID().uuidString
ref = db.collection("rides").document(newDocumentID).setData([
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": newDocumentID,
], merge: true) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(newDocumentID)")
completion(newDocumentID, nil)
}
}
}
This is my solution which works like a charm
let opportunityCollection = db.collection("opportunities")
let opportunityDocument = opportunityCollection.document()
let id = opportunityDocument.documentID
let data: [String: Any] = ["id": id,
"name": "Kelvin"]
opportunityDocument.setData(data) { (error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))
}
}
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?
}