ValueTransformerを使ってAttributedStringを保存するとクラッシュする(SwiftData)
はじめに
SwiftData
の @Model
に AttributedString
持たせようとゴニョゴニョしたらクラッシュした話です。
iOS17 ならすんなりいけたんですが iOS 17.5 だとクラッシュしてなかなかうまくいきませんでした。どうやら私の書き方だと iOS 17.1 以降でクラッシュするようです。
参考
.transformable with ValueTransformer failing after Xcode 15.1 update
環境
- Xcode 15.4
- iOS 17.5
クラッシュする書き方
最初は下記のように書いていました。
import SwiftUI
import SwiftData
@main
struct HogeApp: App {
init() {
AttributedStringTransformer.register()
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Item.self)
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query var items: [Item]
var body: some View {
VStack {
List(items, id: \.id) { item in
Text(item.text)
}
Button("add") {
var string = AttributedString("hoge")
string.font = UIFont.systemFont(ofSize: CGFloat((14...80).randomElement()!))
string.foregroundColor = [UIColor.red, UIColor.blue, UIColor.green, UIColor.orange].randomElement()!
context.insert(Item(text: string))
try? context.save()
}
}
}
}
@objc(AttributedStringTransformer)
class AttributedStringTransformer: ValueTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: AttributedStringTransformer.self))
public static func register() {
let transformer = AttributedStringTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func transformedValueClass() -> AnyClass {
return NSAttributedString.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
guard let attributedString = value as? AttributedString,
let data = try? NSKeyedArchiver.archivedData(withRootObject: NSAttributedString(attributedString), requiringSecureCoding: false) else {
return nil
}
return data as NSData
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let stringData = value as? NSData,
let attributedString = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: stringData as Data) else {
return nil
}
return AttributedString(attributedString)
}
}
@Model class Item {
@Attribute(.unique) var id: String
@Attribute(.transformable(by: AttributedStringTransformer.name.rawValue)) var text: AttributedString
init(id: String = UUID().uuidString, text: AttributedString) {
self.id = id
self.text = text
}
}
ValueTransformer
で AttributedString
を NSAttributedString
に変換してから NSKeyedArchiver
で NSData
にして保存するやり方です。
こちらは iOS 17 だと問題なく動作するのですが iOS 17.5 は以下の理由でクラッシュします。
'Unacceptable type of value for attribute: property = "text"; desired type = NSAttributedString; given type = __SwiftValue; value = hoge
回避策
.transformable with ValueTransformer failing after Xcode 15.1 updateを見た感じ iOS 17.1 以降はtransformedValueClass
と @Model
の型を揃える必要がありそうです。
下記のように NSAttributedString
で揃えました。
@objc(AttributedStringTransformer)
class AttributedStringTransformer: ValueTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: AttributedStringTransformer.self))
public static func register() {
let transformer = AttributedStringTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
override class func transformedValueClass() -> AnyClass {
return NSAttributedString.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
guard let attributedString = value as? NSAttributedString,
let data = try NSKeyedArchiver.archivedData(withRootObject: attributedString, requiringSecureCoding: false) else {
return nil
}
return data as NSData
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let stringData = value as? NSData,
let attributedString = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: stringData as Data) else {
return nil
}
return attributedString
}
}
@Model class Item {
@Attribute(.unique) var id: String
@Attribute(.transformable(by: AttributedStringTransformer.name.rawValue)) var text: NSAttributedString
init(id: String = UUID().uuidString, text: NSAttributedString) {
self.id = id
self.text = text
}
}
画面表示はこんな感じです。
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query var items: [Item]
var body: some View {
VStack {
List(items, id: \.id) { item in
Text(AttributedString(item.text))
}
Button("add") {
var string = AttributedString("hoge")
string.font = UIFont.systemFont(ofSize: CGFloat((14...80).randomElement()!))
string.foregroundColor = [UIColor.red, UIColor.blue, UIColor.green, UIColor.orange].randomElement()!
let item = Item(text: NSAttributedString(string))
context.insert(item)
try? context.save()
}
}
}
}
とりあえずこれで iOS 17 と iOS 17.5 両方で動くようになりました🎉
おわりに
結局直接 AttributedString
を扱うことはできませんでしたが NSAttributedString
なら一発で変換できるのでまあ。。。
こんな感じで Item
に変換用のイニシャライザとコンピューテッドプロパティはやしてもいいですし。
var attributedText: AttributedString {
return AttributedString(text)
}
init(id: String = UUID().uuidString, text: AttributedString) {
self.id = id
self.text = NSAttributedString(text)
}
SwiftData
むずかしい🙃
なにか他にいい方法あったら教えてください🙇♂️
Discussion