Calling function in ViewController but parameters seems wrong - ios

I created a custom class to save a array of Products in NSUserDefaults, but when I will test in my ViewController, I always get this using the autocomplete from Xcode 7.1
The function saveToFile or loadFromFile always shows self: DataFile as unique parameter.
This is my DataFile class
import Foundation
class DataFile {
let userDefaults = NSUserDefaults.standardUserDefaults()
func saveToFile<T>(object: [T], key: String) -> String {
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(object as! AnyObject)
self.userDefaults.setObject(encodedData, forKey: key)
self.userDefaults.synchronize()
return "oi"
}
func loadFromFile<T>(key: String) -> [T]? {
let decoded = self.userDefaults.objectForKey(key) as! NSData
if let decodedProducts = NSKeyedUnarchiver.unarchiveObjectWithData(decoded) as? [T] {
return decodedProducts
}
return nil
}
}
What am I doing wrong?
Thank you

Your funcs are not static function, so, you must create object to use these functions.
let xxx = DataFile()
xxx.loadFromFile(<#T##key: String##String#>)

Related

Encoding generic String object gives nil Swift

I have a UserDefaults class that handles storing, deleting, and fetching of stored objects to defaults. Here's the complete class, neat and simple, I believe:
Now the problem lies in storing function. I couldn't seem to encode an Encodable String object. I know I could just store that object to the defaults, but that would defeat the purpose of this MVDefaults that deals with generic objects.
Anything I'm missing here?
import Foundation
enum MVDefaultsKey: String {
case requestToken = "defaultsRequestToken"
}
/// The class that has multiple class functions for handling defaults.
/// Also has the helper class functions for handling auth tokens.
class MVDefaults {
// MARK: - Functions
/// Stores token.
class func store<T: Encodable>(_ object: T, key: MVDefaultsKey) {
let encoder = JSONEncoder()
let encoded = try? encoder.encode(object)
UserDefaults.standard.set(encoded, forKey: key.rawValue)
}
/// Removes the stored token
class func removeDefaultsWithKey(_ key: MVDefaultsKey) {
UserDefaults.standard.removeObject(forKey: key.rawValue)
}
/// Returns stored token (optional) if any.
class func getObjectWithKey<T: Decodable>(_ key: MVDefaultsKey, type: T.Type) -> T? {
guard let savedData = UserDefaults.standard.data(forKey: key.rawValue) else {
return nil
}
let object = try? JSONDecoder().decode(type, from: savedData)
return object
}
}
Think about what would the string "hello" encoded to JSON, look like. It would just look like:
"hello"
wouldn't it?
That is not a valid JSON (according to here)! You can't encode a string directly to JSON, and you can't decode a string directly either.
For example, this code
let string = try! JSONDecoder().decode(String.self, from: "\"hello\"".data(using: .utf8)!)
will produce the error
JSON text did not start with array or object and option to allow fragments not set.
And
let data = try! JSONEncoder().encode("Hello")
will produce the error:
Top-level String encoded as string JSON fragment.
The work around here is just to store your strings with the dedicated set methods provided by UserDefaults. You can still have your generic method, though, you just need to check the type and cast:
if let str = object as? String {
UserDefaults.standard.set(str, forKey: key)
} else if let int = object as? Int {
...
Whilst Sweeper's comment is helpful and should be the answer (since I won't be able to come up to my own answer without his), I'll still go ahead and share what I did to my Defaults class, to make it handle the non JSON encoding objects (e.g. String, Int, Array, and whatnot).
Here it is:
MVDefaults.swift
import Foundation
enum MVDefaultsKey: String {
case requestToken = "defaultsRequestToken"
case someOtherKey = "defaultsKeyForTests"
}
/// The class that has multiple class functions for handling defaults.
/// Also has the helper class functions for handling auth tokens.
class MVDefaults {
// MARK: - Functions
/// Stores object.
class func store<T: Encodable>(_ object: T, key: MVDefaultsKey) {
let encoder = JSONEncoder()
let encoded = try? encoder.encode(object)
if encoded == nil {
UserDefaults.standard.set(object, forKey: key.rawValue)
return
}
UserDefaults.standard.set(encoded, forKey: key.rawValue)
}
/// Removes the stored object
class func removeDefaultsWithKey(_ key: MVDefaultsKey) {
UserDefaults.standard.removeObject(forKey: key.rawValue)
}
/// Returns stored object (optional) if any.
class func getObjectWithKey<T: Decodable>(_ key: MVDefaultsKey, type: T.Type) -> T? {
if let savedData = UserDefaults.standard.data(forKey: key.rawValue) {
let object = try? JSONDecoder().decode(type, from: savedData)
return object
}
return UserDefaults.standard.object(forKey: key.rawValue) as? T
}
}
And here's the test (spec) that I wrote for testing the Defaults methods. All passed! ✅
MVDefaultsSpec.swift
#testable import Movieee
import Foundation
import Quick
import Nimble
class MVDefaultsSpec: QuickSpec {
override func spec() {
describe("Tests for MVDefaults") {
context("Tests the whole MVDefaults.") {
it("tests the store and retrieve function for any Codable object") {
let data = stubbedResponse("MovieResult")
expect(data).notTo(beNil())
let newMovieResult = try? JSONDecoder().decode(MovieResult.self, from: data!)
MVDefaults.store(newMovieResult, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: MovieResult.self)
// Assert
expect(retrievedObject).notTo(beNil())
// Assert
expect(retrievedObject?.movies?.first?.id).to(equal(newMovieResult?.movies?.first?.id))
}
it("tests the store and retrieve function for String") {
let string = "Ich bin ein Berliner"
MVDefaults.store(string, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: String.self)
// Assert
expect(retrievedObject).notTo(beNil())
// Assert
expect(retrievedObject).to(equal(string))
}
it("tests the removal function") {
MVDefaults.removeDefaultsWithKey(.someOtherKey)
let anyRetrievedObject = UserDefaults.standard.object(forKey: MVDefaultsKey.someOtherKey.rawValue)
let stringRetrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: String.self)
// Assert
expect(anyRetrievedObject).to(beNil())
// Assert
expect(stringRetrievedObject).to(beNil())
}
it("tests the store and retrieve function for Bool") {
let someBool: Bool = true
MVDefaults.store(someBool, key: .someOtherKey)
let retrievedObject = MVDefaults.getObjectWithKey(.someOtherKey, type: Bool.self)
// Assert
expect(retrievedObject).to(equal(someBool))
// Assert
expect(retrievedObject).notTo(beNil())
}
}
}
}
}

