【iOS】CoreData保存自定义数据(swift)

在实际开发中,CoreData默认的几种基础类型,是远远不能满足需求的,因此它才提供了一种叫transformable的类型,也就是可变类型,或者叫可转换类型。简单来说,就是通过某种转换机制,可以将任意类型转换成像默认的基础类型一样的数据类型!

——但是在实际操作中,我们可能会遇到一些令人困惑的警告,以及其它意想不到的坑,这里将我的经验写下来,共大家参考……

关于Xcode的警告:

Xcode会给我们一个️警告,其中的一个,内容大概是这样的:

warning: Misconfigured Property: xxxxxx.xxxx is using a nil or insecure value transformer. Please switch to NSSecureUnarchiveFromDataTransformerName or a custom NSValueTransformer subclass of NSSecureUnarchiveFromDataTransformer

大概的意思就是:xxxxxx中的某个字段xxxx,没有指定(nil),或者用了一个不安全的value转换器,请替换成安全的转换器,也就是NSSecureUnarchiveFromDataTransformerName,或者自定义一个继承自NSSecureUnarchiveFromDataTransformerName的转换器子类NSValueTransformer

并且在控制台,也会给出这么一条信息:

[general] 'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release

或者是这样的️警告:

CoreData: One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using “NSSecureUnarchiveFromData” or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using “NSSecureUnarchiveFromData” when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.

意思跟上面的差不多,个人的理解是:我们使用CoreData提供的几种基础类型之外的数据类型,我们又没有指定转换器(没有在属性查看器面板中指定Transformer的值),也就是nil,默认就是NSKeyedUnarchiveFromDataTransformerName,而这个转换器采用的NSKeyedUnarchiveFromData是一种不安全的编码方式,并且这是即将弃用的方式!……以后会用NSSecureUnarchiveFromData作为默认的转换方式,如果你没有指定,就采用这个默认的NSSecureUnarchiveFromData,但是这种方式要求属性的类型必须遵循NSSecureCoding协议,如果你的属性的类型没有遵循NSSecureCoding,那么就会出问题!

关于NSSecureCoding和transformable属性,可以参考这篇文章
NSSecureCoding and transformable properties in Core Data

接下来,我会用一个简单的案例,来演示如何用CoreData保存自定义的数据类型([UIColor])

这里我创建了一个名为ColorLibrary实体,(这省略了前面的一些步骤,如果对于CoreData的一些基础操作还不太清楚,可以参考其他人的文章),并且创建了一个String类型的字段name,和一个名为colorList,类型为Transformable的字段。这时候,如果什么都不做,我们编译项目,就会看到一个Buildtime的警告,情况就是上面提到的!……

这时候项目虽然能跑,但是心里总不踏实,那么接下来我们就去处理它!

首先,我们添加一个名为ColorValueTransformer的转换器类,让这个类继承自NSSecureUnarchiveFromDataTransformer

//
//  ColorValueTransformer.swift
//  iColorScheme
//
//  Created by Qire_er on 2021/12/28.
//

import Foundation
import UIKit

@objc(UIColorValueTransformer)
final class ColorValueTransformer: NSSecureUnarchiveFromDataTransformer {
    
    // 定义静态属性name,方便使用
    static let name = NSValueTransformerName(rawValue: String(describing: ColorValueTransformer.self))
    
    // 重写allowedTopLevelClasses,确保UIColor在允许的类列表中
    override static var allowedTopLevelClasses: [AnyClass] {
        return [NSArray.self, UIColor.self] // NSArray.self 也要加上,不然不能在数组中使用!
    }
    
    // 定义Transformer转换器注册方法
    public static func register() {
        let transformer = ColorValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

接下来,把我们自定义的ColorValueTransformer名称填在Transformer字段中,同时也把Custom Class设置[UIColor](因为我们要把UIColor以数组的形式存放在数据库中)


Transformer字段

如果这时候运行,Xcode会报错:Cannot find type 'UIColor' in scope(说找不到UIColor!)

因为自动生成的ColorLibrary+CoreDataProperties.swift,并没有帮我们把UIKit的包import进来,而UIColor是UIKit中的东西!

这时候,如果我们手动导入UIKit是不行的!因为默认自动生成的代码,我们就不能手动修改!(即使你在外面修改好,Xcode也会帮你自动清除!)
那我们就需要将自动生成的方式,改成手动生成!方法就是选中实体ColorLibrary,然后修改codegen字段为Manual/None


修改codegen

然后,选中CoreDataTest.xcdatamodeld,在Editor菜单中,找到Create NSManagedObject Subclass...菜单,然后一路next!……
完成之后,会在项目中多出两个文件:ColorLibrary+CoreDataClass.swift和ColorLibrary+CoreDataProperties.swift!


手动生成

然后在ColorLibrary+CoreDataProperties.swift把UIKit的包import进来

import Foundation
import CoreData
import UIKit // 导入UIKit

extension ColorLibrary {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<ColorLibrary> {
        return NSFetchRequest<ColorLibrary>(entityName: "ColorLibrary")
    }

    @NSManaged public var colorList: [UIColor]?
    @NSManaged public var name: String?
    ...
}

如果这个时候我们直接运行,还是有问题!因为我们虽然把需要的东西都准备好了,但是我们没有把我们自定义的ColorValueTransformer真正使用起来,这时候运行Xcode其实还是用默认的NSKeyedUnarchiveFromData,所以运行的时候还是会报以前的警告️ ……

我们最后一件要做的事情就是,在AppDelegate初始化完成之前,注册我们自定义的ColorValueTransformer!

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    override init() {
        super.init()
        ColorValueTransformer.register()    // 注册ColorValueTransformer
    }
    ...
}

为了方便测试,我们在页面上写一个测试button,并绑定点击事件,点击一次就增加一条数据!

