why core data can't save NSNumber value? - ios

I create NSNumber, use bool value control NSNumber.
I build simulator, core data have NSNumber data.
but i turn off simulator. Restart simulator again. NSNumber data not see
thank you for help.
core data value
#NSManaged public var cIsLike: NSNumber
var isLike: Bool {
get {
return Bool(truncating: cIsLike)
}
set {
cIsLike = NSNumber(value: newValue)
}
}
change bool and save data code
private let moc: NSManagedObjectContext
private var videoArray = [CoreVideo]()
init(moc: NSManagedObjectContext) {
self.moc = moc
}
func updateVideo(currentVideo video: CoreVideo, isLike newValue: Bool) {
video.isLike = newValue
save()
}
private func save() {
do {
try moc.save()
} catch let error as NSError {
print("Save failed: \(error.localizedDescription)")
}
}

Related

Fetch CoreData using Generic Model and privateQueueConcurrencyType

I've made a method to fetch my coredata Objects array using generics and privateQueueConcurrencyType. However, I'm doing something wrong somewhere and I'm experiencing crashes because of that.
The place where I get the crash as follow. what am I missing here ??
func fetchData<T: NSFetchRequestResult>(entity: String, model: T.Type, _ custom_predicate: NSPredicate?=nil) throws -> [T] {
let request = NSFetchRequest<T>(entityName: entity)
if custom_predicate != nil {
request.predicate = custom_predicate
}
request.returnsObjectsAsFaults = false
//Crash is the bellow line
return try privateMOC.fetch(request)//This line throws the crash
}
My privateMOC initialisation as follow.
class StorageManager: NSObject {
let privateMOC: NSManagedObjectContext!
private override init() {
privateMOC = CoreDataManager.sharedManager.updateContext
}
private static var SMInstance: StorageManager?
lazy var managedObjectContext: NSManagedObjectContext = {
return CoreDataManager.sharedManager.persistentContainer.viewContext
}()
}
My CoreData stack as follow.
class CoreDataManager {
static let sharedManager = CoreDataManager()
let persistentContainer: NSPersistentContainer!
let viewContext: NSManagedObjectContext!
let updateContext: NSManagedObjectContext!
private init() {
let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Store")
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
self.persistentContainer = container
self.viewContext = persistentContainer.viewContext
//This is where I use the privateQueueConcurrencyType formy privateMOC
let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
_updateContext.parent = self.viewContext
self.updateContext = _updateContext
}
Stacktrace as follow.
When you are using background context (private queue), you should wrap it in perform or performAndWait. With that said the fetch method should be called like this:
context.performAndWait {
context.fetch(request)
}
Because the queue is private and internal to the NSManagedObjectContext instance, it can only be accessed through the perform(:) and the performAndWait(:) methods.
More about using core data in background read here
Edit 1:
performAndWait takes a closure as its parameter that has no return value, so you can't return result/value from it. You need to understand these concepts in Swift.
Let's take your problem for example, you have a function that you want to return array of some values after the fetch request is performed. What you did is not going to work because of what I said earlier, so we will "extract" the context outside of the function.
func fetch<T>(_ type: T.Type, in context: NSManagedObjectContext) -> [T] {
return context.fetch(request)
}
In that way we can wrap the function in whatever context we want either background or viewContext(main).
context.performAndWait {
let products = fetch(SubProduct.self, in: context)
// do some other stuff
}
I can't replicate your crash but I was getting an Error in the line you highlighted.
I didn't have info on you SubProduct so I used the generic Item that comes with Xcode Projects
There are a ton of comments that come with the code.
This is just a standard View to test functionality.
struct CoreDataBackgroundView: View {
#StateObject var vm: CoreDataBackgroundViewModel2 = CoreDataBackgroundViewModel2()
public init(){}
var body: some View {
VStack{
List(vm.items){ item in
Button(action: {
vm.fetchItem(timestamp: item.timestamp)
}, label: {
Text(item.timestamp!.description)
})
}
Text("fetched Item = \(vm.item?.timestamp?.description ?? "nil")")
Text(vm.items.count.description)
Button("fetch", action: {
vm.fetchItems()
})
Button("add", action: {
vm.addItem()
})
}
}
}
struct CoreDataBackgroundView_Previews: PreviewProvider {
static var previews: some View {
CoreDataBackgroundView()
}
}
This is how I incorporated your code
class CoreDataBackgroundViewModel2: ObservableObject{
private let manager = StorageManager.SMInstance
#Published var items: [Item] = []
#Published var item: Item? = nil
///Fetch everything
func fetchItems() {
do{
items = try manager.fetchData(entity: "Item", model: Item.self)
}catch{
print(error)
items = []
}
}
///This way you can just fetch the item(s) that you need
func fetchItem(timestamp: Date?) {
if timestamp != nil{
do{
item = try manager.fetchData(entity: "Item", model: Item.self, NSPredicate(format: "timestamp == %#", timestamp! as CVarArg)).first
}catch{
print(error)
item = nil
}
}else{
item = nil
}
}
func addItem() {
manager.addItem()
}
}
//Something had to change here there is no entry
class StorageManager: NSObject {
let manager = CoreDataManager.sharedManager
let privateMOC: NSManagedObjectContext
private override init() {
privateMOC = manager.container.newBackgroundContext()
}
//I made it a singleton
static var SMInstance: StorageManager = StorageManager()
lazy var managedObjectContext: NSManagedObjectContext = {
return manager.container.viewContext
}()
func fetchData<T: NSFetchRequestResult>(entity: String, model: T.Type, _ custom_predicate: NSPredicate?=nil) throws -> [T] {
let request = NSFetchRequest<T>(entityName: entity)
if custom_predicate != nil {
request.predicate = custom_predicate
}
request.returnsObjectsAsFaults = false
//Didn't get a crash but an Error
//Value of optional type 'NSManagedObjectContext?' must be unwrapped to refer to member 'fetch' of wrapped base type 'NSManagedObjectContext'
// It is bad practice to use ! try to minimize those as much as possible
return try privateMOC.fetch(request)
}
func addItem()
{
privateMOC.perform {
let newItem = Item(context: self.privateMOC)
newItem.timestamp = Date()
newItem.text = "sample"
do{
try self.privateMOC.save()
}catch{
print(error)
}
}
}
}
//I changed your manager because there were a bunch of nil objects that are unnecessary also, I added the ability to use an inMemory conxtext for when you are using previews/canvas
//This is basically the standard setup with comes with all New Projects in Xcode
//Some of the errors probably had to do with the let variables that were after loadPersistentStores. They were called before the store was done loading so they stayed nil. The speed of the load might have given you mixed results.
class CoreDataManager {
static let sharedManager = CoreDataManager.previewAware()
private static func previewAware() -> CoreDataManager{
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"{
return CoreDataManager.preview
}else{
return CoreDataManager.shared
}
}
private static let shared = CoreDataManager()
private static var preview: CoreDataManager = {
let result = CoreDataManager(inMemory: true)
let viewContext = result.container.viewContext
for n in 0..<2 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
//This is usually the AppName
container = NSPersistentContainer(name: "AppName")
//If you are in Preview the setup work take place only on a device or simulator
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = false
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
}
}

