iOS - Mocking UserDefault before loading the view controller - ios

I am currently working on test for my application and I have faced a problem when mocking user defaults. Let me first show you my setup :
this is how I mock user Defaults :
class MockUserDefaults: UserDefaults {
typealias FakeData = Dictionary<String, Any?>
var data: FakeData
convenience init() {
self.init(suiteName: "mocking")!
}
override init?(suiteName suitename: String?) {
data = FakeDefaults()
UserDefaults().removePersistentDomain(forName: suitename!)
super.init(suiteName: suitename)
}
override func object(forKey defaultName: String) -> Any? {
if let data = data[defaultName] {
return data
}
return nil
}
override func set(_ value: Any?, forKey defaultName: String) {
if defaultName == "favs"{
data[defaultName] = value
}
}
}
I have a variable in my view controller called : userDefaults, and I set it like this :
var userDefaults : UserDefaults {
if (NSClassFromString("XCTest") != nil) {
return MockUserDefaults()
}
return UserDefaults.standard
}
this variable is actually an extension to a protocol which a made uiviewcontroller conform to it to make sure all my view controllers have this variable.
I also have a variable in myViewcontroller called favoriteMovie which I set like this :
private var favoriteMovie: Favorite? {
if let favoriteString = userDefaults.value(forKey: "favs") as? String {
return favorites.first(where: {$0.name == favoriteString})
}
return nil
}
now here's where the problem is, when I go and try to test this view controller , I need to set userDefault with an object for example :
myviewController.userDefaults.set("avengers", forKey: "favs")
before the test runs, but the problem is that favoriteMovie variable always return nil and I need it to return an object before the test runs . Any help. Thanks in advance.
UPDATE :
this is the protocol :
protocol Mockable: class {
var userDefaults: UserDefaults { get }
}
this is the extension :
extension UIViewController: Mockable {}
extension Mockable {
var userDefaults : UserDefaults {
if (NSClassFromString("XCTest") != nil) {
return MockUserDefaults()
}
return UserDefaults.standard
}
}

