Retrieve Map from Firebase Firestore using REST API on iOS - ios

I'm trying to retrieve maps values from a Firebase Firestore JSON file. So far I've been able to get any field I wanted but I'm having trouble with maps since they are so nested.
Here you can see an image of the JSON.
And here you can see as text.
"user_metrics": {
"arrayValue": {
"values": [
{
"mapValue": {
"fields": {
"point1": {
"integerValue": "0"
},
"point2": {
"integerValue": "0"
},
"metricDescription": {
"stringValue": "Distanza Pupillare"
},
"metricResult": {
"doubleValue": 6.27
},
"metricDescriptionEn": {
"stringValue": "Distance between pupils"
},
"metricTitle": {
"stringValue": "DBP"
}
}
}
},
And I'm decoding it as follows:
struct SessionResponse : Codable {
let sessions : [Session_Struct]
private enum CodingKeys : String, CodingKey {
case sessions = "documents"
}
}
struct SessionStringValue : Codable {
let value : String
private enum CodingKeys : String, CodingKey {
case value = "stringValue"
}
}
struct Session_Struct : Codable {
let is_first_session : Bool
let device : String
let glassesRefs : [String]
let ref_catalogo : String
let total_time : Double
let data_inizio_sessione : String
let data_fine_user_session : String
let device_id : String
let lineaRefs : [String]
let modelRefs : [String]
let user_metrics : [String]
private enum SessionKeys : String, CodingKey {
case fields
}
private enum FieldKeys : String, CodingKey {
case is_first_session
case device
case glassesRefs
case ref_catalogo
case total_time
case data_inizio_sessione
case data_fine_user_session
case device_id
case lineaRefs
case modelRefs
case user_metrics
}
// MARK: - Total Time
struct TotalTime: Codable {
let doubleValue: Double
}
// MARK: - First Session
struct FirstSession: Codable {
let booleanValue: Bool
}
// MARK: - ListaRefsSizes
struct MapMetrics: Codable {
let arrayValue: MetricsArrayValue
}
// MARK: - ArrayValue
struct MetricsArrayValue: Codable {
let values: [ValueMetric]
}
// MARK: - ArrayValue
struct ValueMetric: Codable {
let mapValue : MapValue
}
// MARK: - ListaRefsSizes
struct ListaRefsGlasses: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let stringValue: String
}
// MARK: - Value
struct MapValue: Codable {
let mapValue: String
}
// MARK: - Value
struct ReferenceValue: Codable {
let referenceValue: String
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SessionKeys.self)
let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields)
is_first_session = try fieldContainer.decode(FirstSession.self, forKey: .is_first_session).booleanValue
device = try fieldContainer.decode(SessionStringValue.self, forKey: .device).value
ref_catalogo = try fieldContainer.decode(ReferenceValue.self, forKey: .ref_catalogo).referenceValue
total_time = try fieldContainer.decode(TotalTime.self, forKey: .total_time).doubleValue
data_inizio_sessione = try fieldContainer.decode(SessionStringValue.self, forKey: .data_inizio_sessione).value
data_fine_user_session = try fieldContainer.decode(SessionStringValue.self, forKey: .data_fine_user_session).value
device_id = try fieldContainer.decode(SessionStringValue.self, forKey: .device_id).value
//Lists
glassesRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .glassesRefs)
.arrayValue.values.map{ $0.stringValue }
lineaRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .lineaRefs)
.arrayValue.values.map{ $0.stringValue }
modelRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .modelRefs)
.arrayValue.values.map{ $0.stringValue }
user_metrics = try fieldContainer.decode(MapMetrics.self, forKey: .user_metrics)
.arrayValue.values.map{ $0.mapValue } // <-- How to read this??
}
}
My issue is that I'm not being able to read the array inside the field "user_metrics". Any idea on how to achieve that? Thanks to anyone who can help!

