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)
}
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 have a struct that looks something like this:
internal class RemoteProfileModel: Decodable {
let userId: String
let company: String
let email: String
let firstName: String
let lastName: String
let department: String
let jobTitle: String
let pictureUri: URL?
let headerUri: URL?
let bio: String
let updatedDate: Date
}
I need to list out these properties in a UITableView. I also need to use different cell types for some of the properties.
I'm thinking perhaps I should convert this struct to a dictionary of key/value pairs, and use the key to determine the cell type.
Is this possible? Is there another way to achieve this? I am unsure if it possible to convert a struct to a dictionary so am not sure this is the best way?
to convert a class to a dictionary,
class RemoteProfileModel: Decodable {
let userId: String
let company: String
let email: String
let firstName: String
let lastName: String
let department: String
let jobTitle: String
let pictureUri: URL?
let headerUri: URL?
let bio: String
let updatedDate: Date
init() {
userId = "666"
company = "AAPL"
email = "hehe#163.com"
firstName = "user"
lastName = "test"
department = "guess"
jobTitle = "poor iOS"
pictureUri = URL(string: "wrong")
headerUri = URL(string: "none")
bio = "China"
updatedDate = Date()
}
func listPropertiesWithValues(reflect: Mirror? = nil) -> [String: Any]{
let mirror = reflect ?? Mirror(reflecting: self)
if mirror.superclassMirror != nil {
self.listPropertiesWithValues(reflect: mirror.superclassMirror)
}
var yourDict = [String: Any]()
for (index, attr) in mirror.children.enumerated() {
if let property_name = attr.label {
//You can represent the results however you want here!!!
print("\(index): \(property_name) = \(attr.value)")
yourDict[property_name] = attr.value
}
}
return yourDict
}
}
Call like this:
let profile = RemoteProfileModel()
profile.listPropertiesWithValues()
In Swift Debugging and Reflection,
A mirror describes the parts that make up a particular instance
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.
I want to put the information I get from the API into the corresponding Label inside, I use the Alamofire to get the API information and put the corresponding Label inside, but I found that my Label text has not been changed, would like to ask this happen What's the problem? Who can answer me for me? Thank you
Here is my Information class:
import Foundation
import Alamofire
class Information {
var account:String?
var date:String?
var name:String?
var sex:String?
var born:String?
var phoneNumber:String?
var email:String?
init(account:String,date:String,name:String,sex:String,born:String,phoneNumber:String,email:String) {
self.account = account
self.date = date
self.name = name
self.sex = sex
self.born = born
self.phoneNumber = phoneNumber
self.email = email
}
typealias DownlaodComplete = () -> ()
func downlaodInformation(completion:#escaping DownlaodComplete) {
Alamofire.request("http://163.18.22.78:81/api/Information/A1001a").responseJSON { response in
print(response)
if let json = response.result.value as? Dictionary<String,Any> {
guard let account = json["Account"] as? String ,let date = json["Date"] as? String , let name = json["Name"] as? String , let sex = json["Sex"] as? String , let born = json["Born"] as? String , let phoneNumber = json["PhoneNumber"] as? String , let email = json["Email"] as? String else {
return
}
self.account = account
self.date = date
self.name = name
self.sex = sex
self.born = born
self.phoneNumber = phoneNumber
self.email = email
completion()
}
}
}
}
And here is my ViewController:
var information:Information?
override func viewDidLoad() {
super.viewDidLoad()
if let currentInformation = information {
currentInformation.downlaodInformation {
self.accountLabel.text = currentInformation.account
self.dateLabel.text = currentInformation.date
self.nameLabel.text = currentInformation.name
self.sexLabel.text = currentInformation.sex
self.bornLabel.text = currentInformation.born
self.phoneNumberLabel.text = currentInformation.phoneNumber
self.emailLabel.text = currentInformation.email
}
}
You need to use your completion block which will be called whenever Alamofire has finished the data request. You can also improve your code a bit by for example have a onCompletion block that passes an Information object and an onError block to display if you have any errors. Example below:
func downlaodInformation(parameterOne: String, parameterTwo: Int, onCompletion: #escaping (Information) -> Void, onError: #escaping(NSError) -> Void) {
Alamofire.request("http://163.18.22.78:81/api/Information/A1001a").responseJSON { response in
if let json = response.result.value as? Dictionary<String,Any> {
let account = json["Account"] as? String
let date = json["Date"] as? String
let name = json["Name"] as? String
let sex = json["Sex"] as? String
let born = json["Born"] as? String
let phoneNumber = json["PhoneNumber"] as? String
let email = json["Email"] as? String
let information = Information(account: account, date: date, name: name, sex: sex, born: born, phoneNumber: phoneNumber, email: email)
onCompletion(information)
} else {
onError(NSError(domain: "Error while getting data", code: 0, userInfo: nil))
}
}
}
Usage:
downlaodInformation(parameterOne: "someParam", parameterTwo: 123, onCompletion: { (currentInformation) in
print(currentInformation.account)
self.accountLabel.text = currentInformation.account
self.dateLabel.text = currentInformation.date
self.nameLabel.text = currentInformation.name
self.sexLabel.text = currentInformation.sex
self.bornLabel.text = currentInformation.born
self.phoneNumberLabel.text = currentInformation.phoneNumber
self.emailLabel.text = currentInformation.email
}) { (error) in
print(error.domain)
}
Here you declare information to be an Information optional
var information:Information?
But you don't give it an initial value, meaning that it is nil
In your viewDidLoad you do the right thing and check whether information has a value:
if let currentInformation = information
But I'm guessing it hasn't, because you haven't created an instance of it. Therefore you don't end up inside your if let loop and never calls downlaodInformation
So you need to create a new instance of Information before you can use it.
However
This leads to a problem with your Information class.
If I was to instantiate an Information object, I'd need to have:
account
date
name
sex
born
phoneNumber
email
Or..since you've created them as optionals, pass nil.
But that is not what you want, is it?
I'm guessing you'd like to do something along the lines of this in your ViewController:
let information = Information()
and then in viewDidLoad
information.downloadInformation( currrentInformation in
self.accountLabel.text = currentInformation.account
....
}
To do so you could change your Information to not take parameters to its constructor and then create another struct which would hold your data.
Something like:
struct Information {
var account:String?
var date:String?
var name:String?
var sex:String?
var born:String?
var phoneNumber:String?
var email:String?
}
class InformationLoader {
func downloadInformation(completion: (Information?) -> ()) {
Alamofire.request("http://163.18.22.78:81/api/Information/A1001a").responseJSON{ response in
print(response)
if let json = response.result.value as? Dictionary<String,Any> {
guard let account = json["Account"] as? String,
let date = json["Date"] as? String,
let name = json["Name"] as? String,
let sex = json["Sex"] as? String,
let born = json["Born"] as? String,
let phoneNumber = json["PhoneNumber"] as? String,
let email = json["Email"] as? String else {
completion(nil)
return
}
let information = Information(account: account, date: date, name: name, sex: sex, born: born, phoneNumber: phoneNumber, email: email)
completion(information)
}
}
}
And you'd need to change your code in the ViewController to:
let informationLoader:InformationLoader()
In viewDidLoad
informationLoader.downloadInformation{ currentInformation in
if let currentInformation = currentInformation {
//populate your textfields
self.accountLabel.text = currentInformation.account
....
}
}
Hope that makes sense and helps you.
Your code has a lot of mistakes, so here is a working variant. Better to call an updateUI or something like that from the closure. I hope this will help:
ViewController.swift:
class ViewController: UIViewController
{
#IBOutlet weak var accountLabel: UILabel!
#IBOutlet weak var dateLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
var information: Information?
override func viewDidLoad()
{
super.viewDidLoad()
information = Information.init(account: "aaaa", date: "dddd", name: "nnnn", sex: "ssss", born: "bbbb", phoneNumber: "pppp", email: "eeee")
information?.downlaodInformation(completion:
{
self.updateUI()
})
}
func updateUI()
{
print("called")
self.accountLabel.text = information?.account
self.dateLabel.text = information?.date
self.nameLabel.text = information?.name
/*self.sexLabel.text = currentInformation.sex
self.bornLabel.text = currentInformation.born
self.phoneNumberLabel.text = currentInformation.phoneNumber
self.emailLabel.text = currentInformation.email*/
}
}
Information.swift:
class Information
{
var account:String?
var date:String?
var name:String?
var sex:String?
var born:String?
var phoneNumber:String?
var email:String?
typealias DownlaodComplete = () -> ()
init(account:String,date:String,name:String,sex:String,born:String,phoneNumber:String,email:String) {
self.account = account
self.date = date
self.name = name
self.sex = sex
self.born = born
self.phoneNumber = phoneNumber
self.email = email
}
func downlaodInformation(completion:#escaping DownlaodComplete) {
Alamofire.request("http://163.18.22.78:81/api/Information/A1001a").responseJSON { response in
print(response)
completion()
if let json = response.result.value as? Dictionary<String,Any>
{
print("Dictionary done")
guard
let account = json["Account"] as? String,
let date = json["Date"] as? String ,
let name = json["Name"] as? String else
{
print("Parse error!")
return
}
self.account = account
self.date = date
self.name = name
/*self.sex = sex
self.born = born
self.phoneNumber = phoneNumber
self.email = email*/
completion()
}
}
}
}
Tested, and got the following response:
SUCCESS: {
Account = A1001a;
Born = 841031;
CarParking = "";
Date = "0001/1/1 \U4e0a\U5348 12:00:00";
Email = "zxc#gmail.com";
Name = Ray;
Phone = 09361811111;
Sex = "\U7537"; } called Dictionary done called
I have the following struct definition:
struct ThreadManager: Equatable {
let fid: Int
let date: NSDate
let forumName: String
let typeid: Int
var page: Int
var threadList: [Thread]
var totalPageNumber: Int?
}
and the thread is :
struct Thread: Equatable {
let author: Author
let replyCount: Int
let readCount: Int
let title: String
let tid: Int
let isTopThread: Bool
var attributedStringDictionary: [String: NSAttributedString]
var postDescripiontTimeString: String
var hasRead: Bool
}
How can I encode a ThreadManager variable to NSData? I tried to used the following functions, but it does not worK.
func encode<T>(var value: T) -> NSData {
return withUnsafePointer(&value) { p in
NSData(bytes: p, length: sizeofValue(value))
}
}
func decode<T>(data: NSData) -> T {
let pointer = UnsafeMutablePointer<T>.alloc(sizeof(T))
data.getBytes(pointer, length: sizeof(T))
return pointer.move()
}
I have ThreadManager items, and I want to store them into sqlite. So I need to convert them to NSData. I have a variable called threadManager, the number of items in its threadList is about 70. I run the code and set a breakpoint, and input encode(threadManager) in xcode console, it is only 73bytes. It is wrong. How can I encode and decode those struct to NSData.
If your database is to be read on any other platform (Android, the web, wherever), you'd better choosing a cross-platform format such as JSON, or spread your struct members in their dedicated columns in a database table.
If you only target iOS/OSX/tvOS/etc, I recommend NSCoder. It is efficient, and most importantly:
NSCoder is platform-independant, which means that your NSData coding and decoding is not dependent on the particular memory layout currently used by the platform. For example, you don't have to fear 32 / 64 bits compatibility.
NSCoder lets you change your type over time, while keeping the ability to import old versions of your struct.
The code below adds a asData() function to your struct, and an init(data:) initializer. Those two let you go back and forth from your struct to NSData.
import Foundation
struct MyStruct {
let name: String
let date: NSDate
}
extension MyStruct {
init(data: NSData) {
let coding = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! Coding
name = coding.name as String
date = coding.date
}
func asData() -> NSData {
return NSKeyedArchiver.archivedDataWithRootObject(Coding(self))
}
class Coding: NSObject, NSCoding {
let name: NSString
let date: NSDate
init(_ myStruct: MyStruct) {
name = myStruct.name
date = myStruct.date
}
required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObjectForKey("name") as! NSString
self.date = aDecoder.decodeObjectForKey("date") as! NSDate
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "name")
aCoder.encodeObject(date, forKey: "date")
}
}
}
let encodedS = MyStruct(name: "foo", date: NSDate())
let data = encodedS.asData()
let decodedS = MyStruct(data: data)
print(decodedS.name)
print(decodedS.date)
#Gwendal Roué : you are right, but I have to build another class according to each struct. I used the following method, it is ugly, but it works. Can you help me to improve it?
init(data: NSData) {
let dictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! NSDictionary
fid = (dictionary["fid"] as! NSNumber).integerValue
date = dictionary["date"] as! NSDate
forumName = dictionary["forumName"] as! String
typeid = (dictionary["typeid"] as! NSNumber).integerValue
page = (dictionary["page"] as! NSNumber).integerValue
totalPageNumber = (dictionary["totalPageNumber"] as? NSNumber)?.integerValue
let threadDataList = dictionary["threadDataList"] as! [NSData]
threadList = threadDataList.map { Thread(data: $0) }
}
extension ThreadManager {
func encode() -> NSData {
let dictionary = NSMutableDictionary()
dictionary.setObject(NSNumber(integer: fid), forKey: "fid")
dictionary.setObject(date, forKey: "date")
dictionary.setObject(forumName, forKey: "forumName")
dictionary.setObject(NSNumber(integer: typeid), forKey: "typeid")
dictionary.setObject(NSNumber(integer: page), forKey: "page")
if totalPageNumber != nil {
dictionary.setObject(NSNumber(integer: totalPageNumber!), forKey: "totalPageNumber")
}
let threadDataList: [NSData] = threadList.map { $0.encode() }
dictionary.setObject(threadDataList, forKey: "threadDataList")
return NSKeyedArchiver.archivedDataWithRootObject(dictionary)
}
}