I have an independent application on watchOS 6 and in my app I am using the Firestore REST API to show the data to the user using URLSession.
Since the Cloud Firestore REST API returns a JSON string, in order to process the data, I have created nested structs. Example: To access the 'title' in a particular response, I do something like this: response.mapValue.fields.title.stringValue.
The app works fine for now. In the long run I plan on creating the URLSessions as background tasks. Right now, I am calling URLSession every time the view is rendered by using .onAppear(functionToUseURLSession())) on my data's List view.
Now the next thing I want to implement is the complication for this particular app.
I am new to Swift and SwiftUI and am having the hardest time just getting started. All the tutorials I've been through online use WKExtensionDelegate to get the data models for the complication.
But in my case, I don't even have a data model! I just have structs calling other structs in a nested fashion in order to process the JSON response I get from the API call.
Any help allowing me to understand this is highly appreciated!
Here is some code to better understand the structure of the App:
struct Firebase: Codable {
var name: String?
var createTime, updateTime: String?
var fields: FirebaseFields
}
Now, FirebaseFields is also another struct:
struct FirebaseFields: Codable {
var emailID, lastName, firstName: String?
var goalsRoutines: GoalsRoutines
enum CodingKeys: String, CodingKey {
case emailID = "email_id"
case lastName = "last_name"
case firstName = "first_name"
}
}
Similarly, GoalsRoutines is also a struct...
As mentioned above, I have made these structs to follow the exact structure of the JSON object is get in response from Firebase API. So I access fields like: Firebase.FirebaseFields.GoalsAndRoutines.whatever.is.my.firebase.schema
My GoalsView.swift is:
var body: some View {
List{
...
...
}.onAppear{
FirebaseServices.getFirebaseData() {
(data) in self.data = data
}
}
}
And finally, in FirebaseServices.swift, func getFirebaseData is:
func getFirebaseData(completion: #escaping ([Value]) -> ()) {
guard let url = URL(string: "FIRESTORE URL HERE") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let data = try! JSONDecoder().decode(Firebase.self, from: data!)
DispatchQueue.main.async {
completion(data.fields.goalsRoutines.arrayValue.values)
}
}.resume()
}
So this is how I currently get the data and show it on the app.
I am really lost on how to do the same on my Complication.
Any help is really appreciated.
Related
import FirebaseFirestoreSwift
import Firebase
// I created this object so that i can map the users data and access it threw this object. example I could say thigs like user.username
// The decodable protocall will read the data dictonary and looks for the exact name for the keys/property names I have listed in the data dictonary, this makes life easier when working with objects and downloading information from an api
struct User: Identifiable, Decodable {
// Im able to delete the uid field out of firebase because this will read the documentID from firebase and store it in this id property, so that I dont have to dupicate that data in the actual body of the object
#DocumentID var id: String?
let username: String
let fullname: String
let profileImageUrl: String
let email: String
let stats: UserStats
// This is a computed property saying if the currently logged in user's id is equal to the id on my object (#DocumentID)
var isCurrentUser: Bool { return Auth.auth().currentUser?.uid == id }
}
struct UserStats: Decodable {
let followers: Int
let following: Int
}
Add ? at the end of each variable.
#FirestoreQuery does little error handling when it comes to decoding.
Also, if you are not using #FirestoreQuery use do try catch instead of try?
I want to use Alamofire to query my backend, encode the response using Alamofire's built-in Codable parsing and then publish an extract from the resulting Struct to be consumed by the caller of my API class. Say I have some JSON data from my backend (simplified, but shows the structure):
{
"total": 123,
"results": [
{"foo" : "bar"},
{"foo" : "baz"}
]
}
and the associated Codable Structs
struct MyServerData: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let foo: String
}
I can get, parse, publish, and subscribe all fine with the following:
func myAPI() -> DataResponsePublisher<MyServerData> {
return AF.request("https://backend/path")
.validate()
.publishDecodable(type: MyServerData.self)
}
myAPI()
.sink { response in /* Do stuff, e.g. update #State */ }
What I'd like to do is to publish just the [Result] array. What's the correct approach to this? Should I use .responseDecodable() and create a new publisher (somehow - .map()?) that returns a [Result].publisher?
While I think I understand the reactive/stream based principles my Combine-fu is still weak and I don't have a clear handle on the transformation of one publisher into another (I'm guessing?)
Thanks in advance!
In addition to using Combine API like map, Alamofire offers two publishers on DataResponsePublisher itself.
.result() extracts the Result from the DataResponse and creates an AnyPublisher<Result<Value, AFError>, Never>.
.value() extracts the Value from the DataResponse and creates a failable publisher, AnyPublisher<Value, AFError>.
So depending on what kind of error handling you want, this could be as simple as:
...
.publishDecodable(...)
.value()
.map(\.results)
I'm trying to implement the MVVM approach to my app and have some trouble with the required struct as Model. In a struct it is hard to manipulate its own data but I need two because its holds the view data.
I request data from a firebase database and after receiving, assigning the data to an array.
As I found out, I can not manipulate the array inside a response closure. When I try to call another function as callback I get the following error:
Partial application of 'mutating' method is not allowed
Here is my code:
mutating func searchLocations(name: String, onlyOwnList: Bool) {
if onlyOwnList{
onlineDataManager.getOwnLocations(searchName: name, userId: appViewModel.currentUser, completionHandler: getLocationsCallback)
}
else{
onlineDataManager.getAvailableLocations(searchName: name, userId: appViewModel.currentUser, completionHandler: getLocationsCallback)
}
}
mutating private func getLocationsCallback(_ locations: BarLocations){
self.locationList.locations.removeAll()
for location in locations.locations{
self.locationList.locations.append(location)
}
}
I'm used to Java so im struggling with the sense of structs.
I hope somebody can help me or tell me, how to do it better
I believe the issue you're facing is that the mutation of the struct needs to happen in the same scope of the first mutating function since structs are pass-by-values.
mutating func searchLocations(name: String, onlyOwnList: Bool) {
if onlyOwnList{
onlineDataManager.getOwnLocations(searchName: name, userId: appViewModel.currentUser) { [weak self] locations in
self?.locationList.locations = locations
}
} else{
onlineDataManager.getAvailableLocations(searchName: name, userId: appViewModel.currentUser) { [weak self] locations in
self?.locationList.locations = locations
}
}
}
To be honest, if your model is handling the loading of its own information, I'd change your model to be a class and not a struct. Either that, or handle the data load outside of the model (in your view controller and then create a simple struct with the information).
I've defined a custom data model for a User Object in Swift like so:
user data model
I've got a function that pulls User data from an API like so:
get data from api
Here's the response when calling the same endpoint with Postman:
api response
And here's the console debug output from line 75 of my function, showing that I'm actually receiving that data: debug output
So that all looks good as far as I can tell.
I'm then using JSONDecoder().decode to decode the jsonData I receive from the api, for which I'm not getting any errors. However, when I'm then printing a field from the returned user object, that field (as well as all others) are "nil": all fields in user object are nil
I'm sure it's something small and stupid but I've spent hours now and can't figure out what it is. Can anyone spot the error and let me know what I'm doing wrong here?
Help much appreciated!!!
For Codable you need to give same name of properties to the json key. And make sure it's in correct scope. For example you email properties inside of detailresponse json object & detailresponse inside of main json object. If you don't wont more class you need to use it's init container method.
class Response: Codable {
var statuscode: Int?
var response_type: Int?
// Other properties
var detailresponse: DetailResponse?
}
class DetailResponse: Codable {
var id: Int?
// Other properties
var socialmediadata: User?
}
class User: Codable {
var id: Int?
var email: String?
// Other properties
}
Now, json will parse like this.
let response = try JSONDecoder().decode(Response.self, from: jsonData)
print(response.detailresponse?.socialmediadata?.email ?? "")
I am making this request:
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
print("Result: \(response.result.value)"
do {
self.list = try JSONDecoder().decode([list].self, from: result!) for event in self.lists {
print(event.title," : ",event.description)
}
} catch let parseError as NSError {
print("JSON Error \(parseError.localizedDescription)")
}
}
Data that ought to look like this (JSON?) - Postman output, all fields not included herein:
{
"start": "2016-02-01 11:30:00",
"end": "2016-02-01 14:42:24",
"id": 3192,
"ownership": false,
}
prints out looking like this in XCode:
{
start = "2016-02-01 11:30:00";
end = "2016-04-14 20:30:00";
"id" = 3192;
ownership = 0;
}
Result : I am not able to parse this using JSONDecoder, error:
"The data couldn’t be read because it isn’t in the correct format".
Newbie to Swift ... so, thanks in advance for the help!
Edit: Edited for clarity with more information. Thanks again!
Alamofire is not "replacing characters in output", it is giving you a different object than the one you expect. If you print out the type of your response.result you should be surprised by the NSDictionary you are likely to get at that point. Our trusted friend print(...) is nice enough to turn this into a String representation of whatever you pass it, but you are not likely to be able to parse this using JSONDecoder since it is not Data (which is what the decoder is expecting).
As I said before: use responseString in order to get the response and turn it into the appropriate Data for parsing using JSONDecoder. In order to be able to control this process properly you want to include your Codable derivative into the question and you are likely to set the date parsing strategy on the JSONDecoder.
Without your struct and some properly formatted JSON from your response (well, Postman will do if it is reasonably complete) we are unlikely to be able to help you any further.
P.S.: It is not an entirely good idea to change your question completely through an edit. You might be better of posting a new question and leaving a comment with a pointer to it on the old one so people revisiting it may be lead to the right place. If you update your question you should usually leave the old one intact and amend it with additional information in order to keep the existing discussion relevant.
As workaround you can just add CodingKey to decoded struct.
Just add to your struct/class
private enum CodingKeys: String, CodingKey {
case event_id = "id"
}
Please refer to https://benscheirman.com/2017/06/swift-json/
I can suggest the following solution:
Firstly you need a pojo class to refer your json object. Easiest way
that I know is the library called SwiftyJSON
(https://github.com/SwiftyJSON/SwiftyJSON) firstly you can add this
library to your project. Then you can create the following pojo class
for your output (optional: You can also install
SwiftyJSONAccelarator(https://github.com/insanoid/SwiftyJSONAccelerator)
to generate pojo classes using json outputs.):
import Foundation
import SwiftyJSON
public class MyOutput: NSObject {
// MARK: Declaration for string constants to be used to decode and also serialize.
internal let kMyOutputEndKey: String = "end"
internal let kMyOutputInternalIdentifierKey: String = "id"
internal let kMyOutputOwnershipKey: String = "ownership"
internal let kMyOutputStartKey: String = "start"
// MARK: Properties
public var end: String?
public var internalIdentifier: Int?
public var ownership: Bool = false
public var start: String?
// MARK: SwiftyJSON Initalizers
/**
Initates the class based on the object
- parameter object: The object of either Dictionary or Array kind that was passed.
- returns: An initalized instance of the class.
*/
convenience public init(object: AnyObject) {
self.init(json: JSON(object))
}
/**
Initates the class based on the JSON that was passed.
- parameter json: JSON object from SwiftyJSON.
- returns: An initalized instance of the class.
*/
public init(json: JSON) {
end = json[kMyOutputEndKey].string
internalIdentifier = json[kMyOutputInternalIdentifierKey].int
ownership = json[kMyOutputOwnershipKey].boolValue
start = json[kMyOutputStartKey].string
}
}
After that after calling url with Alomofire and getting response, you
can simply map the output to your pojo class. Finally, you can use any
field in your class(myOutput in my example):
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
let myOutput = MyOutput.init(json: json)
//use myOutput class for your needs
case .failure( _):
self.createNetworkErrorPopup()
}
}