肇鑫的技术博客

肇鑫 / Owen Zhao

独立开发者,主要开发 iOS、watchOS、macOS 应用。

目前在维护 SleepTapRooster Time,以及 Markdown Writer 相关工具。

最新文章

开发者备份和恢复Realm数据库

RealmSwift

最近在研究带参数的捷径。因为是SiriKit相关,需要把Realm放在应用组里。这样就不能通过Xcode下载容器了。几天时间里,已经有了两次不小心删除掉数据库,而不得不通过备份恢复手机的经历。我开始研究一下如何导出和恢复数据。

思路

因为目的只是为了方便开发,而不是交给用户操作,过程简便,代码容易是重点。

首先想到的是通过长按某个按钮实现导出,然后出了问题就将导出的数据库放在应用里,应用启动时检测是否有数据,如果有,就替换,然后再正常启动。

实现

具体实现的时候,遇到了一个问题,分享出来的URL,并没有写成文件,而是写成了纯文本。

@objc private func exportRealm(_ sender:Any) {
    let sourceURL = Realm.Configuration.defaultConfiguration.fileURL!
    
    // copy realm to cache and rename it to current date and time
    let fm = FileManager.default
    let filename = { () -> String in
        let df = DateFormatter()
        df.dateFormat = "yyyy.MM.dd_HH.mm.ss"
        return "any_counter_2.backup.at.\(df.string(from: Date())).realm"
    }()
    let cacheFolderURL = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let destinationURL = URL(fileURLWithPath: filename, isDirectory: false, relativeTo: cacheFolderURL)
    
    do {
        try fm.copyItem(at: sourceURL, to: destinationURL)
    } catch {
        return
    }

    // export
    let activityViewController = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
    activityViewController.completionWithItemsHandler = { (_, isCompleted, _, activityError) -> Void in
        guard activityError == nil || (activityError! as NSError).code == 3072 else { // Error Domain=NSCocoaErrorDomain Code=3072 \"操作已被取消。\"
            fatalError("\(activityError!)")
        }

        // clear cache after success
        try? fm.removeItem(at: destinationURL)
    }
    present(activityViewController, animated: true, completion: nil)
}

这个问题花费了我不少时间去寻找解决方案。最后我突发奇想解决了。实际上,就是和URL有关。将12行

let destinationURL = URL(fileURLWithPath: filename, isDirectory: false, relativeTo: cacheFolderURL)

替换为

let destinationURL = URL(fileURLWithPath: cacheFolderURL.path + "/" + filename, isDirectory: false)

,问题解决。

类似的事情很早就遇到过,比如

import Foundation

let folder = "/foo"
let file = "bar"

let folderURL = URL(fileURLWithPath: folder, isDirectory: true)
let u1 = URL(fileURLWithPath: file, isDirectory: false, relativeTo: folderURL)
print(u1.path) // /foo/bar

let u2 = URL(fileURLWithPath: folder + "/" + file, isDirectory: false)
print(u2.path) // /foo/bar

print(u1 == u2) // false
print(u1.path == u2.path) // true

即两个path相同的URL,仅仅是因为构造方法的不同,它们就不相等。这其实是很不直观的,人们在没遇到之前,可能就会认为path相等的两个URL就是相等的,但是实际上系统认为不是。

这里也是这样,通过relativeTo构造的URL,iOS系统仅能将它存成一个纯文本。但是直接构造的URL就没有这个问题。

我虽然不认同,但是只能记住。

Realm的坑(四)

RealmSwift

之前的三个坑都是很久远的事情了。今天要填一个新坑。

我们知道,Realm.objects(_)返回的值是Results<T>,由于Realm自身的特性,Results<T>是很有用的,它是lazy动态的。因此,在使用Realm的时候,我们更喜欢使用Results<T>而不是Array

但是Results<T>有一个问题,就是它的排序功能十分有限。它最基本的排序只有一个函数,即sorted(byKeyPath:ascending:)。也就是说,Results<T>只能按照Realm对象的动态属性排序,而不能使用我们经常用到的closure排序。

分析