use NSUserDefaults with array of struct type

I have problems to figure out how to save my string of type "RiskEntry" with NSUserDefaults. I already went through some other posts, but somehow I still did not manage to solve this particular issue.
Let me explain what the code from below does right now: I get some data from my class CustomCell in the following code snippet. Here I first check with an "identifier" which array to update with the new array value "consequences".
It all works fine and the updated array is stored in riskEntry.
However, I cannot work out how to store this with NSUserDefaults now. When I try it with e.g. riskItemDefaults.set(riskEntry, forKey: "riskItem") I get an exception error.
Any idea what I am doing wrong here?
SWIFT3 (I removed all code not relevant for this question)
class: RiskPlan: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellUpdaterDelegate {
var riskEntry = [RiskEntry]()
var riskItemDefaults = UserDefaults.standard
// ------ start of delegate function (receiving from CustomCell) ---------
func transferData(consequencesTranferred: String, identifier: String) {
if let index = riskEntry.index(where: {$0.title as String == identifier}) {
riskEntry[index].consequences = consequencesTranferred
} else {
print ("nothing")
}
// save with NSUserDefaults
riskItemDefaults.set(riskEntry, forKey: "riskItem")
}
}
This is my struct:
public struct RiskEntry {
let title: String
var consequences: String
}
my Custom Cell
// ---------------- delegate to transfer entered data to VC -----------------
protocol CustomCellUpdaterDelegate {
func transferData(consequencesTranferred: String, identifier: String)
}
// ---------------- start of class CellCustomized -----------------
class CustomCell: UITableViewCell, UIPickerViewDataSource, UIPickerViewDelegate, UITextViewDelegate {
var delegate: CustomCellUpdaterDelegate?
// text fields, text views and picker views
#IBOutlet weak var riskTitle: UITextView!
#IBOutlet weak var consequences: UITextView!
// ---------------- listener for text view to save input in string when editing is finished -----------------
func textViewDidEndEditing(_ textView: UITextView) {
if textView.tag == 1 {
textConsequences = consequences.text
nameIdentifier = riskTitle.text
delegate?.transferData(consequencesTranferred: self.textConsequences, identifier: nameIdentifier)
} else {
print ("nothing")
}
}
}
The problem is you can't save your custom array in NSUserDefaults. To do that you should change them to NSData then save it in NSUserDefaults
Here is the code I used in my project it's in swift 2 syntax and I don't think it's going be hard to convert it to swift 3
let data = NSKeyedArchiver.archivedDataWithRootObject(yourObject);
NSUserDefaults.standardUserDefaults().setObject(data, forKey: "yourKey")
NSUserDefaults.standardUserDefaults().synchronize()
and to the get part use this combination
if let data = NSUserDefaults.standardUserDefaults().objectForKey("yourKey") as? NSData {
let myItem = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? yourType
}
hope this will help
Saving objects in UserDefaults have very specific restrictions:
set(_:forKey:) reference:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
You need to serialize your model, either using NSCoding or as an alternative using JSON, to map to a supported value by UserDefaults.
The closest type to a Swift struct that UserDefaults supports might be an NSDictionary. You could copy the struct elements into an Objective C NSDictionary object before saving the data.
I was able to program a solution based on #ahruss (How to save an array of custom struct to NSUserDefault with swift?). However, I modified it for swift 3 and it also shows how to implement this solution in a UITableView. I hope it can help someone in the future:
Add the extension from below to your structure (adjust it to your own variables)
Save the required array item like this:
let encoded = riskEntry.map { $0.encode() }
riskItemDefaults.set(encoded, forKey: "consequences")
riskItemDefaults.synchronize()
Load your item like this
let dataArray = riskItemDefaults.object(forKey: "consequences") as! [NSData]
let savedFoo = dataArray.map { RiskEntry(data: $0)! }
If you'd like to show the saved array item in your cells, proceed this way:
cell.consequences.text = savedFoo[indexPath.row].consequences as String
Here is the complete code, modified for Swift3
structure
// ---------------- structure for table row content -----------------
struct RiskEntry {
let title: String
var consequences: String
}
extension
extension RiskEntry {
init?(data: NSData) {
if let coding = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as? Encoding {
title = coding.title as String
consequences = (coding.consequences as String?)!
} else {
return nil
}
}
func encode() -> NSData {
return NSKeyedArchiver.archivedData(withRootObject: Encoding(self)) as NSData
}
private class Encoding: NSObject, NSCoding {
let title : NSString
let consequences : NSString?
init(_ RiskEntry: RiskEntry) {
title = RiskEntry.title as NSString
consequences = RiskEntry.consequences as NSString?
}
public required init?(coder aDecoder: NSCoder) {
if let title = aDecoder.decodeObject(forKey: "title") as? NSString {
self.title = title
} else {
return nil
}
consequences = aDecoder.decodeObject(forKey: "consequences") as? NSString
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(title, forKey: "title")
aCoder.encode(consequences, forKey: "consequences")
}
}
}