import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let userMetrics: UserMetrics
enum CodingKeys: String, CodingKey {
case userMetrics = "user_metrics"
}
}
// MARK: - UserMetrics
struct UserMetrics: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let mapValue: MapValue
}
// MARK: - MapValue
struct MapValue: Codable {
let fields: Fields
}
// MARK: - Fields
struct Fields: Codable {
let point1, point2: Point
let metricDescription: Metric
let metricResult: MetricResult
let metricDescriptionEn, metricTitle: Metric
}
// MARK: - Metric
struct Metric: Codable {
let stringValue: String
}
// MARK: - MetricResult
struct MetricResult: Codable {
let doubleValue: Double
}
// MARK: - Point
struct Point: Codable {
let integerValue: String
}
there is structure for your json
call the api request and get data to Welcome(renamed it as you want)
func requestHTTP(urlString:String,completionHandler:#escaping(_ model:Welcome?,_ error:Error?) -> Void) {
guard let requestUrl = URL(string: urlString) else {return}
URLSession.shared.dataTask(with: requestUrl) { (data, httpUrlResponse, error) in
if(error == nil && data != nil && data?.count != 0) {
do {
let response = try JSONDecoder().decode(Welcome.self, from: data!)
completionHandler(response,nil)
} catch {
debugPrint("error")
completionHandler(nil,error)
}
} else {
completionHandler(nil,error)
}
}.resume()
}
and your will get your data easily
func getDataOnController() {
requestHTTP(urlString: "your url here") { model, error in
if let model = model {
let mapVlues = model.userMetrics.arrayValue.values.map({$0.mapValue})
print(mapVlues)
print(mapVlues.count)
}
}
}

Related

Retrieve Array from Firebase Firestore using REST API on iOS

I'm using REST APIs to retrieve data from my Firestore DB. I'm forced to use REST API instead of the Firebase SDK since App Clip don't allow to use the latter.
The JSON file is the following: JSON File
And, as text:
{
"name": "projects/myProject/databases/(default)/documents/Brand/rxnBLnp736gqjFBNLxxx",
"fields": {
"descrizione": {
"stringValue": "My project Brand Demo"
},
"descrizione_en": {
"stringValue": "My project Brand Demo"
},
"listaRefsLinea": {
"arrayValue": {
"values": [
{
"referenceValue": "projects/myProject/databases/(default)/documents/Linea/aeeDNuY9xEvRvyM5cxxx"
}
]
}
},
"data_consumption": {
"stringValue": "7xpISf0XxRnfrnUkNxxx"
},
"url_logo": {
"stringValue": "gs://myproject.appspot.com/FCMImages/app-demo-catalogue.png"
},
"web_url": {
"stringValue": "www.mybrand.it"
},
"nome_brand": {
"stringValue": "My project Demo"
}
},
"createTime": "2021-05-19T10:34:51.828685Z",
"updateTime": "2022-05-24T14:03:16.121296Z"
}
And I'm decoding it as follows:
import Foundation
struct BrandResponse : Codable {
let brands : [Brand_Struct]
private enum CodingKeys : String, CodingKey {
case brands = "documents"
}
}
struct StringValue : Codable {
let value : String
private enum CodingKeys : String, CodingKey {
case value = "stringValue"
}
}
struct Brand_Struct : Codable {
let url_logo : String
let web_url : String
let nome_brand : String
let descrizione : String
let listaRefsLinea : [String]
let descrizione_en : String
let data_consumption : String
private enum BrandKeys : String, CodingKey {
case fields
case listaRefsLinea
}
private enum FieldKeys : String, CodingKey {
case url_logo
case web_url
case nome_brand
case descrizione
case listaRefsLinea
case descrizione_en
case data_consumption
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: BrandKeys.self)
let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields)
//listaRefsLinea = try containerListaRefsLinea_2.decode(ArrayValue.self, forKey: .values).referenceValue
nome_brand = try fieldContainer.decode(StringValue.self, forKey: .nome_brand).value
web_url = try fieldContainer.decode(StringValue.self, forKey: .web_url).value
url_logo = try fieldContainer.decode(StringValue.self, forKey: .url_logo).value
descrizione = try fieldContainer.decode(StringValue.self, forKey: .descrizione).value
descrizione_en = try fieldContainer.decode(StringValue.self, forKey: .descrizione_en).value
data_consumption = try fieldContainer.decode(StringValue.self, forKey: .data_consumption).value
listaRefsLinea = [""] // <-- How to read this??
}
}
My issue is that I'm not being able to read the array inside the field "listaRefsLinea". Any idea on how to achieve that? Also I'm afraid that part of the troubles come from the fact that that's a Document Reference variable and as such does not conform to the Codable protocol.
Well. listaRefsLinea is a custom object just like your StringValue
So add these structs:
// MARK: - ListaRefsLinea
struct ListaRefsLinea: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let referenceValue: String
}
and in your custom init decode it to this struct, go down the tree until you get the array and map that to a [String]:
listaRefsLinea = try fieldContainer.decode(ListaRefsLinea.self, forKey: .listaRefsLinea)
.arrayValue.values.map{ $0.referenceValue }

