I have an app that shows 4 options. Everyday you click on one or more of the options. Right now, it's storing in the firebase database like an array of string, where every string is one of the options. Like this
override func addSelection(selection: String) {
self.quitPlan.medications.append(selection)
}
var medications: [String] {
get {
return document[Properties.medications.rawValue] as? [String] ?? []
}
set {
document[Properties.medications.rawValue] = newValue
}
}
But I actually want an array of jsons, with the option and the option. I have try:
override func addSelection(selection: String) {
let medicationSelected = Medication(medication: selection, date: Date())
self.quitPlan.medications.append(medicationSelected)
}
var medications: [Medication] {
get {
return document[Properties.medications.rawValue] as? [Medication] ?? []
}
set {
document[Properties.medications.rawValue] = newValue
}
}
struct Medication {
let medication: String
let date: Date
}
But it's not working, I'm getting 'FIRInvalidArgumentException', reason: 'Unsupported type: __SwiftValue'
You can do something like this:
struct Medication {
let medication: String
let date: Date
private let divider = "|"
func toString() -> String {
return midication + divider + date.toString()
}
func from(_ string: String) -> Medication {
let arr = string.split(divider)
let medication = arr[0]
let date = // TODO: Date from string arr[1]
return Medication(medication: medication, date: date)
}}
and
self.quitPlan.medications.append(medicationSelected.toString())
Firebase cannot save custom Swift structs.
A possible solution is to encode the array of Medication to a JSON string.
struct Medication : Codable {
let medication: String
let date: Date
}
In the database change the type from an array of string to single string
var medications: [Medication] {
get {
guard let medicationJSON = document[Properties.medications.rawValue] as? String,
let data = medicationJSON.data(using: .utf8),
let medi = try? JSONDecoder().decode([Medication].self, from: data) else { return [] }
return medi
}
set {
let medicationData = try! JSONEncoder().encode(newValue)
document[Properties.medications.rawValue] = String(data: medicationData, encoding: .utf8)!
}
}
Firestore can save a Swift struct to a collection, there is a module for this.
First, you should include the module:
import FirebaseFirestoreSwift
Then, just do:
db.collection("yourCollectionName").document(from: yourSwiftObject)
It will be converted to be saved in your Firestore collection.
Related
I am trying to archive data and want to store it in userdefault but app getting crash.
Also tried this
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: selectedPoductDetails, requiringSecureCoding: false)
selectedPoductDetails is dict of type [String: SelectedProductDetail]
import Foundation
class SelectedProductDetail {
let product: String
var amount: Double
var title: String
init(product: String, amount: Double, title: String ) {
self.product = product
self.amount = amount
self.title = title
}
}
May i know why its not working and possible solution for same?
For this case you can use UserDefaults
struct ProductDetail: Codable {
//...
}
let encoder = JSONEncoder()
let selectedProductDetails = ProductDetail()
// Set
if let data = try? encoder.encode(selectedProductDetails) {
UserDefaults.standard.set(data, forKey: "selectedProductDetails")
}
// Get
if let selectedProductDetailsData = UserDefaults.standard.object(forKey: "selectedProductDetails") as? Data {
let selectedProductDetails = try? JSONDecoder().decode(ProductDetail.self, from: selectedProductDetailsData)
}
As mentioned in the comments to use NSKeyedArchiver the class must adopt NSSecureCoding and implement the two required methods.
The types in your class are JSON compatible, so adopt Codable and archive the data with JSONEncoder (or PropertyListEncoder). You could even use a struct and delete the init method
struct SelectedProductDetail: Codable {
let product: String
var amount: Double
var title: String
}
var productDetails = [String: SelectedProductDetail]()
// populate the dictionary
do {
let data = try JSONEncoder().encode(productDetails)
UserDefaults.standard.set(data, forKey: "productDetails")
} catch {
print(error)
}
And load it
do {
guard let data = UserDefaults.standard.data(forKey: "productDetails") else { return }
productDetails = try JSONDecoder().decode([String: SelectedProductDetail].self, from: data)
} catch {
print(error)
}
Note:
UserDefaults is the wrong place for user data. It's better to save the data in the Documents folder
I am trying to map my data to Model.
Where I am using Firestore snapshot listener, to get data.
here, I am getting data and mapping to "User" model that;
do{
let user = try User(dictionary: tempUserDic)
print("\(user.firstName)")
}
catch{
print("error occurred")
}
Here is my Model:
struct User {
let firstName: String
// var lon: Double = 0.0
// var refresh:Int = 0
// var createdOn: Timestamp = Timestamp()
}
//Testing Codable
extension User: Codable {
init(dictionary: [String: Any]) throws {
self = try JSONDecoder().decode(User.self, from: JSONSerialization.data(withJSONObject: dictionary))
}
private enum CodingKeys: String, CodingKey {
case firstName = "firstName"
}
}
Correct me if I am wrong.
Crashing because I am getting "Timestamp" in data.
Data getting from listener :
User Dictionary:
[\"firstName\": Ruchira,
\"lastInteraction\": FIRTimestamp: seconds=1576566738 nanoseconds=846000000>]"
How to map "Timestamp" to Model?
Tried "CodableFirstore" https://github.com/alickbass/CodableFirebase
An approach is to create an extension to type Dictionary that coverts a dictionary to any other type, but automatically modifies Date and Timestamp types to writeable JSON strings.
This is the code:
extension Dictionary {
func decodeTo<T>(_ type: T.Type) -> T? where T: Decodable {
var dict = self
// This block will change any Date and Timestamp type to Strings
dict.filter {
$0.value is Date || $0.value is Timestamp
}.forEach {
if $0.value is Date {
let date = $0.value as? Date ?? Date()
dict[$0.key] = date.timestampString as? Value
} else if $0.value is Timestamp {
let date = $0.value as? Timestamp ?? Timestamp()
dict[$0.key] = date.dateValue().timestampString as? Value
}
}
let jsonData = (try? JSONSerialization.data(withJSONObject: dict, options: [])) ?? nil
if let jsonData {
return (try? JSONDecoder().decode(type, from: jsonData)) ?? nil
} else {
return nil
}
}
}
The .timestampString method is also declared in an extension for type Date:
extension Date {
var timestampString: String {
Date.timestampFormatter.string(from: self)
}
static private var timestampFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(identifier: "UTC")
return dateFormatter
}
}
Usage, like in the case of the question:
let user = tempUserDict.decodeTo(User.self)
I solved this by converting the FIRTimestamp fields to Double (seconds) so the JSONSerialization could parse it accordingly.
let items: [T] = documents.compactMap { query in
var data = query.data() // get a copy of the data to be modified.
// If any of the fields value is a `FIRTimestamp` we replace it for a `Double`.
if let index = (data.keys.firstIndex{ data[$0] is FIRTimestamp }) {
// Convert the field to `Timestamp`
let timestamp: Timestamp = data[data.keys[index]] as! Timestamp
// Get the seconds of it and replace it on the `copy` of `data`.
data[data.keys[index]] = timestamp.seconds
}
// This should not complain anymore.
guard let data = try? JSONSerialization.data(
withJSONObject: data,
options: .prettyPrinted
) else { return nil }
// Make sure your decoder setups the decoding strategy to `. secondsSince1970` (see timestamp.seconds documentation).
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
return try? decoder.decode(T.self, from: data)
}
// Use now your beautiful `items`
return .success(items)
I would like to store the previous 4 days closing event in an individual struct so that i can make reference to them later on in the program. How would you go about storing the the closing event for each 4 days after sorting them from the JSON API.
The code below has sorted the previous 4 days but i am unable to figure how to store each day to use them separately
class DailyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=MSFT&apikey=demo"
let urlObj = URL(string: jsonUrlString)
URLSession.shared.dataTask(with: urlObj!) {(data, response, error) in
guard let data = data else { return }
do {
let forex = try JSONDecoder().decode(Root.self, from: data)
let sortedKeys = forex.timeSeriesDaily.keys.sorted(by: >)
let requestedKeys = sortedKeys.prefix(4)
var requestedPrices = [String:Forex]()
requestedKeys.forEach{ requestedPrices[$0] = forex.timeSeriesDaily[$0] }
print(requestedPrices)
print()
} catch {
print(error)
}
}.resume()
}
struct Root: Codable {
let metaData: [String: String]
let timeSeriesDaily: [String:Forex]
enum CodingKeys: String, CodingKey {
case timeSeriesDaily = "Time Series (Daily)"
case metaData = "Meta Data"
}
}
struct Forex: Codable {
let open, high, low, close: String
enum CodingKeys: String, CodingKey {
case open = "1. open"
case high = "2. high"
case low = "3. low"
case close = "4. close"
}
}
}
One way is to create a struct with four properties for this and add a specific init that takes an array
struct LastFour {
var close1: String
var close2: String
var close3: String
var close4: String
init?(_ closes: [String]) {
guard closes.count >= 4 else {
return nil
}
close1 = closes[0]
close2 = closes[1]
close3 = closes[2]
close4 = closes[3]
}
}
and then use map when initialising the struct from the dictionary
let lastFour = LastFour(requestedPrices.values.map {$0.close})
Note that the init is optional and returns nil in case the array is to short, another option could be to throw an error for instance.
Maybe a more flexible solution would be to use an array internally in the struct and then access the data via a method or perhaps computed properties
struct LastFour {
private var closeEvents: [String]
func close(at index: Int) -> String {
}
}
This would of course require similar code for init and checking the correct size but it would be easier to change if more or less elements are needed
My suggestion is to create another struct with the date and the close price
struct CloseData {
let date, price : String
}
and populate it
do {
let forex = try JSONDecoder().decode(Root.self, from: data)
let sortedKeys = forex.timeSeriesDaily.keys.sorted(by: >)
let requestedKeys = sortedKeys.prefix(4)
let requestedPrices = requestedKeys.map{ CloseData(date: $0, price: forex.timeSeriesDaily[$0]!.close) }
The result is an array of CloseData items
I have classes like these:
class MyDate
{
var year : String = ""
var month : String = ""
var day : String = ""
init(year : String , month : String , day : String) {
self.year = year
self.month = month
self.day = day
}
}
class Lad
{
var firstName : String = ""
var lastName : String = ""
var dateOfBirth : MyDate?
init(firstname : String , lastname : String , dateofbirth : MyDate) {
self.firstName = firstname
self.lastName = lastname
self.dateOfBirth = dateofbirth
}
}
class MainCon {
func sendData() {
let myDate = MyDate(year: "1901", month: "4", day: "30")
let obj = Lad(firstname: "Markoff", lastname: "Chaney", dateofbirth: myDate)
let api = ApiService()
api.postDataToTheServer(led: obj)
}
}
class ApiService {
func postDataToTheServer(led : Lad) {
// here i need to json
}
}
And I would like to turn a Lad object into a JSON string like this:
{
"firstName":"Markoff",
"lastName":"Chaney",
"dateOfBirth":
{
"year":"1901",
"month":"4",
"day":"30"
}
}
EDIT - 10/31/2017: This answer mostly applies to Swift 3 and possibly earlier versions. As of late 2017, we now have Swift 4 and you should be using the Encodable and Decodable protocols to convert data between representations including JSON and file encodings. (You can add the Codable protocol to use both encoding and decoding)
The usual solution for working with JSON in Swift is to use dictionaries. So you could do:
extension Date {
var dataDictionary {
return [
"year": self.year,
"month": self.month,
"day": self.day
];
}
}
extension Lad {
var dataDictionary {
return [
"firstName": self.firstName,
"lastName": self.lastName,
"dateOfBirth": self.dateOfBirth.dataDictionary
];
}
}
and then serialize the dictionary-formatted data using JSONSerialization.
//someLad is a Lad object
do {
// encoding dictionary data to JSON
let jsonData = try JSONSerialization.data(withJSONObject: someLad.dataDictionary,
options: .prettyPrinted)
// decoding JSON to Swift object
let decoded = try JSONSerialization.jsonObject(with: jsonData, options: [])
// after decoding, "decoded" is of type `Any?`, so it can't be used
// we must check for nil and cast it to the right type
if let dataFromJSON = decoded as? [String: Any] {
// use dataFromJSON
}
} catch {
// handle conversion errors
}
If you just need to do this for few classes, providing methods to turn them into dictionaries is the most readable option and won't make your app noticeably larger.
However, if you need to turn a lot of different classes into JSON it would be tedious to write out how to turn each class into a dictionary. So it would be useful to use some sort of reflection API in order to be able to list out the properties of an object. The most stable option seems to be EVReflection. Using EVReflection, for each class we want to turn into json we can do:
extension SomeClass: EVReflectable { }
let someObject: SomeClass = SomeClass();
let someObjectDictionary = someObject.toDictionary();
and then, just like before, we can serialize the dictionary we just obtained to JSON using JSONSerialization. We'll just need to use object.toDictionary() instead of object.dataDictionary.
If you don't want to use EVReflection, you can implement reflection (the ability to see which fields an object has and iterate over them) yourself by using the Mirror class. There's an explanation of how to use Mirror for this purpose here.
So, having defined either a .dataDictionary computed variable or using EVReflection's .toDictionary() method, we can do
class ApiService {
func postDataToTheServer(lad: Lad) {
//if using a custom method
let dict = lad.dataDictionary
//if using EVReflection
let dict = lad.toDictionary()
//now, we turn it into JSON
do {
let jsonData = try JSONSerialization.data(withJSONObject: dict,
options: .prettyPrinted)
// send jsonData to server
} catch {
// handle errors
}
}
}
May this GitHub code will help you.
protocol SwiftJsonMappable {
func getDictionary() -> [String: Any]
func JSONString() -> String
}
extension SwiftJsonMappable {
//Convert the Swift dictionary to JSON String
func JSONString() -> String {
do {
let jsonData = try JSONSerialization.data(withJSONObject: self.getDictionary(), options: .prettyPrinted)
// here "jsonData" is the dictionary encoded in JSON data
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
// here "decoded" is of type `Any`, decoded from JSON data
return jsonString
// you can now cast it with the right type
} catch {
print(error.localizedDescription)
}
return ""
}
//Convert Swift object to Swift Dictionary
func getDictionary() -> [String: Any] {
var request : [String : Any] = [:]
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if let lable = child.label {
//For Nil value found for any swift propery, that property should be skipped. if you wanna print nil on json, disable the below condition
if !checkAnyContainsNil(object: child.value) {
//Check whether is custom swift class
if isCustomType(object: child.value) {
//Checking whether its an array of custom objects
if isArrayType(object: child.value) {
if let objects = child.value as? [AMSwiftBase] {
var decodeObjects : [[String:Any]] = []
for object in objects {
//If its a custom object, nested conversion of swift object to Swift Dictionary
decodeObjects.append(object.getDictionary())
}
request[lable] = decodeObjects
}
}else {
//Not an arry, and custom swift object, convert it to swift Dictionary
request[lable] = (child.value as! AMSwiftBase).getDictionary()
}
}else {
request[lable] = child.value
}
}
}
}
return request
}
//Checking the swift object is swift base type or custom Swift object
private func isCustomType(object : Any) -> Bool {
let typeString = String(describing: type(of: object))
if typeString.contains("String") || typeString.contains("Double") || typeString.contains("Bool") {
return false
}
return true
}
//checking array
private func isArrayType(object : Any) -> Bool {
let typeString = String(describing: type(of: object))
if typeString.contains("Array"){
return true
}
return false
}
//Checking nil object
private func checkAnyContainsNil(object : Any) -> Bool {
let value = "\(object)"
if value == "nil" {
return true
}
return false
}
}
https://github.com/anumothuR/SwifttoJson
I want to use structs for the (very simple) model of my app.
However NSKeyedArchiver only accepts objects (extending NSObjects).
Is there any good way to save a struct to a file?
A very simple approach I used sometimes. The quantity of code you need to write is no more then in the class/NSCoding scenario.
First of all import the great SwiftyJSON lib.
Let's start with a simple struct
struct Starship {
let name: String
let warpSpeed: Bool
let captain: String?
init(name: String, warpSpeed: Bool, captain: String? = nil) {
self.name = name
self.warpSpeed = warpSpeed
self.captain = captain
}
}
Let's make it convertible to/from a JSON
struct Starship {
let name: String
let warpSpeed: Bool
let captain: String?
init(name: String, warpSpeed: Bool, captain: String? = nil) {
self.name = name
self.warpSpeed = warpSpeed
self.captain = captain
}
init?(json: JSON) {
guard let
name = json["name"].string,
warpSpeed = json["warpSpeed"].bool
else { return nil }
self.name = name
self.warpSpeed = warpSpeed
self.captain = json["captain"].string
}
var asJSON: JSON {
var json: JSON = [:]
json["name"].string = name
json["warpSpeed"].bool = warpSpeed
json["captain"].string = captain
return json
}
}
That's it. Let's use it
let enterprise = Starship(name: "Enteriprise D", warpSpeed: true, captain: "JeanLuc Picard")
let json = enterprise.asJSON
let data = try! json.rawData()
// save data to file and reload it
let newJson = JSON(data: data)
let ship = Starship(json: newJson)
ship?.name // "Enterprise D"
Updated solution for Swift 5+
No need to import anything
Make your struct codable
struct Starship: Codable { // Add this
let name: String
let warpSpeed: Bool
let captain: String?
init(name: String, warpSpeed: Bool, captain: String? = nil) {
self.name = name
self.warpSpeed = warpSpeed
self.captain = captain
}
}
Functions to read from and write to file
var data: Starship
func readFromFile(filePathURL: URL) throws {
let readData = try Data(contentsOf: filePathURL)
self.data = try JSONDecoder().decode(Starship.self, from: readData)
}
func writeToFile(filePathURL: URL) throws {
let jsonData = try JSONEncoder().encode(self.data)
try jsonData.write(to: filePathURL)
}