Here are two ways to fix it.
1) By doing some DI. In you viewController declare userDefaults as non-computed property as below
var userDefaults : UserDefaults?
In your test case, create MockUserDefaults object, set values and assign it to viewController when you are initiating it as below,
let mockUD = MockUserDefaults()
mockUD.set("avengers", forKey: "favs")
myviewController.userDefaults = mockUD
Now you will get the avengers object.
2) As the question is updated, here is the fix to hold the mockDefaults object,
struct AssociatedMock {
static var key: UInt8 = 0
}
extension Mockable {
private (set) var _mockDefaults: MockUserDefaults? {
get {
guard let value = objc_getAssociatedObject(self, &AssociatedMock.key) as? MockUserDefaults else {
return nil
}
return value
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedMock.key, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var userDefaults : UserDefaults {
if (NSClassFromString("XCTest") != nil) {
if self._mockDefaults == nil {
self._mockDefaults = MockUserDefaults()
}
return self._mockDefaults!
}
return UserDefaults.standard
}
}

Related

Unable to store an array of custom objects in UserDefaults [duplicate]

This question already has answers here:
Saving custom Swift class with NSCoding to UserDefaults
(12 answers)
Closed 2 years ago.
I have a custom object called Badge and I have an array of Badges ([Badge]) that I want to store in UserDefaults. I believe I may be doing it incorrectly. I am able to get my code to build but I get the following error on start inside getBadges() : Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value**. Can someone help. I have tried the solution from here but had no luck.
//
// Badge.swift
//
import Foundation
class Badge: NSObject {
var name: String
var info: String
var score: Float?
init(name: String, info: String, score: Float?) {
self.name = name
self.info = info
self.score = score
}
static func ==(lhs: Badge, rhs: Badge) -> Bool {
return lhs.name == rhs.name
}
func encodeWithCoder(coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.info, forKey: "info")
}
}
//
// BadgeFactory.swift
//
import Foundation
class BadgeFactory {
let defaults: UserDefaults
var badges: [Badge] = []
var userBadges: [Badge] = []
static let b = "Badges"
init() {
self.defaults = UserDefaults.standard
self.userBadges = self.getBadges()
}
func addBadges(score: Float) -> [Badge]
{
var new_badges: [Badge] = []
for badge in self.badges {
if (!self.checkIfUserHasBadge(badge: badge) && badge.score != nil && score >= badge.score!) {
new_badges.append(badge)
self.userBadges.append(badge)
}
}
self.defaults.set(self.userBadges, forKey: BadgeFactory.b)
return new_badges
}
func checkIfUserHasBadge(badge: Badge) -> Bool
{
if self.badges.contains(badge) {
return true
}
else {
return false
}
}
func getBadges() -> [Badge] {
return self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
}
func loadDefaultBadges() {
// Score badges.
self.badges.append(Badge(name: "Badge1", info: "My cool badge", score: 80))
self.badges.append(Badge(name: "Badge2", info: "My second cool badge", score: 90))
}
}
//
// ViewController.swift
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var bf = BadgeFactory()
bf.getBadges()
bf.addBadges(score: 85)
}
}
The reason for this error is located in your getBadges() function:
func getBadges() -> [Badge] {
return self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
}
With as! you are implicitly unwrapping the array you expect. But as long as you didn't write data to this userDefaults key, array(forKey:) will always return nil!
For this reason, you need to use safe unwrapping here, for example like so:
return self.defaults.array(forKey: BadgeFactory.b) as? [Badge] ?? [].
But that's not the only problem. Like you already stumbled about, you still need to implement the solution of the thread you posted. Custom NSObjects cannot be stored in Defaults without encoding.
You need to implement the NSCoding protocol in your Badge class (init(coder:) is missing) and use an Unarchiver for reading, along with an Archiver for writing your data to defaults.
So your code should look something like this:
class Badge: NSObject, NSCoding {
var name: String
var info: String
var score: Float?
init(name: String, info: String, score: Float?) {
self.name = name
self.info = info
self.score = score
}
required init?(coder: NSCoder) {
self.name = coder.decodeObject(forKey: "name") as! String
self.info = coder.decodeObject(forKey: "info") as! String
self.score = coder.decodeObject(forKey: "score") as? Float
}
static func ==(lhs: Badge, rhs: Badge) -> Bool {
return lhs.name == rhs.name
}
func encodeWithCoder(coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.info, forKey: "info")
coder.encode(self.score, forKey: "score")
}
}
class BadgeFactory {
...
func addBadges(score: Float) -> [Badge] {
...
let data = NSKeyedArchiver.archivedData(withRootObject: self.userBadges)
defaults.set(data, forKey: BadgeFactory.b)
...
}
func getBadges() -> [Badge] {
guard let data = defaults.data(forKey: BadgeFactory.b) else { return [] }
return NSKeyedUnarchiver.unarchiveObject(ofClass: Badge, from: data) ?? []
}
...
}
the error likely comes from this line in your viewDidLoad:
bf.getBadges()
This will try to execute self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
At this point UserDefaults are empty (because you do that before calling .addBadges or providing any other value for the key). So self.defaults.array(forKey: BadgeFactory.b) will evaluate to nil and the forced casting as! [Badge] will fail at runtime with a message like the one you provided.
To resolve I would adjust the function like this:
func getBadges() -> [Badge] {
return (self.defaults.array(forKey: BadgeFactory.b) as? [Badge]) ?? []
}

Setters and Getters with Generics Swift

I was wondering if there is a way to do this in a better way with generics. I have this singleton which needs a setter and another getter for every object. Plus, I have to check that the property is not nil in every getter, which is a lot of repeated code. ex:
class DataManager : NSObject {
private override init(){}
private var postData : [PostModel]?
private var userData : [UserModel]?
private var commentsData : [CommentsModel]?
private var photosData : [PhotosModel]?
private var albumsData : [AlbumsModel]?
private var todosData : [TodosModel]?
static let shared = DataManager()
//MARK : Setters
func setUserData(data : [UserModel]) {
self.userData = data
}
func setPostData(data : [PostModel]) {
self.postData = data
}
func setCommentsData(data : [CommentsModel]) {
self.commentsData = data
}
func setAlbumsData(data : [AlbumsModel]) {
self.albumsData = data
}
func setPhotosData(data : [PhotosModel]) {
self.photosData = data
}
func setTodosData(data : [TodosModel]) {
self.todosData = data
}
//MARK : Getters
func getUserData() -> [UserModel]? {
if self.userData != nil {
return self.userData!
}
return nil
}
func getPostData() -> [PostModel]? {
if self.postData != nil {
return self.postData!
}
return nil
}
func getCommentsData() -> [CommentsModel]? {
if self.commentsData != nil {
return self.commentsData!
}
return nil
}
func getAlbumsData() -> [AlbumsModel]? {
if self.albumsData != nil {
return self.albumsData!
}
return nil
}
func getPhotosData() -> [PhotosModel]? {
if self.photosData != nil {
return self.photosData!
}
return nil
}
func getTodosData() -> [TodosModel]? {
if self.todosData != nil {
return self.todosData!
}
return nil
}
}
I was wondering if all this logic could be done in one single method, maybe using generics?
If you want to force all object to set as a none optional and also get as a none optional, you don't need to define them as optional at the first place:
So instead of:
private var postData : [PostModel]?
you should have:
private var postData = [PostModel]()
This will gives you an empty none optional array and it can not be set or get as an optional.
If you want them to be nil before someone get them (for memory management or etc.), You can make them lazy:
private lazy var postData = [PostModel]()
So now postData will be nil until someone tries to read the value of it.
If you need to do some extra job when someone tries to set one of these, you can observe for changes before set and after set of the value:
private var postData = [PostModel]() {
willSet { /* right before the value is going to set */ }
didSet { /* right after the value is set */ }
}
Note that lazy properties can not have observers
So seems like you don't have any of the functions at all. And you can refactor your code to something like this:
class DataManager : NSObject {
private override init(){}
lazy var postData = [PostModel]()
lazy var userData = [UserModel]()
lazy var commentsData = [CommentsModel]()
lazy var photosData = [PhotosModel]()
lazy var albumsData = [AlbumsModel]()
lazy var todosData = [TodosModel]()
}

