肇鑫的技术博客

业精于勤,荒于嬉

有关XCUITest的一些补充(一)

今天在编写项目截图的时候,遇到了好几个XCUITest的问题。解决之后,感觉这些问题应该算是蛮经典的,于是把它们记录下来,方便以后查阅。

最先遇到的是测试无法运行成功,提示有两个,一个是“Undefined symbols”,一个是“Linker command failed with exit code 1 (use -v to see invocation)”。

经过查看详细日志,发现是SPM(Swift Package Manager)的问题。当为应用目标时,SPM引入的第三方框架,如果该框架对于其它框架有依赖,那么SPM会自动导入该依赖的框架。但是在XCUITest的时候,或许是没有用到SPM,被依赖的框架并不会自动引用。因此,需要手动添加第三方框架之外,还需要手动再添加第三方框架依赖的框架。具体要添加多少,就要看错误日志提示的是哪个框架了。

解决了这个问题之后,测试终于可以通过。但是同时,又发生了一个新的问题。就是虽然Xcode显示Test成功了。但是却一直显示Testing,长久也没有测试完成。

经过查询,发现这也是Xcode一个bug。当使用XCUITest测试时,需要将并行测试的选项关闭,否则就会一直显示Testing。

还有一个注意事项,就是XCUITest测试时,必须将项目的目标设定为XCUITest这个目标,这个目标默认是隐藏的,必须手动添加出来。如果把应用作为目标,然后运行XCUITest,还是可能会出现一直显示Testing的问题。

最后,如果你需要检测文本,需要注意文本语言问题,将翻译的strings文件添加到XCUITest的目标,并不能自动调用并使用NSLocalizedString宏,因此,需要用或进行检测,像这样:

XCTAssertTrue(app.staticTexts["使用云端服务"].exists || app.staticTexts["Try Cloud"].exists)

参考

iCloud Key-Value同步nil值后,再次同步会出错问题的绕过

这个问题我已经向苹果报告了。FB13171112

具体的内容可以看我的这篇推文

临时的解决办法,就是不直接使用nil,而是把它封装起来。

// 之前
var foo:Int? = nil
// 现在
struct Bar:Codable {
    var foo:Int? = nil
}

这样就能避免直接使用nil了。缺点就是需要将它转换成Data再同步,多了几步。不过如果使用第三方框架的话,步骤其实不用多,第三方框架已经写好了。

Core Data融合升级时可能会遇到的问题

更新1

有关local store和cloud store的补充说明。

苹果文档讲述了Core Data融合的基本的方法。不过,它讲述的比较简略。代码好多部分都是占位符,需要自己填写。此外,苹果的融合,默认的单机模式,也就是没有使用CloudKit的同步的情况。如果你使用CloudKit,就会遇到苹果没有提到的情况。

Core Data Model Versioning and Data Migration

融合升级的步骤

融合升级的基本步骤:

  1. 融合
  2. 打开新数据库

不过我更建议:

  1. 查询是否能够融合
  2. 融合
  3. 打开新数据库

下面的文章基于后者。

1. 查询是否能够融合

private func canMigration() -> Bool {
    let sourceURL = Bundle.main.url(forResource: "Model", withExtension: "mom", subdirectory: "Model.momd")
    let destinationURL = Bundle.main.url(forResource: "Model 2", withExtension: "mom", subdirectory: "Model.momd")
    let sourceModel = NSManagedObjectModel(contentsOf: sourceURL!)
    let destinationModel = NSManagedObjectModel(contentsOf: destinationURL!)
    
    if let _ = try? NSMappingModel.inferredMappingModel(forSourceModel: sourceModel!, destinationModel: destinationModel!)  {
        return true
    }
    
    return false
}

注意:在Xcode中,创建模型的文件扩展名分别是“xcdatamodeld”和“xcdatamodel”,但是生成应用之后,它们的扩展名变成了“momd”和“mom”,并且前者变成了独立的文件夹。

xcode_model

这一步的作用是,提前知晓能否转换。如果这一步都不成,就不用执行下一步了。如果直接执行下一步,那么两部的错误可能会同时呈现,增加调试的难度。

2. 融合

private func migragtionFromModel2Model2() {
    let container = NSPersistentContainer(name: "Model")
    let moc = container.viewContext
    let applicationSupportFolderURL = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
#if targetEnvironment(macCatalyst)
    let storeURL = URL(fileURLWithPath: "Photo Organizer/Model.sqlite", isDirectory: false, relativeTo: applicationSupportFolderURL)
#else
    let storeURL = URL(fileURLWithPath: "Model.sqlite", isDirectory: false, relativeTo: applicationSupportFolderURL)
#endif
    let options:Dictionary<AnyHashable, Any>? = [
        NSMigratePersistentStoresAutomaticallyOption : NSNumber(value: true),
        NSInferMappingModelAutomaticallyOption: NSNumber(value: true),
        NSPersistentHistoryTrackingKey: NSNumber(value: true)
    ]
    
    if let _ = try? moc.persistentStoreCoordinator!.addPersistentStore(type: NSPersistentStore.StoreType.sqlite, configuration: "Local", at: storeURL, options: options) {
        print("success")
    } else {
        print("failed")
    }
}

此处需要注意两点:

  1. macCatalyst下的sqlite位置与iOS不同,前面多了一个应用名。(行11到15)
  2. 如果你之前使用过CloudKit同步,那么还需要多一个“ NSPersistentHistoryTrackingKey: NSNumber(value: true)”。(行20)否则数据库就会以只读方式打开,无法同步。

3. 打开新数据库

这个就是融合升级之前打开的方式。值得一提的是,如果你融合升级之后,还想要对于数据库的内容进行额外的处理。那么应该在这一步完成之后,进行处理。