JsonEncoder always return empty array

Im trying to save struct array into UserDefaults and I cant figure out why JsonEncoder return empty data. I have setup model that conforms Codable protocol
struct MenuItem : Codable{
let name : String?
let icon : String?
init(name : String?, icon : String?){
self.name = name
self.icon = icon
}
}
and also created Defaults manager for saving it into user defaults.
class DefaultsManager {
static let shared = DefaultsManager()
init(){}
var items: [MenuItem]{
get{
if let json = UserDefaults.standard.data(forKey: "key"){
return decodeFromJson(jsonData: json)
} else {
return []
}
}
set{
let json = codeToJson(data: items)
UserDefaults.standard.set(json, forKey: "key")
}
}
fileprivate func codeToJson<T:Codable>(data: Array<T>) -> Data?{
do {
return try JSONEncoder().encode(data)
} catch {
print(error)
return nil
}
}
fileprivate func decodeFromJson<T:Codable>(jsonData: Data) -> [T]{
do {
return try JSONDecoder().decode(Array<T>.self, from: jsonData)
} catch {
print(error)
return []
}
}
}
but whatever I do JsonEncoder returns empty data.. I tried to google but without success.
That's a very common mistake.
In a setter of a computed property the new value is represented by the implicit newValue variable, it's not the property itself.
set {
let json = codeToJson(data: newValue)
UserDefaults.standard.set(json, forKey: "key")
}
Change Array<T>.self to [MenuItem].self
return try JSONDecoder().decode([MenuItem].self, from: jsonData)

Can't save custom class array to UserDefaults

I'm trying to save a custom class array to UserDefaults but it doesn't work. I get nil back on if let. I looked everywhere online. I'm using Swift 4.2
extension UserDefaults {
func saveReciters(_ reciters: [Reciter]) {
do {
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: reciters, requiringSecureCoding: false)
self.set(encodedData, forKey: UD_RECITERS)
} catch {
debugPrint(error)
return
}
}
func getReciters() -> [Reciter] {
if let reciters = self.object(forKey: UD_RECITERS) as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: reciters) as! [Reciter]
} else {
print("EMPTY RECITERS")
return [Reciter]()
}
}
}
UserInfo={NSDebugDescription=Caught exception during archival: -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600001babcc0
Thats my class:
class Reciter: NSCoding {
private(set) public var name: String
private(set) public var image: UIImage?
private(set) public var surahs: [Surah]
private(set) public var documentID: String
private let quranData = QuranData()
init(name: String, image: UIImage?, surahCount: Int?, documentID: String) {
self.name = name
self.image = image
self.documentID = documentID
if let surahCount = surahCount {
surahs = Array(quranData.getAllSurahs().prefix(surahCount))
} else {
surahs = quranData.getAllSurahs()
}
}
func encode(with aCoder: NSCoder) {
}
required init?(coder aDecoder: NSCoder) {
}
}
On my Surah class i get nil back. All other properties i get back succesfully
Most often I see developer's use codeable, here I am using user as an example:
YourDataModel.swift
struct User: Codable {
var userId: String = ""
var name: String = ""
var profileImageData: Data? }
UserDefaults.swift
import Foundation
extension UserDefaults {
/// The current user of the application, see `./Models/User.swift`
var currentUser: User? {
get {
guard let userData = self.object(forKey: #function) as? Data else { return nil }
return try? JSONDecoder().decode(User.self, from: userData)
}
set {
guard let newuser = newValue else { return }
if let userData = try? JSONEncoder().encode(newuser) {
self.set(userData, forKey: #function)
}
}
}
}
Transform the data into json data... #function is the function or value name i.e.
// For the case the user doesn't yet exist.
if ( UserDefaults.standard.currentUser == nil ) {
// Create a new user
user = User()
// Generate an id for the user, using a uuid.
user?.userId = UUID().uuidString
} else {
// otherwise, fetch the user from user defaults.
user = UserDefaults.standard.currentUser
}

iOS - Mocking UserDefault before loading the view controller

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
}
}

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