【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文件,查看里面的数据是不是加进来啦!
终端查看数据完整代码:
- 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)")
}
}
}
}
- 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 {
}
- 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 {
}
- 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)
}
}
- 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() // 保存到数据库
}
}