    private var addBtn: UIButton! // 测试按钮
    let colorList = [UIColor.red, UIColor.yellow, UIColor.orange, UIColor.green, UIColor.blue]  // 定义颜色列表
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        addBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
        addBtn.setTitle("addBtn", for: .normal)
        addBtn.backgroundColor = .red
        addBtn.center = view.center
        addBtn.addTarget(self, action: #selector(addHandler), for: .touchUpInside)
        
        view.addSubview(addBtn)
        view.backgroundColor = .white
    }
    
    // 按钮点击事件处理函数
    @objc private func addHandler() {
        
        let colorLibrary = ColorLibrary(context: managedObectContext)
        colorLibrary.name = "好看的颜色呀!^_^"
        colorLibrary.colorList = self.colorList
        
        print(colorLibrary)
        
        appDelegate.saveContext() // 保存到数据库
    }

最后,Run ……

是不是一下感觉世界清净了!_

为了验证结果,我们在终端打开CoreDataTest.sqlite文件,查看里面的数据是不是加进来啦!

终端查看数据

完整代码:

  1. AppDelegate.swift
//
//  AppDelegate.swift
//  CoreDataTest
//
//  Created by Qire_er on 2021/12/28.
//

import UIKit
import CoreData

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // 重写init方法
    override init() {
        super.init()
        ColorValueTransformer.register() // 注册ColorValueTransformer(只要保证在初始化完成之前注册,用什么方式都可以)
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentContainer(name: "CoreDataTest")
        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)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.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)")
            }
        }
    }

}
  1. ColorLibrary+CoreDataClass.swift
//
//  ColorLibrary+CoreDataClass.swift
//  CoreDataTest
//
//  Created by Qire_er on 2021/12/28.
//
//

import Foundation
import CoreData

@objc(ColorLibrary)
public class ColorLibrary: NSManagedObject {

}
  1. ColorLibrary+CoreDataProperties.swift
//
//  ColorLibrary+CoreDataProperties.swift
//  CoreDataTest
//
//  Created by Qire_er on 2021/12/28.
//
//

import Foundation
import CoreData
import UIKit

extension ColorLibrary {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<ColorLibrary> {
        return NSFetchRequest<ColorLibrary>(entityName: "ColorLibrary")
    }

    @NSManaged public var colorList: [UIColor]?
    @NSManaged public var name: String?

}

extension ColorLibrary : Identifiable {

}
  1. ColorValueTransformer.swift
//
//  ColorValueTransformer.swift
//  iColorScheme
//
//  Created by Qire_er on 2021/12/28.
//

import Foundation
import UIKit

@objc(UIColorValueTransformer)
final class ColorValueTransformer: NSSecureUnarchiveFromDataTransformer {
    
    // 定义静态属性name,方便使用
    static let name = NSValueTransformerName(rawValue: String(describing: ColorValueTransformer.self))
    
    // 重写allowedTopLevelClasses,确保UIColor在允许的类列表中
    override static var allowedTopLevelClasses: [AnyClass] {
        return [NSArray.self, UIColor.self] // NSArray.self 也要加上,不然不能在数组中使用!
    }
    
    // 定义Transformer转换器注册方法
    public static func register() {
        let transformer = ColorValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}
  1. ViewController
//
//  ViewController.swift
//  CoreDataTest
//
//  Created by Qire_er on 2021/12/28.
//

import UIKit
import CoreData

class ViewController: UIViewController {
    
    let appDelegate = UIApplication.shared.delegate as! AppDelegate // 获取AppDelegate
    lazy var managedObectContext = appDelegate.persistentContainer.viewContext // 管理对象上下文
    let entityName = "ColorLibrary" // 把实体名写成一个属性,以免写错!
    
    
    private var addBtn: UIButton! // 测试按钮
    let colorList = [UIColor.red, UIColor.yellow, UIColor.orange, UIColor.green, UIColor.blue]  // 定义颜色列表
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        addBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
        addBtn.setTitle("addBtn", for: .normal)
        addBtn.backgroundColor = .red
        addBtn.center = view.center
        addBtn.addTarget(self, action: #selector(addHandler), for: .touchUpInside)
        
        view.addSubview(addBtn)
        view.backgroundColor = .white
    }
    
    // 按钮点击事件处理函数
    @objc private func addHandler() {
        
        let colorLibrary = ColorLibrary(context: managedObectContext)
        colorLibrary.name = "好看的颜色呀!^_^"
        colorLibrary.colorList = self.colorList
        
        print(colorLibrary)
        
        appDelegate.saveContext() // 保存到数据库
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容