这就使得我们在有些时候,为了排序的方便,要么增加磁盘占用,添加额外的动态属性;要么,就只能使用Array类型。

值得一提的是,有时,如果是涉及到当前时间这一类的排序变量,即便我们使用额外的动态属性,也是不能排序的。因为这个值一直在变。因此就只能使用Array类型。

didSet不执行的问题

特别的,我们需要注意,在Realm的动态属性中,didSet是不会执行的,这应该是和Objective-C的运行时相关。因此,如果需要使用didSet,就需要改成其它的方式。

参考资料

相关

Realm模型升级(二)

RealmSwift

Realm模型升级(一)中讲了对象添加了新属性之后要如何升级。最近我又遇到了新的问题,要将对象改名。

新问题

因为业务的需要,必须将代码

class Item:Object {
    static let calendar = Calendar(identifier: .gregorian)
    
    @objc dynamic var title = ""
    @objc dynamic var addedDate:Date = Date()
    @objc dynamic var relatedItem:Item? = nil
    let records = List<Record>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class Record:Object {
    static let dateFormatter:DateFormatter = {
        let df = DateFormatter()
        df.locale = Locale(identifier: "zh")
        df.dateStyle = .medium
        df.timeStyle = .none
        
        return df
    }()
    
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

更改为

class ACItem:Object {
    static let calendar = Calendar(identifier: .gregorian)
    
    @objc dynamic var title = ""
    @objc dynamic var addedDate:Date = Date()
    @objc dynamic var relatedItem:ACItem? = nil
    let records = List<ACRecord>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class ACRecord:Object {
    static let dateFormatter:DateFormatter = {
        let df = DateFormatter()
        df.locale = Locale(identifier: "zh")
        df.dateStyle = .medium
        df.timeStyle = .none
        
        return df
    }()
    
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

即原来的类的名称之前,需要添加AC字样。更改了代码之后运行,发现数据不见了。

分析

打开数据库文件查看(如图)。原来,虽然代码中的类变了,但是数据库中的类还是原来的名字,需要手动迁移。

realm database

解决方案

23-39行,利用之前的Item对象,生成ACItem对象;利用Item对象中的records属性,生成ACRecord对象。需要注意的事,因为代码中此时已经没有了Item类和Record类,我们没法使用Item.className(),而只能直接使用"Item"

41-43行,删除掉旧数据。

let config:Realm.Configuration = {
    // update realm
    let config = Realm.Configuration(
        fileURL: destinationURL,
        
        // Set the new schema version. This must be greater than the previously used
        // version (if you've never set a schema version before, the version is 0).
        schemaVersion: 3,
        
        // Set the block which will be called automatically when opening a Realm with
        // a schema version lower than the one set above
        migrationBlock: { migration, oldSchemaVersion in
            // We haven’t migrated anything yet, so oldSchemaVersion == 0
            if (oldSchemaVersion < 1) {
                // Nothing to do!
            }
            
            if (oldSchemaVersion < 2) {
                // Nothing to do!
            }
            
            if (oldSchemaVersion < 3) {
                migration.enumerateObjects(ofType: "Item") { (oldObject, _) in
                    let acItem = migration.create(ACItem.className())
                    acItem["title"] = oldObject!["title"]
                    acItem["addedDate"] = oldObject!["addedDate"]
                    
                    guard let list = acItem["records"] as? List<MigrationObject>,
                        let oldList = oldObject!["records"] as? List<MigrationObject> else {
                        fatalError()
                    }
                    
                    oldList.forEach { o in
                        let acRecord = migration.create(ACRecord.className())
                        acRecord["id"] = o["id"]
                        acRecord["addedDate"] = o["addedDate"]
                        list.append(acRecord)
                    }
                }
                
                // delete
                migration.deleteData(forType: "Item")
                migration.deleteData(forType: "Record")
            }
    })
    
    return config
}()

总结

  1. 代码中对象的改名,意味着数据库中数据的迁移。
  2. MigrationObjectEnumerateBlock中的oldObject,实际上一个词典,我们通过键来进行数据的查找。

深入研究RealmSwift的通知

RealmSwift

RealmSwift官方文档在介绍通知时,简单易懂。在一般情况下,我们只需要要使用官方文档的代码就足够使用。但是,如果我们有更高的追求,就需要深入研究了。官方的代码:

class ViewController: UITableViewController {
    var notificationToken: NotificationToken? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        let realm = try! Realm()
        let results = realm.objects(Person.self).filter("age > 5")

        // Observe Results Notifications
        notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
            guard let tableView = self?.tableView else { return }
            switch changes {
            case .initial:
                // Results are now populated and can be accessed without blocking the UI
                tableView.reloadData()
            case .update(_, let deletions, let insertions, let modifications):
                // Query results have changed, so apply them to the UITableView
                tableView.beginUpdates()
                tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                     with: .automatic)
                tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.endUpdates()
            case .error(let error):
                // An error occurred while opening the Realm file on the background worker thread
                fatalError("\(error)")
            }
        }
    }

    deinit {
        notificationToken?.invalidate()
    }
}

其中.update(_, let deletions, let insertions, let modifications):中的三个变量,其实是相互关联的。根据苹果的文档performBatchUpdates(_:completion:)

Deletes are processed before inserts in batch operations. This means the indexes for the deletions are processed relative to the indexes of the table view’s state before the batch operation, and the indexes for the insertions are processed relative to the indexes of the state after all the deletions in the batch operation.

在批量操作中,删除优先于插入。这意味着删除的索引是基于批量操作之前的表格状态,而插入的索引则基于所有删除操作完成之后的状态。

问题来了。苹果告诉我们,在批量操作时,删除优先于插入,先删除,后插入。但是实际上还有更新操作,它的顺序又在哪里呢?

测试代码:

import UIKit
import RealmSwift

class TableViewController: UITableViewController {
    var items:Results<Item>!
    var token:NotificationToken? = nil

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            let realm = try Realm()
            items = realm.objects(Item.self).sorted(byKeyPath: "id")
            