UserDefaults saving array of dictionaries

I'm trying to save dictionary into UserDefaults And then I want to fetch data or delete.
class userDefaultsManager {
static func getAllUsers()->[UserModel]{
if let all = UserDefaults.standard.array(forKey: "usersKey") as? [Dictionary<String,Any>] {
return all.map{UserModel.init(dictionary: $0)}
}
return []
}
static func insertUser(name:String, email:String)->Bool {
let newUserModel = UserModel.init(name: name, email: email)
var all = getAllUser()
all.append(newUserModel)
UserDefaults.standard.set(all.map{$0.dictionary}, forKey: "usersKey")
return UserDefaults.standard.synchronize()
}
static func deleteUser(email:String)->Bool {
var all = getAllUser()
let index = all.index{$0.email == email}
if index != nil {
all.remove(at: index!)
UserDefaults.standard.set(all.map{$0.dictionary}, forKey: "usersKey")
UserDefaults.standard.synchronize()
return true
} else {
return false
}
}
}
class UserModel:NSObject{
var name: String!
var email: String!
init(name:String, email:String) {
self. name = name
self. email = email
super.init()
}
init(dictionary:[String:Any]) {
self.name = ""
self.email = ""
super.init()
self.setValuesForKeys(dictionary)
}
var dictionary:[String:Any] {
return self.dictionaryWithValues(forKeys: ["name","email"]) //Error here
}
}
The code working on swift 3 but I got error with swift 4 on var dictionary:[String:Any]
here is the error:
implicit Objective-C entrypoint -[Myapp.UserModel name] is
deprecated and will be removed in Swift 4
Please any help to fix this will be appreciated.
Your classes inherited from NSObject, and using objc KVC, it was fine for Swift3, because Swift3 assumed all NSObject subclasses as #objc by default, in Swift4 you need to declare your accessors #objc to make them available for obj-c KVC operations.
https://forums.developer.apple.com/thread/81789

How to access elements from an NSObject in swift?

