I have successfully parsed JSON by creating a Model. But now I want a View Model which talks to View and the View updates whenever the Model Updates. But I m having hard time initialising the model.
My simplified model looks like this.
PeopleListM.swift
import SwiftUI
import Alamofire
struct PeopleListM: Decodable {
var all: [Person]
enum CodingKeys: String, CodingKey {
case all = "people"
}
struct Person: Decodable {
let name: String
let email: String
let favorite: Bool
let lastSent: String?
enum CodingKeys: String, CodingKey {
case name
case email = "email_id"
case favorite
case lastSent = "last_sent"
}
}
init() {
let headers: HTTPHeaders = ["disableauth" : "true", "Content-Type": "application/json"]
AF.request("MY URL", parameters: nil, headers: headers)
.validate()
.responseDecodable(of: PeopleListM.self) { (response) in
guard let result = response.value else { return }
self.all = result.all
}
}
This code gives me error Escaping closure captures mutating 'self' parameter
I tried changing line self.all = result.all with self = result, same error.
My further plan is create a class which conforms to ObservableObject
PeopleListVM.swift
import SwiftUI
class PeopleListVM: ObservableObject {
let people = PeopleListVM.createModel()
static func createModel() {
PeopleListM()
}
}
final class PeopleListVM: ObservableObject {
#Published var people = PeopleListVM.createModel()
static func createModel() {
PeopleListM()
}
func request() {
// networking here...
// update people in callback
// ... self.people = newPeople
}
}
struct Model: View {
#ObservableObject var vm = PeopleListVM()
// ...
}
You generally won't put networking code in struct, because networking always comes with state changes, when value type is meant to be immutable.
Related
I have the following struct:
struct Category: Codable, Hashable {
let id: Int
let name: String
let image: String
}
I also have a Resource struct as shown below:
struct Resource<T: Codable> {
let url: URL
var headers: [String: String] = [:]
var method: HttpMethod = .get([])
let modelType: T
}
When I try to create an instance of Resource, it fails:
let resource = Resource(url: URL.allCategories, modelType: Category.self)
// Type 'Category.Type' cannot conform to 'Decodable'
UPDATE:
enum HttpMethod {
case get([URLQueryItem])
case post(Data?)
case delete
var name: String {
switch self {
case .get:
return "GET"
case .post:
return "POST"
case .delete:
return "DELETE"
}
}
}
SOLUTION:
I believe the modelType should be T.Type instead of T. I made the change and now I am not getting any errors.
struct Resource<T: Codable> {
let url: URL
var headers: [String: String] = [:]
var method: HttpMethod = .get([])
let modelType: T.Type
}
I'm trying to instantiate my viewmodel for testing, in this case I don't need its parameters, but as it asks me to add them, when I try I get an error "Constant 'data' used before being initialized"
This is my code:
struct Account: Codable {
let details: [Details]?
}
struct Details: Codable {
let id: String?
let currency: String?
let interest: Float64?
let date: String?
}
class DetailViewModel {
private let data: Details?
init(data: Details) {
self.data = data
}
}
Tests:
class DetailViewModelTest: QuickSpec {
override func spec() {
var viewModel : DetailViewModel!
let data: Details!
viewModel = DetailViewModel(data: data) // I have error in this line
}
}
Is there a way to instantiate my viewmodel without parameters? Or a better way to handle this?
To use Details in a test with hardcoded values you either need to create it from some json or add another init to initialise all values, here I am using the latter. I am adding it in an extension and created a static method that uses the init to create an object with hard coded values.
extension Details {
private init(id: String, currency: String, interest: Float64, date: String) {
self.id = id
self.currency = currency
self.interest = interest
self.date = date
}
static func createStub() -> Details {
Details(id: "1", currency: "EUR", interest: 1.23, date: "2022-02-12")
}
}
This is one way of doing it, the init could be designed in many ways but this is to show you how to move forward.
This can then be used in the test class
class DetailViewModelTest: QuickSpec {
override func spec() {
let viewModel = DetailViewModel(data: Details.createStub())
//...
}
}
you should:
let data: Details = Details() // create your data
I currently have two classes in my app.
One class stores important values in variables (ex. size, color, width) that correspond to user inputs.
Another class initiates a URLRequest to an API in order to fetch data.
My goal is to make a parameter in the URL change depending on the values stored in the variable of the first class.
For instance, the URL looks like this: "www.google.com/docs/(VAR)"
I made VAR into a variable in the class with an API request and I'm trying to mutate it with conditional statements using variables from the first class.
Unfortunately, this is giving me an error and I am unable to transfer data between two classes.
Any suggestions?
Here is the code for the first class:
import Foundation
class Product: ObservableObject, CustomStringConvertible {
#Published var color = ""
#Published var size = ""
#Published var finish = ""
#Published var cut = ""
#Published var length = ""
#Published var type = ""
var description: String {
"\(size) - \(finish) - \(cut) -\(length)"
}
}
Here is the code for the APIRequest
import Foundation
class APIRequest: ObservableObject {
#Published
var lengthOptions: [String] = []
init () {
fetchLengths { lengths in
self.lengthOptions = lengths[0].OptionList.map { $0.OptionName }
}
}
private func fetchLengths (completion: #escaping ([OptionsHead]) -> ()) {
let catalogID = 1877
// how can I change catalogID based on Product class variables
guard let url = URL(string: "blablaAPI.com/Products/\(catalogID)/Options?limit=1&offset=3")
else {
fatalError("URL is not correct")
}
var request = URLRequest(url: url)
request.addValue("12345", forHTTPHeaderField: "PrivateKey")
request.addValue("https://www.blaaaaa.com", forHTTPHeaderField: "SecureURL")
request.addValue("12345", forHTTPHeaderField: "Token")
request.httpMethod = "GET" // "POST", "PUT", "DELE"
URLSession.shared.dataTask(with: request) { data, _, _ in
let outputs = try!
JSONDecoder().decode([OptionsHead].self, from: data!)
DispatchQueue.main.async {
completion(outputs)
}
}.resume()
}
}
you could try passing the Product into you APIRequest class,
to change the catalogID based on Product class variables, like this:
class APIRequest: ObservableObject {
#Published var lengthOptions: [String] = []
#ObservedObject var product: Product
init (product: Product) {
self.product = product
fetchLengths { lengths in
self.lengthOptions = lengths[0].OptionList.map { $0.OptionName }
}
}
private func fetchLengths (completion: #escaping ([OptionsHead]) -> ()) {
var catalogID = 1877
// how can I change catalogID based on Product class variables
if product.type == "some type" {
catalogID = 1234
} else {
catalogID = 4321
}
....
}
I'm working with a backend developer that likes to encapsulate json bodies in another object such as data:
Example:
GET: /user/current:
{
data: {
firstName: "Evan",
lastName: "Stoddard"
}
}
I would simply like to just call json decode on the response to get a User struct that I've created but the added data object requires another struct. To get around this I created a generic template class:
struct DecodableData<DecodableType:Decodable>:Decodable {
var data:DecodableType
}
Now I can get my json payload and if I want to get a User struct just get the data property of my template:
let user = JSONDecoder().decode(DecodableData<User>.self, from: jsonData).data
This is all fine and dandy until sometimes, the key, data, isn't always data.
I feel like this is most likely fairly trivial stuff, but is there a way I can add a parameter in my template definition so I can change the enum coding keys as that data key might change?
Something like the following?
struct DecodableData<DecodableType:Decodable, Key:String>:Decodable {
enum CodingKeys: String, CodingKey {
case data = Key
}
var data:DecodableType
}
This way I can pass in the target decodable class along with the key that encapsulates that object.
No need for coding keys. Instead, you need a simple container that parses the JSON as a dictionary that has exactly one key-value pair, discarding the key.
struct Container<T>: Decodable where T: Decodable {
let value: T
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: T].self)
guard dict.count == 1 else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected exactly 1 key value pair, got \(dict.count)")
}
value = dict.first!.value
}
}
If the JSON is empty or has more than one key-value pair, an exception is raised.
Assuming a simple struct such as
struct Foo: Decodable, Equatable {
let a: Int
}
you can parse it regardless of the key:
let foo1 = try! JSONDecoder().decode(
Container<Foo>.self,
from: #"{ "data": { "a": 1 } }"#.data(using: .utf8)!
).value
let foo2 = try! JSONDecoder().decode(
Container<Foo>.self,
from: #"{ "doesn't matter at all": { "a": 1 } }"#.data(using: .utf8)!
).value
foo1 == foo2 // true
This also works for JSON responses that have null as the value, in which case you need to parse it as an optional of your type:
let foo = try! JSONDecoder().decode(
Container<Foo?>.self,
from: #"{ "data": null }"#.data(using: .utf8)!
).value // nil
Try with something like this:
struct GenericCodingKey: CodingKey {
var stringValue: String
init(value: String) {
self.stringValue = value
}
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
struct DecodableData<DecodableType: CustomDecodable>: Decodable {
var data: DecodableType
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: GenericCodingKey.self)
data = try container.decode(DecodableType.self, forKey: GenericCodingKey(value: DecodableType.dataKey))
}
}
protocol CustomDecodable: Decodable {
static var dataKey: String { get }
}
extension CustomDecodable {
static var dataKey: String {
return "data" // This is your default
}
}
struct CustomDataKeyStruct: CustomDecodable {
static var dataKey: String = "different"
}
struct NormalDataKeyStruct: CustomDecodable {
//Change Nothing
}
Here i am getting API response of all of my api.
{
"success" : true,
"message" : "",
"data" : {
/multipal data parameter/
}
}
And here is my codable model
struct Login: Codable {
let success: Bool
let message: String
let data: Data
struct Data: Codable {
}
}
How can i create common Sturct for success and message parameter.
You can make the root struct representing the network response generic, this will allow you to keep the success and message parts common between all specialised responses.
struct NetworkResponse<ResponseData:Codable>: Codable {
let success: Bool
let message: String
let data: ResponseData
}
You shouldn't create custom types with the same name as built in types, since that will lead to confusion, especially for other people reading your code, so I renamed your custom Data type to ResponseData.
For instance you can create a LoginResponse model and decode it like below. You can do the same for other responses from the same API.
let loginResponse = """
{
"success" : true,
"message" : "",
"data" : {
"username":"test",
"token":"whatever"
}
}
"""
struct LoginResponse: Codable {
let username: String
let token: String
}
do {
print(try JSONDecoder().decode(NetworkResponse<LoginResponse>.self, from: Data(loginResponse.utf8)))
} catch {
print(error)
}
Common structure :
I have created something like that
struct statusModel<T:Codable>: Codable {
let message : String
let resultData : [T]?
let status : Int
enum CodingKeys: String, CodingKey {
case message = "message"
case resultData = "resultData"
case status = "status"
}
}
Regular model (resultData)
struct modelInitialize : Codable {
let profileimgurl : String?
let projecturl : String?
enum CodingKeys: String, CodingKey {
case profileimgurl = "profileimgurl"
case projecturl = "projecturl"
}
}
You can set like as below
do {
guard let reponseData = responseData.value else {return} //Your webservice response in Data
guard let finalModel = try?JSONDecoder().decode(statusModel<modelInitialize>.self, from: reponseData) else {return}
}