I get a web API response, which I loop through and initialize a Customer class for each JSON object in the array. The Customer class has a base class with a couple optional properties. I get errors when there are null values in the JSON, and I am not sure how to handle them properly. I am especially unsure of how to handle a null Date in swift. If anyone could give me some advice, I would really appreciate it!
JSON returned from Web API:
[{"Id":1,"BusinessId":1,"CompanyName":"Test Company4","FirstName":"Mike","LastName":"Doe","CustomerType":1,"CustomerStatus":1,"IsDeleted":false,"Created":"2016-12-22T20:12:10.2760144Z","CreatedBy":1,"Modified":"2016-12-29T19:29:26.1245219Z","ModifiedBy":1},{"Id":2,"BusinessId":1,"CompanyName":"Test Company5","FirstName":"Mike","LastName":"Doe","CustomerType":1,"CustomerStatus":1,"IsDeleted":false,"Created":"2017-01-03T20:18:51.7639708Z","CreatedBy":1,"Modified":null,"ModifiedBy":null}]
Customer class:
class Customer : BaseEntity {
var Id: Int64
var BusinessId: Int64
var CustomerType: Int64
var CustomerStatus: Int64
var CompanyName: String
var FirstName: String
var LastName: String
init?(json: [String: Any]) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
guard let id = json["Id"] as? Int64,
let businessId = json["BusinessId"] as? Int64,
let customerType = json["CustomerType"] as? Int64,
let customerStatus = json["CustomerStatus"] as? Int64,
let companyName = json["CompanyName"] as? String,
let firstName = json["FirstName"] as? String,
let lastName = json["LastName"] as? String,
let isDeleted = json["IsDeleted"] as? Bool
else {
return nil
}
var created = Date()
if let cdate = json["Created"] as? String {
created = formatter.date(from: cdate)!
}
let createdBy = json["CreatedBy"] as? String
var modified = Date()
if let mdate = json["Modified"] as? String {
modified = formatter.date(from: mdate)!
}
let modifiedBy = json["ModifiedBy"] as? String
self.Id = id
self.BusinessId = businessId
self.CustomerType = customerType
self.CustomerStatus = customerStatus
self.CompanyName = companyName
self.FirstName = firstName
self.LastName = lastName
super.init(isDeleted: isDeleted, created: created, createdBy: createdBy, modified: modified, modifiedBy: modifiedBy)
}
}
BaseEntity class that contains some optional properties:
class BaseEntity {
var IsDeleted: Bool
var Created: Date
var CreatedBy: String
var Modified: Date?
var ModifiedBy: String?
init(isDeleted: Bool, created: Date, createdBy: String, modified: Date, modifiedBy: String) {
self.IsDeleted = isDeleted
self.Created = created
self.CreatedBy = createdBy
self.Modified = modified
self.ModifiedBy = modifiedBy
}
}
One of the problems I am running into is when I initialize Customer, my Customer object looks like this. Then when I try to insert it into a database, it does not recognize the BaseEntity properties as properties of Customer:
Id = 1
BusinessId = 1
CustomerType = 1
CustomerStatus = 1
CompanyName = "Test Company4"
FirstName = "Mike"
LastName = "Doe"
BaseEntity
IsDeleted = true
Created = '1/1/2017'
CreatedBy = 1
Modified = '1/1/2017'
ModifiedBy = 1
You need to define your class type for model
class BaseEntity {
to
class BaseEntity: NSObject {
This is the way you can create your model class: -
//Model Class
class BaseModel: NSObject {
var name: String
var address: String
var mobilenumber: Int
init(name: String?, address: String?, mobilenumber: Int?) {
self.name = name ?? ""
self.address = address ?? ""
self.mobilenumber = mobilenumber ?? 0
}
}
//Pass value inside model class
class ViewController: UIViewController {
var model = [BaseModel]() //Model Array Initialization here
override func viewDidLoad() {
super.viewDidLoad()
//Pass value inside Model
model.append(BaseModel(name: "Your name", address: "Your address", mobilenumber: 5545545452))
}
}
//Get value from Model class
class DetailsController: UIViewController {
var details: BaseModel?
override func viewDidLoad() {
super.viewDidLoad()
//Retrive value from model
let name = details?.name ?? ""
let address = details?.address ?? ""
let mobile = details?.mobilenumber ?? 0
}
}
Related
i'm currently struggling to fetch any changes from an published variable in SwiftUI. Most of the code is created after this tutorial on YouTube.
It's basically an app, that fetches cryptos from a firebase database. To avoid high server costs I want to update any changes of the coins to the database but not have an observer to lower the download rate.
What's the bug?
When I'm adding a coin to my favorites, it sends the data correctly to the database and updates the UI. However when I try to filter the coins the Coin-array switches back to it's previous state. I also added a breakpoint on the CoinCellViewModel(coin: coin)-Line but it only gets executed when I change the filterBy. Here's a little visualisation of the bug:
Repository
class CoinsRepository: ObservableObject {
#Published var coins = [Coin]()
var ref: DatabaseReference!
init() {
self.ref = Database.database().reference()
loadDatabase(ref)
}
func loadDatabase(_ ref: DatabaseReference) {
ref.child("coins").observeSingleEvent(of: .value) { snapshot in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var coinNames: [String] = []
self.coins = dictionaries.compactMap({ (key: String, value: Any) in
guard let dic = value as? [String: Any] else { return nil }
coinNames.append(dic["name"] as? String ?? "")
return Coin(dic)
})
}
}
func updateFavorite(_ coin: Coin, state: Bool) {
let path = ref.child("coins/\(coin.name)")
var flag = false
path.updateChildValues(["favorite": state]) { err, ref in
if let err = err {
print("ERROR: \(err.localizedDescription)")
} else {
var i = 0
var newCoinArray = self.coins
for coinA in newCoinArray {
if coinA.name == coin.name {
newCoinArray[i].favorite = state
}
i += 1
}
// I guess here's the error
DispatchQueue.main.async {
self.objectWillChange.send()
self.coins = newCoinArray
}
}
}
}
}
ViewModel
class CoinListViewModel: ObservableObject {
#Published var coinRepository = CoinsRepository()
#Published var coinCellViewModels = [CoinCellViewModel]()
#Published var filterBy: [Bool] = UserDefaults.standard.array(forKey: "filter") as? [Bool] ?? [false, false, false]
#Published var fbPrice: Double = 0.00
#Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.combineLatest(coinRepository.$coins, $fbPrice, $filterBy)
.map(filter)
.sink { coins in
self.coinCellViewModels = coins.map { coin in
CoinCellViewModel(coin: coin)
}
}
.store(in: &cancellables)
}
...
}
updateFavorite(_ coin: Coin, state: Bool) get's called in the CoinCellViewModel() but I guess the code isn't necessary here...
I'm fairly new to the Combine topic and not quite getting all the new methods, so any help is appreciated!
So I'm trying to save my form into a model, but i keep getting error "Type of expression is ambiguous without more context", I've tried with just a simple model with 2 properties and have no problem. So I'm stuck and not sure how to move forward, haven't found anything helpful on internet.
let me = Trip(id: UUID().uuidString, name: "Test", description: "Test", total: "0.00", coverImage: "lion", phase: Phase.new.rawValue, startDate: Date(), endDate: Date(), tenative: false, gallery: [], members: [], maxSeats: 50, paymentPlan: false) < -- this where i get the error
//print(text)
// let trip = Trip(body: text)
// Amplify.DataStore.save(trip) { result in
// switch result {
// case .success:
// print("saved trip")
// case .failure(let error):
// print(error)
// }
//
// }
// presentationMode.wrappedValue.dismiss()
} ```
``` public struct Trip: Model {
public let id: String
public var name: String
public var description: String
public var total: String?
public var coverImage: String
public var phase: Phase?
public var startDate: Temporal.DateTime
public var endDate: Temporal.DateTime
public var tenative: Bool
public var gallery: [String]?
public var members: [String]?
public var maxSeats: Int?
public var paymentPlan: Bool?
public init(id: String = UUID().uuidString,
name: String,
description: String,
total: String? = nil,
coverImage: String,
phase: Phase? = nil,
startDate: Temporal.DateTime,
endDate: Temporal.DateTime,
tenative: Bool,
gallery: [String]? = [],
members: [String]? = [],
maxSeats: Int? = nil,
paymentPlan: Bool? = nil) {
self.id = id
self.name = name
self.description = description
self.total = total
self.coverImage = coverImage
self.phase = phase
self.startDate = startDate
self.endDate = endDate
self.tenative = tenative
self.gallery = gallery
self.members = members
self.maxSeats = maxSeats
self.paymentPlan = paymentPlan
}
}
I actually is working now, I just recreated the model and tried again and error went away.
I have a view that listens to a Model via and ObservableObject:
class Feed : ObservableObject {
// Posts to be displayed
#Published var posts = [Posts]()
...
...
}
And the Posts model looks like:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
}
Which contains multiple other Struct models like Friends and Bars. However, when I do change a value within one of these other models, it doesn't trigger the #Published to fire, so the view isn't redrawn. For example, the Friends model looks like:
struct Friends : Hashable {
static func == (lhs: Friends, rhs: Friends) -> Bool {
return lhs.id == rhs.id
}
let name: String
let username: String
let id : String
var thumbnail : UIImage?
var profileImgURL : String?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
but when I change the thumbnail, the views are not redrawn. But when I change something directly apart of the Posts model, like the description attribute, the view is redrawn. How am I able to have the view redraw when the underlying model values are changed?
I change the thumbnail as shown:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
}
}
}
}
But if I were to change the description doing the same thing:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
And when I do this, the view does update and change. I can see that the thumbnails are being loaded correctly, too, because I can print out the data sent, and sometimes the thumbnails are redrawn for the view correctly.
EDIT
As suggested I tried adding a mutating func to the struct:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
mutating func addInitThumbnail(img : UIImage) {
self.intializer.thumbnail = img
}
}
and then using it:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
if let thumbnailImg = UIImage(data: data) {
self.posts[indexOfPost].addInitThumbnail(img: thumbnailImg)
}
}
}
}
}
}
but it did not work either.
However, when I do:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
}
the images are loaded and set correctly...? So I think it might have something to do with UIImages directly?
I tried using mutating function and also updating value directly, both cases it worked.
UPDATED CODE (Added UIImage in new struct)
import SwiftUI
import Foundation
//Employee
struct Employee : Identifiable{
var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon
init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}
mutating func updateAddress(with value: Address){
address = value
}
}
//User profile image
struct UserIcon {
var profile: UIImage?
init(profile: UIImage) {
self.profile = profile
}
mutating func updateProfile(image: UIImage) {
self.profile = image
}
}
//Address
struct Address {
var houseName: String = ""
var houseNumber: String = ""
var place: String = ""
init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}
func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}
//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
#Published var users : [Employee] = []
func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}
func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}
func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}
//EmployeeView
struct EmployeeView: View {
#ObservedObject var vm = EmployeeViewModel()
var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)
}.accentColor(Color.init("blackTextColor"))
}
func fetch() {
self.vm.initialize()
}
}
it's been a long time but still :
1 - mutating func is not necessary.
2 - The re-rendering won't happen if you only change the nested object and not the "observed" object it self.
3 - You can play with the getters and setters as well, to pull the wanted value to change and update it back.
Considering we have a complex object such as :
struct Content{
var listOfStuff : [Any] = ["List", 2, "Of", "Stuff"]
var isTheSkyGrey : Bool = false
var doYouLikeMyMom : Bool = false
var status : UIImage? = UIImage(systemName: "paperplane")
}
Now let's wrap/nest this object into a ContentModel for the View. If, while using the #State var contentModel : ContentModel in the View, we change change one of the properties directly by accessing the nested object(like so : model.content.status = "Tchak"), it will not trigger a re-rendering because the ContentModel itself didn't change.
Understanding this, we need to trigger a tiny useless change in the ContentModel :
struct ContentModel {
private var change : Bool = false
private var content : Content = Content() {
didSet{
// this will trigger the view to re-render
change.toggle()
}
}
//the value you want to change
var status : UIImage?{
get{
contentModel.status
}
set{
contentModel.status = newValue
}
}
}
Now what's left to do is to observe the change of the content in the view.
struct ContentPouf: View {
#State var contentModel = ContentModel()
var body: some View {
Image(uiImage: contentModel.status)
.onTapGesture {
contentModel.status = UIImage(systemName: "pencil")
}
}
}
and using an ObservableObject it would be :
class ContentObservable : ObservableObject {
#Published var content : ContentModel = ContentModel()
func handleTap(){
content.status = UIImage(systemName: "pencil")
}
}
and
#StateObject var viewModel : ContentObservable = ContentObservable()
var body: some View {
Image(uiImage :viewModel.content.status)
.onTapGesture {
viewModel.handleTap()
}
}
I am doing an application which need to store the user's data in local. I read the documentation in Apple website, it covers part of the info in how to encode basic type, such as String and Int. However, when I try to encode with the [CLLocation]type, it failed. I am asking if some expert can give me any hint on how to encode such type in Swift?
Here is my code about the Model class.
import Foundation
import UIKit
import CoreLocation
import os.log
import CoreData
//route model
//store the model in the local database.
//change all the [CLLocations] to become the String inorder to store it in local.???? not willing
class Route: NSObject, NSCoding {
//MARK: Properties
var name :String
var area : String
var image: UIImage?
var date : DispatchTime
var routePoints = [CLLocation]()
var rating: Int
//MARK: Achieving paths
//static properties, belong to the
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("Routes")
//MARK: NSCoding
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.name)
aCoder.encode(image, forKey: PropertyKey.image)
aCoder.encode(date, forKey: PropertyKey.date)
aCoder.encode(area, forKey: PropertyKey.area)
//unable to encode the [cllocation]
aCoder.encode(routePoints, forKey: PropertyKey.routePoints)
aCoder.encode(rating, forKey: PropertyKey.rating)
}
//should also added in the locations as one of the variable.
init?(Name: String, Area: String, Date: DispatchTime, Image: UIImage?, RoutePoints: [CLLocation], Rating: Int) {
guard (Rating >= 0) && (Rating <= 5) else {
return nil
}
self.name = Name
self.area = Area
self.image = Image
self.date = Date
self.routePoints = RoutePoints
self.rating = Rating
}
required convenience init?(coder aDecoder: NSCoder){
//this is the necessary property
//these are optional properties
let rating = aDecoder.decodeInteger(forKey: PropertyKey.rating)
guard let date = aDecoder.decodeObject(forKey: PropertyKey.date) as? DispatchTime,
let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String,
let image = aDecoder.decodeObject(forKey: PropertyKey.image) as? UIImage,
let area = aDecoder.decodeObject(forKey: PropertyKey.area) as? String,
let routePoints = aDecoder.decodeObject(forKey: PropertyKey.routePoints) as? [CLLocation] else{
print("Unable to decode")
return nil
}
self.init(Name: name, Area: area, Date: date, Image: image, RoutePoints: routePoints, Rating: rating)
}
//MARK: Types
struct PropertyKey {
static let name = "name"
static let image = "image"
static let date = "date"
static let area = "area"
static let rating = "rating"
static let routePoints = "routePoints"
}
}
Best wishes
The reason is that CLLocation can't be stored with NSCoder. You could implement a simple coder / decoder using a swift map for this type of value to basically store the locations in a dictionary.
let p1 = CLLocation(latitude: 1, longitude: 1)
let p2 = CLLocation(latitude: 2, longitude:2)
var locations = [p1, p2]
let codedArray = locations.map{ ["lat":$0.coordinate.latitude, "long":$0.coordinate.longitude] }
let decodedArray = codedArray.map{ CLLocation(latitude:$0["lat"]!, longitude:$0["long"]!) }
let jsonResult = try JSONSerialization.jsonObject(with: jsonData!, options: .mutableContainers) as! NSDictionary
When debugger comes on above line, in debug console there is ->
jsonResult =(NSDictionary) 2 key/value pairs
>[0] = “CompanyList” : 140 elements
>[1] = “StatusTable” : 1 element
jsonResult contains 2 array
Now i wanna to traverse CompanyList using Loop
like
let arr_CompanyList = [CompanyList]()
for dictionary in json as! [[CompanyList]]
{
//arr_CompanyList.append(dictionary)
}
but is gives error
Here is CompanyList Class
public class CompanyList {
public var companyAlt_Key : Int?
public var company_Name : String?
public var tableName : String?
}
How should i do?
You cannot convert your JSON Array response to directly your Class objects array, you need to create your custom class object from the JSON response. Also instead of using NSDictionary in swift use native type Dictionary.
if let jsonResult = (try? JSONSerialization.jsonObject(with: jsonData!, options: [])) as? [String:Any] {
if let companyList = jsonResult["CompanyList"] as? [[String:Any]] {
//Now loop through the companyList array
let arr_CompanyList = companyList.flatMap(CompanyList.init)
//To get array of companyname
let companyNames = companyList.flatMap { $0["Company_Name"] as? String }
print(companyNames)
}
}
Now simply add one init with your CompanyList class like this way.
public class CompanyList {
public var companyAlt_Key : Int?
public var company_Name : String?
public var tableName : String?
init?(dictionary: [String:Any]) {
guard let companyAltKey = dictionary["CompanyAlt_Key"] as? Int,
let companyName = dictionary["Company_Name"] as? String,
let tableName = dictionary["TableName"] as? String else {
return nil
}
self.companyAlt_Key = companyAltKey
self.company_Name = companyName
self.tableName = tableName
}
}
Note: Inside init? method with dictionary you need to access your key that contains value according to your class property.