Saving custom NSObject in NSUserDefaults

I'm having a modal entity file as below,
import UIKit
class MyProfile: NSObject {
var userName : String = ""
func initWithDict(dict: NSMutableDictionary) {
self.userName = dict.objectForKey("username") as! String
}
}
Saving that entity by encoding as below,
let myDict: NSMutableDictionary = ["username": "abc"]
let myEntity:MyProfile = MyProfile()
myEntity.initWithDict(myDict)
let userDefaults = NSUserDefaults.standardUserDefaults()
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(myEntity)
userDefaults.setObject(encodedData, forKey: "MyProfileEntity")
userDefaults.synchronize()
Getting that saved entity as below,
let myEntity:MyProfile = MyProfile()
let userDefaults = NSUserDefaults.standardUserDefaults()
guard let decodedNSData = userDefaults.objectForKey("MyProfileEntity") as? NSData,
myEntity = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData) as? MyProfile!
else {
print("Failed")
return
}
print(myEntity.userName)
It's not working, having crashes and lot of syntax errors, I'm new to swift,
It's showing some syntax errors like definition conflicts with previous value in the unarchiveObjectWithData line. If I fix that error, then at the time of getting the entity from userdefaults it's crashing.
can anyone suggest how can I resolve it?
To save custom object into user default, you must implement NSCoding protocol. Please replace your custom data model like this:
class MyProfile: NSObject,NSCoding {
var userName : String = ""
#objc required init(coder aDecoder:NSCoder){
self.userName = aDecoder.decodeObjectForKey("USER_NAME") as! String
}
#objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.userName, forKey: "USER_NAME")
}
init(dict: [String: String]) {
self.userName = dict["username"]!
}
override init() {
super.init()
}
}
Here is the code for saving and retrieving MyProfile object:
// Save profile
func saveProfile(profile: MyProfile){
let filename = NSHomeDirectory().stringByAppendingString("/Documents/profile.bin")
let data = NSKeyedArchiver.archivedDataWithRootObject(profile)
data.writeToFile(filename, atomically: true)
}
// Get profile
func getProfile() -> MyProfile?{
if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/profile.bin")){
let unarchiveProfile = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MyProfile
return unarchiveProfile
} else{
return nil
}
}
Now here is the code snippet how to use those method:
// Create profile object
let profile = MyProfile(dict: ["username" : "MOHAMMAD"])
// save profile
saveProfile(profile)
// retrieve profile
if let myProfile = getProfile(){
print(myProfile.userName)
}else{
print("Profile not found")
}
You can't do this:
let myEntity:MyProfile = MyProfile()
Then later on, do this:
myEntity = ...
When something is defined with 'let', you cannot change it.
Change to
var myEntity: MyProfile?
It is possible that
NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData)
is returning nil. You then proceed to force unwrapping by adding
as? MyProfile!
try changing this to
as? MyProfile
Then later, see if you got something back
if let myEntity = myEntity {
print(myEntity.userName)
}