JSON decoding double nested array in Swift

currently I'm trying to decode JSON with a nested Array. The nested array can have some random numbers of the object inside it. I try to decode it but turns out it return an errors
CodingKeys(stringValue: "itenaries", intValue: nil),
debugDescription : "Expected to decode Array<Any> but found a dictionary
Sample JSON data
{
"itenaries": {
"days":
[
[
{
"itenary_id":0,
"itenary_location_name":"Batu Caves Temple"
}
],
[
{
"itenary_id":0,
"itenary_location_name":"KL Tower "
},
{
"itenary_id":1,
"itenary_location_name":"KL Forest Eco Park"
}
]
]
}
}
My Struct
struct Itenaries : Codable {
let itenaries : [[Days]]
}
struct Days : Codable {
let itenary_id : Int
let itenary_location_name : String
}
Decoding Implementation
let decoder = JSONDecoder()
let itenary = try decoder.decode(Itenaries.self, from: fileData)
print(itenary.itenaries[0][0].itenary_id)
Where do you decode the days key? That's the problem. You need an intermediate struct
struct Root : Decodable {
let itenaries : Itenary
}
struct Itenary : Decodable {
let days : [[Days]]
}
...
let result = try decoder.decode(Root.self, from: fileData)
print(result.iternaries.days[0][0].itenary_id)
i'd probably do something like
struct Name:Codable {
var itenaries:itenaries
}
struct itenaries:Codable {
var days = [[Days]]
}
struct Days : Codable {
let itenary_id : Int
let itenary_location_name : String
}
so basically according the structure of your Json file
Root struct -> itenaries -> [[days]]
hope you understand :)
[Edited]
you can try these.
I'm getting correct result using this approach
Result
struct MainResponse : Codable {
let itenaries : Itenaries
}
struct Itenaries : Codable {
let days : [[Days]]
}
struct Days : Codable {
let itenary_id : Int
let itenary_location_name : String
}
if let path = Bundle.main.path(forResource: "nested_array", ofType: "json") {
do {
let responseData = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let decoder = JSONDecoder()
let mainResponse = try decoder.decode(MainResponse.self, from: responseData)
print(mainResponse.itenaries.days[0][0].itenary_id)
print(mainResponse.itenaries.days[0][0].itenary_location_name)
print(mainResponse.itenaries.days[1][0].itenary_id)
print(mainResponse.itenaries.days[1][0].itenary_location_name)
print(mainResponse.itenaries.days[1][1].itenary_id)
print(mainResponse.itenaries.days[1][1].itenary_location_name)
// output
// 0
// Batu Caves Temple
// 0
// KL Tower
// 1
// KL Forest Eco Park
} catch let error {
print(error.localizedDescription)
}
}
Your model is not correct, replace it by the following:
struct ItenariesResponse: Codable {
let itenaries: Itenaries
}
struct Itenaries: Codable {
let days: [[Day]]
}
struct Day: Codable {
let itenaryID: Int
let itenaryLocationName: String
enum CodingKeys: String, CodingKey {
case itenaryID = "itenary_id"
case itenaryLocationName = "itenary_location_name"
}
}
Then replace the type you decode like that:
let itenary = try decoder.decode(ItenariesResponse.self, from: fileData)

How to create a Model for dynamic keys , json parsing ,swift5