I've set some variables as an Object
import UIKit
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
Then in the controller I set this object and pass it to a class to store it:
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
I need to store this object and then access the elements within in a view later, This is what I have:
import Foundation
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
}
But when I try to print the the object I just get
Printing testResultArray: [<ProjectName.SpeedTestResult: 0x127b85e50>]
How do I access elements within the object and store this object and retrieve it for later use in a view?
class TestResult : NSObject, NSSecureCoding {
var testTime: NSDate?
override init() {
super.init()
}
#objc required init?(coder decoder: NSCoder) {
self.testTime = decoder.decodeObjectForKey("testTime") as? NSDate
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testTime, forKey: "testTime")
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "TestResult: %#", self.testTime ?? "null")
}
}
class SpeedTestManager : NSObject, NSSecureCoding {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
override init() {
super.init()
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testResultArray, forKey: "testResultArray")
}
#objc required init?(coder decoder: NSCoder) {
self.testResultArray = decoder.decodeObjectForKey("testResultArray") as! [NSObject]
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "SpeedManager: [%#]", self.testResultArray.map({"\($0)"}).joinWithSeparator(","))
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testResult = TestResult()
testResult.testTime = NSDate()
let speedManager = SpeedTestManager()
speedManager.addTestResult(testResult)
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(speedManager), forKey: "speedManager")
NSUserDefaults.standardUserDefaults().synchronize()
if let archivedSpeedManager = NSUserDefaults.standardUserDefaults().objectForKey("speedManager") as? NSData {
let unarchivedSpeedManager = NSKeyedUnarchiver.unarchiveObjectWithData(archivedSpeedManager)
print("SpeedManager: \(unarchivedSpeedManager ?? "null")")
}
else {
print("Failed to unarchive speed manager")
}
}
}
Here is one way you can do it:
import Foundation
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
for result in testResultArray {
// This will crash if result is not a SpeedTestResult.
// print((result as! SpeedTestResult).testTime)
// This is better:
if let timeResult = result as? SpeedTestResult
{
print(timeResult.testTime)
}
else
{
print("Not time type...")
}
}
}
}
var testResult = SpeedTestResult()
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
This addresses your specific question, but there are some other problems here:
If you are going to store only SpeedTestResult instances in
testResultArray, then why not make it of type
[SpeedTestResutl]()?
If you will store different types of tests in the array, then how do
you find out which type of test an NSObject element represents?
There are ways... In the above code we at least make sure we are not treating a wrong type of object as a SpeedTestResult.
When you do SpeedTestManager().addTestResult(testResult), you don't
keep a reference to the SpeedTestManager instance. The next time
you make the same call, you will be creating a different
SpeedTestManager instance.
This is not really a problem, but SpeedTestManager does not have to
be a sub-class of NSObject, unless you want to use it in
Objective-C.
You probably don't want to print the content of testResultArray in
the addTestResult() method. You could have other methods for
accessing the array.
To add your test results to the same test manager, you could do:
let myTestManager = SpeedTestManager()
myTestManager.addTestResult(testResult)
// create other test results and add them ...

How to Implement Time-based NSCache with setObject Swift 2.0

I have an NSDictionary which I cached. I need to implement a time-based setObject with timestamp. NSCache Class doesn't have a setExpiry. Any help would be appreciated.
This is the extension I have so far:
import Foundation
extension NSCache {
class var sharedInstance : NSCache {
struct Static {
static let instance : NSCache = NSCache()
}
return Static.instance
}
}
I found NSCache Extension at http://nshipster.com/nscache/ . Any easy way to implement with an expiry timestamp?
extension NSCache {
subscript(key: AnyObject) -> AnyObject? {
get {
return objectForKey(key)
}
set {
if let value: AnyObject = newValue {
setObject(value, forKey: key)
} else {
removeObjectForKey(key)
}
}
}
}
Here is the basic approach.
PS: I haven't tested this code and I wrote it in the text editor. It may require some tweaks depending on your requirements :)
import Foundation
protocol Cacheable: class {
var expiresAt : NSDate { get set }
}
class CacheableItem : Cacheable {
var expiresAt = NSDate()
}
extension NSCache {
subscript(key: AnyObject) -> Cacheable? {
get {
if let obj = objectForKey(key) as? Cacheable {
var now = NSDate();
if now.isGreaterThanDate(obj.expiresAt) {
removeObjectForKey(key)
}
}
return objectForKey(key) as? Cacheable
}
set {
if let value = newValue {
setObject(value, forKey: key)
} else {
removeObjectForKey(key)
}
}
}
}
extension NSDate
{
func isGreaterThanDate(dateToCompare : NSDate) -> Bool
{
var isGreater = false
if self.compare(dateToCompare) == NSComparisonResult.OrderedDescending {
isGreater = true
}
return isGreater
}
}
Based on this Stack Overflow answer.
You can also use a timer to empty the queue:
private let ExpiringCacheObjectKey = "..."
private let ExpiringCacheDefaultTimeout: NSTimeInterval = 60
class ExpiringCache : NSCache {
/// Add item to queue and manually set timeout
///
/// - parameter obj: Object to be saved
/// - parameter key: Key of object to be saved
/// - parameter timeout: In how many seconds should the item be removed
func setObject(obj: AnyObject, forKey key: AnyObject, timeout: NSTimeInterval) {
super.setObject(obj, forKey: key)
NSTimer.scheduledTimerWithTimeInterval(timeout, target: self, selector: "timerExpires:", userInfo: [ExpiringCacheObjectKey : key], repeats: false)
}
// Override default `setObject` to use some default timeout interval
override func setObject(obj: AnyObject, forKey key: AnyObject) {
setObject(obj, forKey: key, timeout: ExpiringCacheDefaultTimeout)
}
// method to remove item from cache
func timerExpires(timer: NSTimer) {
removeObjectForKey(timer.userInfo![ExpiringCacheObjectKey] as! String)
}
}

Resources