            token = items.observe{ [weak self] (changes: RealmCollectionChange) in
                guard let tableView = self?.tableView else { return }
                switch changes {
                case .initial:
                    // Results are now populated and can be accessed without blocking the UI
                    tableView.reloadData()
                case .update(_, let deletions, let insertions, let modifications):
                    print("dels: \(deletions), ins: \(insertions), modifies: \(modifications)")
                    
                    // Query results have changed, so apply them to the UITableView
                    tableView.beginUpdates()
                    tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                         with: .automatic)
                    tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                         with: .automatic)
                    tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                         with: .automatic)
                    tableView.endUpdates()
                case .error(let error):
                    // An error occurred while opening the Realm file on the background worker thread
                    fatalError("\(error)")
                }
            }
        } catch let error {
            print(error)
        }
        
        DispatchQueue.main.async {
            self.addItems()
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
            self.addDeleteUpdate()
        }
    }
    
    deinit {
        token?.invalidate()
        token = nil
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item.id)
        cell.detailTextLabel?.text = item.title

        return cell
    }
}

extension TableViewController {
    func addItems() {
        do {
            let realm = try Realm()
            try realm.write {
                (0..<100).forEach {
                    let item = Item()
                    item.id = $0
                    item.title = String($0)
                    realm.add(item)
                }
            }
        } catch let error {
            print(error)
        }
    }
    
    func addDeleteUpdate() {
        do {
            let realm = try Realm()
            try realm.write {
                // add
                
                (100..<200).forEach {
                    let item = Item()
                    item.id = $0
                    item.title = String($0)
                    realm.add(item)
                }
                
                // delete
                stride(from: 20, to: 60, by: 2).forEach {
                    let item = realm.object(ofType: Item.self, forPrimaryKey: $0)
                    realm.delete(item!)
                }
                
                // update
                (80..<120).forEach {
                    let item = realm.object(ofType: Item.self, forPrimaryKey: $0)!
                    item.title = String(item.id + 1000)
                }
            }
        } catch let error {
            print(error)
        }
    }
}

应用启动之后,我们先插入100个数据,然后执行插入、删除和更新的操作。然后我们打印.update(_, let deletions, let insertions, let modifications):的数据:

dels: [20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58], ins: [80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], modifies: [80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

我们可以看到,删除的索引是和原始顺序一致的。插入的索引则是基于删除,进行了移动。最后更新的部分,也是和原始顺序一致的,并且我们可以看到,它的后面的部分,和插入的索引进行了合并,所以只有一半。

结论

当进行批量操作时,执行的顺序是,先执行修改、之后是删除、最后是插入。

Realm模型升级(一)

RealmSwift

所谓模型升级,是指数据库存储的数据,随着需求的变化,产生了格式的改变。这个时候,由于数据库里有数据,直接修改模型并运行程序,程序就会崩溃,原因是模型发生了变化。

Realm本身提供了模型升级的功能。官方文档的实例也简单易懂。

https://realm.io/docs/swift/latest#migrations

不过,今天我遇到一个特殊情况,发现需要在升级的时候使用List对象,是不能直接转换的,摸索之后找到解决方案。先看问题。

问题

已有代码

class Item:Object {
    @objc dynamic var title = ""
    let records = List<Record>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class Record:Object {
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

新需求需要在Item中添加一个新的属性@objc dynamic var addedDate:Date = Date()

class Item:Object {
    @objc dynamic var title = ""
    @objc dynamic var addedDate:Date = Date()
    let records = List<Record>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class Record:Object {
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

升级代码如下:

// update realm
let config = Realm.Configuration(
    // Set the new schema version. This must be greater than the previously used
    // version (if you've never set a schema version before, the version is 0).
    schemaVersion: 1,
    
    // Set the block which will be called automatically when opening a Realm with
    // a schema version lower than the one set above
    migrationBlock: { migration, oldSchemaVersion in
        // We haven’t migrated anything yet, so oldSchemaVersion == 0
        if (oldSchemaVersion < 1) {
            // Nothing to do!
            // Realm will automatically detect new properties and removed properties
            // And will update the schema on disk automatically
            
            migration.enumerateObjects(ofType: Item.className()) { oldObject, newObject in
                newObject!["addedDate"] = (oldObject!["records"] as! List<Record>).first?.addedDate ?? Date()
            }
        }
})

// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config

// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let _ = try! Realm()

运行时程序出错,提示不能将List<DynamicObject>转换为List<Record>

解决

经过查找资料发现,需要将其转化为List<MigrationObject。代码如下。究其原因,应该是Realm实例还没有创建,任何Realm的对象都是不可用的。(RecordObject子类)

// update realm
let config = Realm.Configuration(
    // Set the new schema version. This must be greater than the previously used
    // version (if you've never set a schema version before, the version is 0).
    schemaVersion: 1,
    
    // Set the block which will be called automatically when opening a Realm with
    // a schema version lower than the one set above
    migrationBlock: { migration, oldSchemaVersion in
        // We haven’t migrated anything yet, so oldSchemaVersion == 0
        if (oldSchemaVersion < 1) {
            // Nothing to do!
            // Realm will automatically detect new properties and removed properties
            // And will update the schema on disk automatically
            
            migration.enumerateObjects(ofType: Item.className()) { oldObject, newObject in
                newObject!["addedDate"] = (oldObject!["records"] as? List<MigrationObject>)?.first?["addedDate"] as? Date ?? Date()
            }
        }
})

// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config

// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let _ = try! Realm()

相关文章

Realm模型升级(二)

iOS项目CocoaPods的安装与使用

SwiftRealmSwiftXcode

安装

网上看了好几篇文章,综合起来才成功,这个是在macOS Sierra 10.12.4 (16E195)下正确安装CocoaPods的步骤:

1. 替换源为镜像

gem sources -l

查看当前的gem源的地址,由于墙的缘故,如果源是https://rubygems.org/,我们需要将它删除。

gem sources --remove https://rubygems.org/

然后替换为ruby china的镜像。

gem sources --add https://gems.ruby-china.org/

确认源替换是否成功

gem sources -l

2. 升级gem

系统自带的gem版本较低,使用时会出现莫名其妙的问题,因此要把它升级到最新版

sudo gem update --system

如果上面代码提示没有权限,改成

sudo gem update -n /usr/local/bin --system

3. 安装cocoapods

在macOS 10.11和10.12中安装时,安装到/usb/bin会提示错误,因此需要安装到/usb/local/bin里

sudo gem install -n /usr/local/bin cocoapods

4. 替换源

默认的源更新起来很慢,需要替换才能变快。如果你想使用默认的,可以跳过这一步。

查看当前的源:

pod repo 

删除master:

pod repo remove master

添加清华的源:

git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git ~/.cocoapods/repos/master

更新源:

pod repo update

5. 更新pod

如果你做过上一步,可以跳过这一步。

pod setup

这步时间较长,需要耐心等待。

使用

如果你使用了第4步的替换源,那么需要在你的每个Podfile的最前面,添加一行。

source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
  1. 新建一个iOS的项目。
  2. 在终端,进入到该项目的文件夹。
  3. 执行pod init
  4. 打开自动生成的Podfile文件
  5. 在其中添加你要使用的框架的名称
  6. 保存好Podfile文件
  7. 执行pod install
  8. 在Xcode中,关闭你的项目
  9. 在文件夹,找到pod生成的.xcworkspace文件,打开它。
  10. 像平常一样使用框架就可以了。

比如我新建的项目叫PodTest,添加了RealmSwift,最终的Podfile如下:

这其中大部分的内容都是pod自动生成的,我需要修改/添加的只有第三行,iOS的版本。第四行,去除警告。以及pod 'RealmSwift'的那一行。

最后的那段,是RealmSwift网站要求的。你需要什么框架,就到那个框架的官网,按照提示复制粘贴上去就可以了。

最后记得,每次修改完Podfile之后,都要记得运行一遍pod install

# Uncomment the next line to define a global platform for your project
platform :ios, '10.3'
inhibit_all_warnings!

target 'PodTest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!
  pod 'RealmSwift'

  # Pods for PodTest

  target 'PodTestTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'PodTestUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = '3.1'
    end
  end
end

小技巧

  1. 第四行的inhibit_all_warnings!,可以使得Xcode不对框架中不当的帮助说明生成警告。因为框架是第三方的,我们即便看到了,也不会修改它。
  2. 有时会由于网络问题导致下载的框架不全,此时可以使用这个命令重新下载。
pod deintegrate && pod install

参考资料

Realm的坑(三)

RealmSwift

上一个坑里,Realm的坑(二),我们使用非受管对象避开必须使用写入交易的问题。但是,每次使用时都要设置属性还是挺麻烦的。我们寻求一种可以一劳永逸的方式。

最初的想法

我最初想到的是利用NSCopying协议,然后利用Object对象的properties属性给Object实例的属性赋值。如:

extension Object:NSCopying {
    public func copy(with zone: NSZone? = nil) -> Any {
        let o = Object()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        return o
    }
}

由于NSCopying的返回值是Any,因此,在使用时需要转换,像这样:

let anotherBar = bar.copy() as! Foo
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

但是这里发生了一个问题。当前版本的Realm中有一个错误,不能在swift中直接创建Object(),程序会崩溃。因此我提交了一个issue

进阶

既然不能直接使用Object(),我决定使用通用类型的函数,像这样:

func unmanagedCopy<T>(of i:T) -> T where T:Object {
    let o = T()
    for p in i.objectSchema.properties {
        let value = i.value(forKey: p.name)
        switch p.type {
        case .linkingObjects:
            break
        default:
            o.setValue(value, forKey: p.name)
        }
    }
    
    return o
}

let anotherBar = unmanagedCopy(of: bar)
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

这么做,带来一个好处,就是我在具体使用的时候不用进行类型转换了。缺点就是这是一个全局函数,也许放到一个struct里会更好一些?

最终的解决方案

在上面提单的issue里,**JadenGeller**给了我两个很好的建议。下面的代码是最终的解决方案。

// MARK: - UnmanagedCopy Protocol
protocol UnmanagedCopy {
    func unmanagedCopy() -> Self
}

extension Object:UnmanagedCopy{
    func unmanagedCopy() -> Self {
        let o = type(of:self).init()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        return o
    }
}

首先建立一个UnmanagedCopy的协议。虽然直接写这个函数也可以,但是建立协议可以使函数的目的更加明确。

然后在Object对象的扩展里实现了这个协议。这里用let o = type(of:self).init()避开了不能使用Object()的问题,变量o的类型是Self。函数返回值类型Self可以确保最终的类型与self的实际类型相同,这就保证了使用时不必再进行二次转换。具体使用:

let anotherBar = bar.unmanagedCopy()
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

这个函数已经基本够用了,它包含你的实例里所有持久性数据的属性,但是不包括ignore函数里包含的属性public class func ignoredProperties() -> [String]。所以,如果你有额外的需求,就应该在Object的子类里重写这个函数。如:

class Foo:Object {
    dynamic var id:Int = 0
    dynamic var name:String = ""
    dynamic var age:Int = 8
    
    var temp = "temp value"
    
    override class func ignoredProperties() -> [String] {
        return ["age", "temp"]
    }
    
    override class func primaryKey() -> String? {
        return "id"
    }
    
    override func unmanagedCopy() -> Self {
        let o = type(of:self).init()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        o.age = age
        o.temp = temp
        
        return o
    }
}

有关Self的更多用法,看Self的用法

相关

Realm的坑(一)
Realm的坑(二)
Realm的坑(三)
Realm的坑(四)

Realm的坑(二)

RealmSwift

managed与unmanaged

我们都知道,变量都有其生命周期。当存在变量的强引用时,变量会一直存在。比如,有

struct Foo {
   var name:String
}
let bar = Foo(name:"John")

bar在赋值后一直存在。我们始终都可以使用bar.name来获得name属性的值John。但是在Realm中一切都变得不一样了。在Realm中,你通过子类继承Object对象来构建你的Model。然后通过Realm.add来添加这个子类的实例到Realm中。

class Foo:Object {
    dynamic var name:String = ""
}
let bar = Foo()

let realm = try! Realm()
try! realm.write {
	realm.add(bar)
}

你新建出来的bar是unmanaged的实例。realm.add(bar)之后,bar就是managed的实例了。一旦受管,该实例的用法就不能再那么随心所欲了,之后对它的操作就必须在Realm的交易里完成。如:

let bar = Foo()
bar.name = "Marry" // ok

let realm = try! Realm()
try! realm.write {
	realm.add(bar)
	bar.name = "Kelly" // ok
}

bar.name = "Jimmy" // *** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'

为什么name被设置成Marry和Kelly的时候没问题,但是一设置成Jimmy就不行了呢?

  1. 这是因为bar的状态发生了改变。在realm.add()之前,bar是非受管的状态,此时bar在Realm数据库中没有记录。它的行为和一般的类的实例类似,因此可以直接设置name
  2. realm.add()之后,bar在数据库中有了记录,变成了受管的状态,此时再更改它,就必须在realm的写入交易中完成。就像Kelly那样。
  3. Jimmy的部分,由于没有在写入交易中完成,程序会抛出异常并终止运行。

如果不想添加额外的写入交易,就需要添加新建一个非托管对象,然后在添加它到Realm,如:

let anotherBar = Foo()
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar)
}

这样的代码有一点儿问题。anotherBarbar对应的是两个不同的对象。现在数据库中同时存在baranotherBar对应的对象了。因此我们需要删除bar

let anotherBar = Foo()
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar)
    realm.delete(bar)
}

这很不环保。对于有主键的类的实例,可以使用add(object: Object, update: true),比如:

class Foo:Object {
    dynamic var id:Int = 0
    dynamic var name:String = ""
    
    override class func primaryKey() -> String? {
        return "id"
    }
}

let bar = Foo()
bar.id = 1
bar.name = "Marry" // ok

let realm = try! Realm() 
try! realm.write {
    realm.add(bar)
    bar.name = "Kelly" // ok
}
  
let anotherBar = Foo()
anotherBar.id = 1
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

每次还要重新设置非受管对象还是挺麻烦的,如果能直接生成一个就好了。这就是第三个坑。Realm的坑(三)

相关

Realm的坑(一)
Realm的坑(二)
Realm的坑(三)
Realm的坑(四)