NsMutable is not subtype of [anyobject] in swift

I want to get the array from NSUserdefault and store it into NsMutableArray but unfortunately it fails.
error
NsMutable is not subtype of [anyobject] in swift
code
arrayImage = NSUserDefaults.standardUserDefaults().arrayForKey("ImageArray") as NSMutableArray
I am also trying to do downcast NSArray->NSString like in this post.
The NSUserDefaults arrayForKey method returns an optional array of AnyObject. It you want it to be mutable just declare it using "var".// example:
var mutableArray = NSUserDefaults.standardUserDefaults().arrayForKey("ImageArray")
If you want it to be immutable you have to declare it using "let".
let imutableArray = NSUserDefaults.standardUserDefaults().arrayForKey("ImageArray")
If you want to make sure your method does not return nil before setting any value to userdefaults you can use the nil coalescing operator "??" to return an empty array as follow:
var mutableArray = NSUserDefaults.standardUserDefaults().arrayForKey("ImageArray") ?? []
If you want to store images using user defaults you have to first convert your image to NSData. So you should create a method class to help you with it:
class Save {
class func image(key:String, _ value:UIImage) {
NSUserDefaults.standardUserDefaults().setObject(UIImagePNGRepresentation(value), forKey: key)
}
class func imageArray(key:String, _ value:[UIImage]) {
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(value), forKey: key)
}
}
class Load {
class func image(key:String) -> UIImage! {
return UIImage(data: ( NSUserDefaults.standardUserDefaults().objectForKey(key) as NSData))!
}
class func imageArray(key:String) -> [UIImage]! {
if let myLoadedData = NSUserDefaults.standardUserDefaults().dataForKey(key) {
return (NSKeyedUnarchiver.unarchiveObjectWithData(myLoadedData) as? [UIImage]) ?? []
}
return []
}
}
Testing
let myProfilePicture1 = UIImage(data: NSData(contentsOfURL: NSURL(string: "http://i.stack.imgur.com/Xs4RX.jpg")!)!)!
let myProfilePicture2 = UIImage(data: NSData(contentsOfURL: NSURL(string: "https://lh6.googleusercontent.com/-Axxzrunya9Y/AAAAAAAAAAI/AAAAAAAAAHo/gsDxk5FlliE/photo.jpg")!)!)!
Save.image("myPic", myProfilePicture1)
let myLoadedPic = Load.image("myPic")
let myImageArray = [myProfilePicture1,myProfilePicture2]
Save.imageArray("myPicArray", myImageArray)
let myLoadedPicArray = Load.imageArray("myPicArray")
Apple doc says:
You can also create an NSArray object directly from a Swift array literal, following the same bridging rules outlined above. When you explicitly type a constant or variable as an NSArray object and assign it an array literal, Swift creates an NSArray object instead of a Swift array.
A brief example here:
let imageArray: [AnyObject]? = NSUserDefaults.standardUserDefaults().arrayForKey("ImageArray")
if let array = imageArray{
var arrayImage: NSArray = array
}

Save struct in class to NSUserDefaults using Swift