I am parsing a json data and trying to create a Model but can't figure out how to achieve the title and extract properties from the json data (which I have provided), as pageids property is dynamic. Please tell me how can I create Model to extract the title property from the page using id (stored in pageids property)
link for jsonData https://en.wikipedia.org/w/api.php?exintro=&titles=canterbury%20bells&indexpageids=&format=json&pithumbsize=500&explaintext=&redirects=1&action=query&prop=extracts%7Cpageimages
I tried little a bit, below is my code but I don't think that's correct
var ID = ""
struct Document:Codable {
let batchcomplete:String
let query:Query
}
struct Query:Codable {
let normalized:[Normalized]
let pages:Pages
var pageids:[String]{
didSet{
ID = oldValue[0]
}
}
}
struct Normalized:Codable {
let from:String
let to:String // it is a name of an flower
}
struct Pages:Codable {
let id:[Pages2]
enum CodingKeys:CodingKey {
case id = "\(ID)"
}
}
struct Pages2:Codable {
let title:String // this is an official name of flower
let extract:String // this is a body
let thumbnail:Thumbnail
}
struct Thumbnail:Codable {
let source:String //this is an url for photo
}
The model to map your JSON will be something like this:
struct Document: Codable {
let batchcomplete: String
let query: Query
}
struct Query: Codable {
let normalized: [Normalized]
var pageids: [String]
let pages: [String: Page]
}
struct Normalized: Codable {
let from: String
let to: String
}
struct Page: Codable {
let title: String
let extract: String
let thumbnail: Thumbnail
}
struct Thumbnail: Codable {
let source: String
}
and you have access to each page using pageids array and pages dictionary:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(Document.self, from: Data(jsonString.utf8))
decoded.query.pageids.forEach { id in
guard let page = decoded.query.pages[id] else { return }
print(page.title)
}
} catch {
print(error)
}
However I would prefer to make a small change to the model in order to make access to pages easier. That will require to customly implement the decoding of Query struct:
struct Query: Decodable {
let normalized: [Normalized]
let pages: [Page]
enum CodingKeys: String, CodingKey {
case normalized
case pageids
case pages
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
normalized = try container.decode([Normalized].self, forKey: .normalized)
let pageids = try container.decode([String].self, forKey: .pageids)
let pagesDict = try container.decode([String: Page].self, forKey: .pages)
pages = pageids.compactMap { pagesDict[$0] }
}
}
Then, access to each page would be as simple as a loop:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(Document.self, from: Data(jsonString.utf8))
decoded.query.pages.forEach { page in
print(page.title)
}
} catch {
print(error)
}

Json parsing Swift not getting print

I am new to Swift. I want to fetch some json data from the server using the url. I tried many other solutions but they didn't work. I want to print the duration key (text and value) from the array and then print it in console.
The Json data is attached below
{
"status": "OK",
"rows": [
{
"elements": [
{
"duration": {
"text": "3 hours 49 mins",
"value": 13725
},
"distance": {
"text": "225 mi",
"value": 361715
},
"status": "OK"
}
]
}
],
"origin_addresses": [
"Washington, DC, USA"
],
"destination_addresses": [
"New York, NY, USA"
]
}
Attached Code
func getdatajson1(){
if let url = URL(string: "http://www.json-generator.com/api/json/get/bQywstyfkO?indent=2") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let res = try JSONDecoder().decode(Root.self, from: data)
print(res.rows)
} catch let error {
print(error)
}
}
}.resume()
}
}
struct Root: Codable {
let rows: [Root2]
}
struct Root2: Codable {
let elements: [Root3]
}
struct Root3: Codable {
let elements: [node]
}
struct node: Codable {
let duration : [valuesarray]
}
struct valuesarray: Codable {
let text : String
let value : Int
}
The duration is an Object and not an Array, also change your names and you can use this:
struct Root: Decodable {
let rows: [Rows]
}
struct Rows: Decodable {
let elements: [Elements]
}
struct Elements: Decodable {
let duration, distance: LocationValues
}
struct LocationValues: Decodable {
let text: String
let value: Int
}
func getdatajson1(){
if let url = URL(string: "http://www.json-generator.com/api/json/get/bQywstyfkO?indent=2") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let res = try JSONDecoder().decode(Root.self, from: data)
if let row = res.rows.first, let elements = row.elements.first {
print(elements.duration.text) //This is how you can get the text value
print(elements.distance.text) //This will print the distance
}
} catch let error {
print(error)
}
}
}.resume()
}
}
Replace your codable struct with the below
class Result: Codable {
var status:String?
var rows:[Row]?
var origin_addresses:[String]?
var destination_addresses:[String]?
}
class Row: Codable {
var elements:[Element]?
}
class Element: Codable {
var status:String?
var duration:Duration?
var distance:Distance?
}
class Duration: Codable {
var text:String?
var value:Int?
}
class Distance: Codable {
var text:String?
var value:Int?
}
You should update your node model like below
struct node: Codable {
let duration : valuesarray
let distance : valuesarray
let status : String
}
And you can access your duration data from API response like below
if let rows = res.rows, rows.count > 0 {
//Access the element objects from rows
let arrElements = rows[0].elements, arrElements.count > 0 {
if let durationData = arrElements[0].duration { //get your duration object
print(durationData.text)
print(durationData.value)
}
}
}
try this:
and a hint: go to https://app.quicktype.io/ -> here you can paste your json and you will get your datastructs for free! ;)
func getdatajson1(){
if let url = URL(string: "http://www.json-generator.com/api/json/get/bQywstyfkO?indent=2") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let res = try JSONDecoder().decode(Welcome.self, from: data)
print(res.rows)
} catch let error {
print(error)
}
}
}.resume()
}
}
getdatajson1()
struct Welcome: Codable {
let status: String
let rows: [Row]
let originAddresses, destinationAddresses: [String]
enum CodingKeys: String, CodingKey {
case status, rows
case originAddresses = "origin_addresses"
case destinationAddresses = "destination_addresses"
}
}
// MARK: - Row
struct Row: Codable {
let elements: [Element]
}
// MARK: - Element
struct Element: Codable {
let duration, distance: Distance
let status: String
}
// MARK: - Distance
struct Distance: Codable {
let text: String
let value: Int
}