I have a class and inside the class is a (swift) array, based on a global struct.
I want to save an array with this class to NSUserDefaults. This is my code:
struct mystruct {
var start : NSDate = NSDate()
var stop : NSDate = NSDate()
}
class MyClass : NSObject {
var mystructs : [mystruct]
init(mystructs : [mystruct]) {
self.mystructs = mystructs
super.init()
}
func encodeWithCoder(encoder: NSCoder) {
//let val = mystructs.map { $0 as NSObject } //this also doesn't work
let objctvtmrec = NSMutableArray(mystructs) //gives error
encoder.encodeObject(objctvtmrec)
//first approach:
encoder.encodeObject(mystructs) //error: [mystructs] doesn't conform to protocol 'anyobject'
}
}
var records : [MyClass] {
get {
var returnValue : [MyClass]? = NSUserDefaults.standardUserDefaults().objectForKey("records") as? [MyClass]
if returnValue == nil
{
returnValue = []
}
return returnValue!
}
set (newValue) {
let val = newValue.map { $0 as AnyObject }
NSUserDefaults.standardUserDefaults().setObject(val, forKey: "records")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
I already subclassed to NSObject, and I know I need NSCoding. But I don't find any way to convert the struct array to an NSMuteableArray or something similar I can store. The only idea until now is to go through each entry and copy it directly to a new array or to use much or objective-c code all over the project, so i never need to convert from swift arrays to objective-c arrays. Both are things I don't want to do.
Swift structs are not classes, therefore they don't conform to AnyObject protocol. You have to rethink your approach. Here are some suggestions:
Convert your struct to final class to enforce immutability
final class MyStruct {
let start : NSDate = NSDate()
let stop : NSDate = NSDate()
}
encoder.encodeObject(mystructs)
Map them as an array dictionaries of type [String: NSDate]
let structDicts = mystructs.map { ["start": $0.start, "stop": $0.stop] }
encoder.encodeObject(structDicts)
NSUserDefaults is limited in the types it can handle: NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary, and Bool. Thus no Swift objects or structs can be saved. Anything else must be converted to an NSData object.
NSUserDefaults does not work the same way as NSArchiver. Since you already have added NSCoder to your classes your best choice might be to save and restore with NSArchiver to a file in the Documents directory..
From the Apple NSUserDefaults Docs:
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.
I've developed a small library which may help. You can use it as a replacement of NSCoding for Swift structs.
You would need to implement a Koting protocol for mystruct:
struct mystruct: Koting {
var start : NSDate = NSDate()
var stop : NSDate = NSDate()
// MARK: - Koting
init?(koter: Koter) {
guard let start: NSDate = koter.dekotObject(forKey: "start"),
let stop: NSDate = koter.dekotObject(forKey: "stop") else {
return nil
}
self.init(start: start, stop: stop)
}
func enkot(with koter: Koter) {
koter.enkotObject(start, forKey: "start")
koter.enkotObject(stop, forKey: "stop")
}
}
Since then you may easily convert the struct to Data and back:
let str = mystruct(start: NSDate(/*...*/), stop: NSDate(/*...*/))
guard let data = str.de_data else { return } // potentially may be nil
let restoredStr = mystruct.de_from(data: data) // if data is irrelevant, returns `nil`
Finally, this is what you do to implement NSCoding:
class MyClass: NSObject, NSCoding {
var mystructs: [mystruct]
init(mystructs: [mystruct]) {
self.mystructs = mystructs
super.init()
}
func encode(with aCoder: NSCoder) {
guard let datas = mystructs.flatMap { $0.de_data } else { return }
aCoder.encode(datas, forKey: "mystructs")
}
required convenience init?(coder aDecoder: NSCoder) {
guard let datas = aDecoder.decodeObject(forKey: "mystructs") as? [Data],
let mystructs = datas.flatMap { mystruct.de_from(data: $0) } else {
return nil
}
self.init(mystructs : mystructs)
}
}
It's pretty much the same code you would write if NSCoding supported Swift structs.
I use this this in my project while coding with Swift 4:
let jsonData = """ {"variable1":1234,"variable2":"someString"}"""
struct MyStruct:Codable{
var variable1 :Int
var variable2:String
}
let whatever = try JSONDecoder().decode(MyStruct.self,from:jsonData)
let encoded =try JSONEncoder().encode(whatever)
UserDefaults.standart.set(encoded, forKey:"encodedData")
to fetch data from UserDefaults
if let data = UserDefaults.standard.object(forKey: "encodedData") as? Data {
let myStruct = try JSONDecoder().decode(MyStruct.self, from: data)
print(myStruct)
}

Resources