Save Struct to UserDefaults

I have a struct that I want to save to UserDefaults. Here's my struct
struct Song {
var title: String
var artist: String
}
var songs: [Song] = [
Song(title: "Title 1", artist "Artist 1"),
Song(title: "Title 2", artist "Artist 2"),
Song(title: "Title 3", artist "Artist 3"),
]
In another ViewController, I have a UIButton that appends to this struct like
#IBAction func likeButtonPressed(_ sender: Any) {
songs.append(Song(title: songs[thisSong].title, artist: songs[thisSong].artist))
}
I want it so that whenever the user clicks on that button also, it saves the struct to UserDefaults so that whenever the user quits the app and then opens it agian, it is saved. How would I do this?
In Swift 4 this is pretty much trivial. Make your struct codable simply by marking it as adopting the Codable protocol:
struct Song:Codable {
var title: String
var artist: String
}
Now let's start with some data:
var songs: [Song] = [
Song(title: "Title 1", artist: "Artist 1"),
Song(title: "Title 2", artist: "Artist 2"),
Song(title: "Title 3", artist: "Artist 3"),
]
Here's how to get that into UserDefaults:
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
And here's how to get it back out again later:
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
This is my UserDefaults extension in main thread, to set get Codable object into UserDefaults
// MARK: - UserDefaults extensions
public extension UserDefaults {
/// Set Codable object into UserDefaults
///
/// - Parameters:
/// - object: Codable Object
/// - forKey: Key string
/// - Throws: UserDefaults Error
public func set<T: Codable>(object: T, forKey: String) throws {
let jsonData = try JSONEncoder().encode(object)
set(jsonData, forKey: forKey)
}
/// Get Codable object into UserDefaults
///
/// - Parameters:
/// - object: Codable Object
/// - forKey: Key string
/// - Throws: UserDefaults Error
public func get<T: Codable>(objectType: T.Type, forKey: String) throws -> T? {
guard let result = value(forKey: forKey) as? Data else {
return nil
}
return try JSONDecoder().decode(objectType, from: result)
}
}
Update This is my UserDefaults extension in background, to set get Codable object into UserDefaults
// MARK: - JSONDecoder extensions
public extension JSONDecoder {
/// Decode an object, decoded from a JSON object.
///
/// - Parameter data: JSON object Data
/// - Returns: Decodable object
public func decode<T: Decodable>(from data: Data?) -> T? {
guard let data = data else {
return nil
}
return try? self.decode(T.self, from: data)
}
/// Decode an object in background thread, decoded from a JSON object.
///
/// - Parameters:
/// - data: JSON object Data
/// - onDecode: Decodable object
public func decodeInBackground<T: Decodable>(from data: Data?, onDecode: #escaping (T?) -> Void) {
DispatchQueue.global().async {
let decoded: T? = self.decode(from: data)
DispatchQueue.main.async {
onDecode(decoded)
}
}
}
}
// MARK: - JSONEncoder extensions
public extension JSONEncoder {
/// Encodable an object
///
/// - Parameter value: Encodable Object
/// - Returns: Data encode or nil
public func encode<T: Encodable>(from value: T?) -> Data? {
guard let value = value else {
return nil
}
return try? self.encode(value)
}
/// Encodable an object in background thread
///
/// - Parameters:
/// - encodableObject: Encodable Object
/// - onEncode: Data encode or nil
public func encodeInBackground<T: Encodable>(from encodableObject: T?, onEncode: #escaping (Data?) -> Void) {
DispatchQueue.global().async {
let encode = self.encode(from: encodableObject)
DispatchQueue.main.async {
onEncode(encode)
}
}
}
}
// MARK: - NSUserDefaults extensions
public extension UserDefaults {
/// Set Encodable object in UserDefaults
///
/// - Parameters:
/// - type: Encodable object type
/// - key: UserDefaults key
/// - Throws: An error if any value throws an error during encoding.
public func set<T: Encodable>(object type: T, for key: String, onEncode: #escaping (Bool) -> Void) throws {
JSONEncoder().encodeInBackground(from: type) { [weak self] (data) in
guard let data = data, let `self` = self else {
onEncode(false)
return
}
self.set(data, forKey: key)
onEncode(true)
}
}
/// Get Decodable object in UserDefaults
///
/// - Parameters:
/// - objectType: Decodable object type
/// - forKey: UserDefaults key
/// - onDecode: Codable object
public func get<T: Decodable>(object type: T.Type, for key: String, onDecode: #escaping (T?) -> Void) {
let data = value(forKey: key) as? Data
JSONDecoder().decodeInBackground(from: data, onDecode: onDecode)
}
}
If the struct contains only property list compliant properties I recommend to add a property propertyListRepresentation and a corresponding init method
struct Song {
var title: String
var artist: String
init(title : String, artist : String) {
self.title = title
self.artist = artist
}
init?(dictionary : [String:String]) {
guard let title = dictionary["title"],
let artist = dictionary["artist"] else { return nil }
self.init(title: title, artist: artist)
}
var propertyListRepresentation : [String:String] {
return ["title" : title, "artist" : artist]
}
}
To save an array of songs to UserDefaults write
let propertylistSongs = songs.map{ $0.propertyListRepresentation }
UserDefaults.standard.set(propertylistSongs, forKey: "songs")
To read the array
if let propertylistSongs = UserDefaults.standard.array(forKey: "songs") as? [[String:String]] {
songs = propertylistSongs.flatMap{ Song(dictionary: $0) }
}
If title and artist will never be mutated consider to declare the properties as constants (let) .
This answer was written while Swift 4 was in beta status. Meanwhile conforming to Codable is the better solution.
Here is a modern Swift 5.1 #propertyWrapper, allowing to store any Codable object in form of a human readable JSON string:
#propertyWrapper struct UserDefaultEncoded<T: Codable> {
let key: String
let defaultValue: T
init(key: String, default: T) {
self.key = key
defaultValue = `default`
}
var wrappedValue: T {
get {
guard let jsonString = UserDefaults.standard.string(forKey: key) else {
return defaultValue
}
guard let jsonData = jsonString.data(using: .utf8) else {
return defaultValue
}
guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else {
return defaultValue
}
return value
}
set {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let jsonData = try? encoder.encode(newValue) else { return }
let jsonString = String(bytes: jsonData, encoding: .utf8)
UserDefaults.standard.set(jsonString, forKey: key)
}
}
}
Usage:
extension Song: Codable {}
#UserDefaultEncoded(key: "songs", default: [])
var songs: [Song]
func addSong(_ song: Song) {
// This will automatically store new `songs` value
// to UserDefaults
songs.append(song)
}
From here:
A default object must be a property list—that is, an instance of (or for collections, a combination of instances of):
NSData
,
NSString
,
NSNumber
,
NSDate
,
NSArray
, or
NSDictionary
. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
You need to use NSKeydArchiver. Documentation can be found here and examples here and here.
If you are just trying to save this array of songs in UserDefaults and nothing fancy use this:-
//stores the array to defaults
UserDefaults.standard.setValue(value: songs, forKey: "yourKey")
//retrieving the array
UserDefaults.standard.object(forKey: "yourKey") as! [Song]
//Make sure to typecast this as an array of Song
If you are storing a heavy array, I suggest you to go with NSCoding protocol or the Codable Protocol in swift 4
Example of coding protocol:-
struct Song {
var title: String
var artist: String
}
class customClass: NSObject, NSCoding { //conform to nsobject and nscoding
var songs: [Song] = [
Song(title: "Title 1", artist "Artist 1"),
Song(title: "Title 2", artist "Artist 2"),
Song(title: "Title 3", artist "Artist 3"),
]
override init(arr: [Song])
self.songs = arr
}
required convenience init(coder aDecoder: NSCoder) {
//decoding your array
let songs = aDecoder.decodeObject(forKey: "yourKey") as! [Song]
self.init(are: songs)
}
func encode(with aCoder: NSCoder) {
//encoding
aCoder.encode(songs, forKey: "yourKey")
}
}
I'd imagine that it should be quite common to represent a user's settings as an observable object. So, here's an example of keeping observable data synchronised with user defaults and updated for xCode 11.4. This can be used in the context of environment objects also.
import SwiftUI
final class UserData: ObservableObject {
#Published var selectedAddress: String? {
willSet {
UserDefaults.standard.set(newValue, forKey: Keys.selectedAddressKey)
}
}
init() {
selectedAddress = UserDefaults.standard.string(forKey: Keys.selectedAddressKey)
}
private struct Keys {
static let selectedAddressKey = "SelectedAddress"
}
}
Swift 5
If you want need to save struct in UserDefault using only on data format.
Smaple struct
struct StudentData:Codable{
var id: Int?
var name: String?
var createdDate: String?
// for decode the value
init(from decoder: Decoder) throws {
let values = try? decoder.container(keyedBy: codingKeys.self)
id = try? values?.decodeIfPresent(Int.self, forKey: .id)
name = try? values?.decodeIfPresent(String.self, forKey: .name)
createdDate = try? values?.decodeIfPresent(String.self, forKey: .createdDate)
}
// for encode the value
func encode(to encoder: Encoder) throws {
var values = encoder.container(keyedBy: codingKeys.self)
try? values.encodeIfPresent(id, forKey: .id)
try? values.encodeIfPresent(name, forKey: .name)
try? values.encodeIfPresent(createdDate, forKey: .createdDate)
}
}
There are two types to convert as data
Codable (Encodable and Decodable).
PropertyListEncoder and PropertyListDecoder
First we using the Codable (Encodable and Decodable) to save the struct
Example for save value
let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
guard let data = try? JSONEncoder().encode(value) else {
fatalError("unable encode as data")
}
UserDefaults.standard.set(data, forKey: "Top_student_record")
Retrieve value
guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
// write your code as per your requirement
return
}
guard let value = try? JSONDecoder().decode(StudentData.self, from: data) else {
fatalError("unable to decode this data")
}
print(value)
Now we using the PropertyListEncoder and PropertyListDecoder to save the struct
Example for save value
let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
guard let data = try? PropertyListEncoder().encode(value) else {
fatalError("unable encode as data")
}
UserDefaults.standard.set(data, forKey: "Top_student_record")
Retrieve value
guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
// write your code as per your requirement
return
}
guard let value = try? PropertyListDecoder().decode(StudentData.self, from: data) else {
fatalError("unable to decode this data")
}
print(value)
In your convenience you can use the any type to save the struct in userDefault.
Here is a simpler solution
#propertyWrapper
struct CodableUserDefault<Value: Codable> {
let key: String
let defaultValue: Value
private let container: UserDefaults = .standard
var wrappedValue: Value {
get {
guard let data = container.data(forKey: key), let object = try? JSONDecoder().decode(Value.self, from: data) else {
return defaultValue
}
return object
}
set {
container.set(try? JSONEncoder().encode(newValue), forKey: key)
}
}
}
Usage
enum ACodableEnum: String, Codable {
case first
case second
}
class SomeController {
#CodableUserDefault<ACodableEnum>(key: "key", defaultValue: .first)
private var aCodableEnum: ACodableEnum
}

Resources