酱酱不惊

Break my life, rebuild it

0%

SwiftUI中的NavigationStack是iOS 16引入基于数据类型驱动的导航视图。通过将数据类型与视图关联起来,提供了类型安全的使用方式。本文并非介绍NavigationStack基本使用的文章,故这里就不多做介绍了。

在实际应用中,通过编码的方式维护导航视图栈是非常常见的行为,比如:打开不同的视图;退出部分视图等。虽然在官方的NavigationStack示例中,也提供了维护导航视图路径的方法,但仅能支持一些简单的场景。另外,在使用上也存在一些局限性。整体的可操作性与UIKit中的UINavigationController有些差距。

下面先来看看示例文档中路径绑定的例子。

NavigationStack的路径绑定方法

当需要维护多个视图界面时,通过在初始化方法中传入绑定path的数组,然后通过对数组的操作来完成视图的导航,比如:给数组插入元素或者删除元素,即完成界面的打开和退出。

1
2
3
4
5
6
7
8
9
10
@State private var presentedParks: [Park] = []

NavigationStack(path: $presentedParks) {
List(parks) { park in
NavigationLink(park.name, value: park)
}
.navigationDestination(for: Park.self) { park in
ParkDetails(park: park)
}
}

这个示例中的问题是只能支持打开相同的界面,因为绑定的数组容器元素是一个具体的类型。

为了解决这个问题,需要使用到类型无关的列表容器NavigationPath,初始化path参数也支持传入绑定NavigationPath。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@State private var presentedPaths = NavigationPath()

NavigationStack(path: $presentedPaths) {
List {
ForEach(parks) { park in
NavigationLink(park.name, value: park)
}
ForEach(zoos) { zoo in
NavigationLink(zoo.name, value: zoo)
}
}
.navigationDestination(for: Park.self) { park in
ParkDetails(park: park)
}
.navigationDestination(for: Zoo.self) { zoo in
ZooDetails(park: zoo)
}
}

NavigationPath也支持简单的数据添加和删除元素的等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// Appends a new value to the end of this path.
public mutating func append<V>(_ value: V) where V : Hashable

/// Appends a new codable value to the end of this path.
public mutating func append<V>(_ value: V) where V : Decodable, V : Encodable, V : Hashable

/// Removes values from the end of this path.
///
/// - Parameters:
/// - k: The number of values to remove. The default value is `1`.
///
/// - Precondition: The input parameter `k` must be greater than or equal
/// to zero, and must be less than or equal to the number of elements in
/// the path.
public mutating func removeLast(_ k: Int = 1)

日常使用中的问题

虽然上述2种方法提供了维护视图栈的方法,但在实际使用过程中还是会有一些小问题:

  1. 总是需要创建一个类型来关联视图。比如:某些静态界面或者本来就不需要传入参数的视图。
  2. 类型与单个视图绑定。比如:多个视图接收相同的模型参数。
  3. 在视图中无法直接获取绑定的导航数据列表容器。
  4. NavigationPath中提供的容器操纵方法不够。

基于枚举的路径绑定

Swift中的枚举非常强大,跟Objective-C和C中的完全不是一种东西。甚至可以说如果不会使用枚举,就根本不了解Swift。这篇文章非常建议阅读:Swift 最佳实践之 Enum

首先,我们通过枚举来表示应用中的视图类型,结合嵌套的枚举来表示子级的视图类型。另外,通过关联值来传递子视图或视图的入参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum AppViewRouter: Hashable {

enum Category: Hashable {
case list
case detail(Int)
}

case profile
case category(Category)

var title: String {
switch self {
case .profile:
return "Profile"
case .category(let category):
switch category {
case .list:
return "Category List"
case .detail(let id):
return "Category Detail: \(id)"
}
}
}
}

在NavigationStack的初始化方法中,通过包含视图枚举的数组来进行绑定。在一个navigationDestination中通过不同的枚举来完成对不同的视图创建,通过编译器,也可以帮助我们不会遗漏没有处理的视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct ContentView: View {

@State
private var presentedRouters: [AppViewRouter] = []

var body: some View {
NavigationStack(path: $presentedRouters) {
LinkView(title: "Home")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: AppViewRouter.self) { park in
switch park {
case .profile:
LinkView(title: park.title)
case .category(let category):
switch category {
case .list:
LinkView(title: park.title)
case .detail(let id):
LinkView(title: park.title)
}
}
}
}
.environment(\.myNavigationPath, $presentedRouters)
}
}

为了能够在子视图中操作当前的导航栈,我们创建了一个环境Binding值,将当前的绑定的枚举数组注入到环境中。

1
2
3
4
5
6
7
8
9
10
11
12

private struct MyNavigationPathKey: EnvironmentKey {
static let defaultValue: Binding<[AppViewRouter]> = .constant([AppViewRouter]())
}

extension EnvironmentValues {
var myNavigationPath: Binding<[AppViewRouter]> {
get { self[MyNavigationPathKey.self] }
set { self[MyNavigationPathKey.self] = newValue }
}
}

在LinkView中,我们获取绑定路径的环境Binding值,通过对路径数据的添加或删除操作,以实现导航栈的控制。当然,也可以使用NavigationLink继续打开新的视图,以一种类型安全并且带有层级结构的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct LinkView: View {

let title: String

@Environment(\.myNavigationPath) var customValue

var body: some View {
VStack(alignment: .leading, spacing: 20) {
NavigationLink("Go Profile", value: AppViewRouter.profile)
NavigationLink("Go Category List", value: AppViewRouter.category(.list))
Button("Go Category Detail") {
customValue.wrappedValue.append(AppViewRouter.category(.detail(999)))

}
Button("Back") {
if !customValue.wrappedValue.isEmpty {
customValue.wrappedValue.removeLast()
}
}

Button("Back to Root") {
customValue.wrappedValue.removeAll()
}
}
.padding()
.navigationTitle(title)
}

}

总结

通过上述的例子我们可以看到,使用枚举来定义应用的视图层级是一种非常的好的方式,而关联值也很好的解决了视图的入参问题。将绑定的视图数组注入到环境中,也能让子视图方便的控制整个界面的导航。

整个过程不需要涉及新的内容,都是一些在SwiftUI开发中的常见的部分。但在使用体验上要比之前好得多。

完整Demo地址:Github地址

文中谈到的问题与Core Data有关,故并不局限于SwiftUI应用或不启用CloudKit的场景。

在开发SwiftUI应用时,结合Core Data with CloudKit来提供跨平台数据同步的是个非常不错的选择。当通过Xcode创建使用Core Data with CloudKit新项目时,会提供一些模板代码,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct PersistenceController {
static let shared = PersistenceController()

let container: NSPersistentCloudKitContainer

init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataDemo")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
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.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}

示例代码不多,但提供了一些方便的功能,可以支持应用的初期开发。

PersistenceController中的问题

上述示例中代码存在一些潜在的问题及职责不清晰,随着项目的不断迭代,会逐渐变得更加臃肿及无法扩展。下面我们来分析一下存在的问题。

Swift的单例问题

PersistenceController提供了一个shared的单例属性定义,但PersistenceController本身是一个结构体,即一个值类型。而值类型在赋值给一个变量、常量或者传递给函数的时候会产生一个拷贝,这就破坏了单例模式的设计初衷。

所以在Swift中创建单例时,正常的用法应该是使用引用类型进行声明。

1
2
3
class PersistenceController {
...
}

或者使用actor进行声明,关于Actor的介绍可以参考:Swift 新并发框架之 actor

1
2
3
actor PersistenceController {
...
}

职责过多的初始化方法

init(inMemory: Bool = false)中的代码耦合严重,处理了许多事情。在实际使用中,还会有更多的代码加入进来,整个代码变得更加复杂。问题如下:

  1. 以硬编码的方式创建NSPersistentCloudKitContainer实例。
  2. 通过inMemory参数修改NSPersistentCloudKitContainer的存储行为。
  3. 加载完毕后对NSPersistentCloudKitContainer实例进行配置。

虽然上述问题对这个示例来说无所谓,但在实际应用中会是个非常糟糕的例子。在我的开发过程中,还会碰到了以下需求:

  1. 修改数据库的存储路径到App Group。
  2. 定制不同行为的NSPersistentCloudKitContainer的实例。
  3. 将模型相关的代码转换成SPM,用于在主应用和extension中复用。
  4. 更方便的进行单元测试。

fatalError

虽然模板代码中已经加了注释,需要替换成合适的处理方法。但在开发过程中极少会触发到这个错误,导致容易被忽视。建议的做法是通过Debug宏进行隔离处理,避免线上启动就闪退的可能性。

重构PersistenceController

创建不同的NSPersistentCloudKitContainer的子类是个不错的选择,将对实例的配置及相关方法封装到特定的子类中。这也是苹果推荐的一种方式:Subclass the Persistent Container

为了更灵活的创建不同的NSPersistentCloudKitContainer实例,在初始化方法中通过配置信息来完成NSPersistentCloudKitContainer实例的创建。

优化init方法

首先,创建一个包含初始化配置信息的结构体。这个结构体中包含了NSPersistentCloudKitContainer的类型信息,允许从外部注入需要创建的子类类型。另外,也支持从外部传入NSManagedObjectModel对象,当将Core Data的相关代码模块化后,通常需要从非mainBundle进行加载,故可以自行创建NSManagedObjectModel实例后,传递到NSPersistentCloudKitContainer的初始化过程中。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
final class MyPersistenceController {

struct Configuration {

let containerClass: NSPersistentCloudKitContainer.Type

let name: String

let managedObjectModel: NSManagedObjectModel?

init(containerClass: NSPersistentCloudKitContainer.Type, name: String, managedObjectModel: NSManagedObjectModel? = nil) {
self.containerClass = containerClass
self.name = name
self.managedObjectModel = managedObjectModel
}

}

let container: NSPersistentCloudKitContainer

init(_ configuration: MyPersistenceController.Configuration) {
if let managedObjectModel = configuration.managedObjectModel {
container = configuration.containerClass.init(name: configuration.name, managedObjectModel: managedObjectModel)
} else {
container = configuration.containerClass.init(name: configuration.name)
}

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.
#if DEBUG
fatalError("Unresolved error \(error), \(error.userInfo)")
#else

#endif
}
})
}

}

创建NSPersistentCloudKitContainer的子类

针对CloudKit持久化和内存中持久化,创建各自不同的子类MyPersistentCloudKitContainer和MyInMemeryContainer。

Extension已经成为iOS开发中不可缺少的一环,为了能够在extension中访问Core Data的数据,需要修改默认保存的数据库文件目录,可在重写defaultDirectoryURL方法修改默认保存的路径。关于怎么通过App Group创建共享目录这里就不展开了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyPersistentCloudKitContainer: NSPersistentCloudKitContainer {

// override class func defaultDirectoryURL() -> URL {
// FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "xxx")!
// }

override func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Void) {
super.loadPersistentStores(completionHandler: block)

viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
viewContext.automaticallyMergesChangesFromParent = true
}

}

class MyInMemeryContainer: NSPersistentCloudKitContainer {

override init(name: String, managedObjectModel model: NSManagedObjectModel) {
super.init(name: name, managedObjectModel: model)

persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")

addPreviewData()
}

func addPreviewData() {
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
}

}

创建不同的静态属性

通过扩展的方式,创建不同的静态属性,可以根据实际的情况创建不同的NSPersistentCloudKitContainer配置。比如:可以提供一个专门用于在extension访问配置,禁用掉CloudKit相关的配置属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension MyPersistenceController {

static var managedObjectModel: NSManagedObjectModel = {
NSManagedObjectModel.mergedModel(from: [.main])!
}()

static var `default`: MyPersistenceController = {
let configuration: MyPersistenceController.Configuration = .init(
containerClass: MyPersistentCloudKitContainer.self,
name: "CoreDataDemo",
managedObjectModel: managedObjectModel)

return MyPersistenceController(configuration)
}()

static var preview: MyPersistenceController = {
MyPersistenceController(.init(containerClass: MyInMemeryContainer.self, name: "CoreDataDemo"))
}()

}

总结

通过在初始化方法中注入创建NSPersistentCloudKitContainer所需的参数,完成了对NSPersistentCloudKitContainer的强依赖。然后,通过创建不同的NSPersistentCloudKitContainer子类封装不同的配置。最终可以非常方便的创建不同的PersistenceController实例,应用到不同的场景中。

完整Demo代码: Github地址

CocoaPods是iOS平台上流行的第三方包管理工具。基本的介绍及使用方法可参考《唐巧的博客——用CocoaPods做iOS程序的依赖管理》,这里就不做介绍了。

在CocoaPods的使用过程中,会根据Podfile的内容,来抓取第三方库的源文件和生成其它相关的文件。这些文件均存放在Pods目录中,并通过生成的Pods.xcodeproj项目来进行管理。除此之外,还有几个关键的文件位于目录:Pods/Target Support Files/Pods-XXX/ 中,下面会讲到如何使用它们来解决我们标题中的问题。

问题:什么情况下需要共享Pods.xcodeproj ?

有很多iOS App会同时开发iPhone和iPad两个独立客户端,而不是开发单独的Universal客户端。这时候通常有两个.xcodeproj工程,虽然单个工程通过两个Target也能达到类似的效果,但这里不考虑这种情况。如果两个.xcodeproj工程可以共享Pods.xcodeproj,那么维护Pods会是非常的方便。

阅读全文 »

学习

在整个2015年里,自己花在读书上的时间很少,这个问题也一直持续到2016年上半年。在结束这1年的高强度工作后,终于有时间可以多看看书了。今年在还买了Kindle后,明显的提高了我看书的效率,可以有更多的碎片时间拿来利用。

2016年我的书单:《成为技术领导者》、《从优秀到卓越》、《人性的弱点》、《把时间当作朋友》、《人类简史:从动物到上帝》、《尼采:在世纪的转折点上》、《必然》、《引爆点》、《重新定义公司:谷歌是如何运营的》。

年度最喜欢的书非《人类简史:从动物到上帝》莫属了,其实《把时间当作朋友》我也非常喜欢。

除了看书,今年还报名了《混沌研习社》,成为付费会员,听了很多不同内容的分享。算是我在网上付费最大一笔的学习支出了,个人觉得还是非常值得的。

职业

在今年里,终于结束了1年在创业公司的工作。这段的经历给我深深的上了一课,“传统企业玩互联网是找死,不转是等死”,这话说的真没错,传统企业是做不好互联网的。这次的失败让我对不靠谱的公司有了更一步的理解。

这一整年里,自己的职业发展没有任何进步,这给了我非常大的紧迫感,我需要再次变革。

生活

2016年里上海房价疯狂上涨,幅度前所未闻,不少地方都直接翻翻了,让我非常的郁闷。为什么一个国家可以让这样不公平的事情一直发生?虽然,我大兲朝本来就是这样。

2016年将要过去了,我们又多相处了一年,也见了我们的父母,终于快要修成正果了,这算是我2016里最大的收获。

2017

2017年必然是我人生最重要的一个阶段,我必须投入更大的努力来证明我可以的。

最近这段时间发现自己对GTD的实践有所懈怠,所以想重读《尽管去做——无压工作的艺术》,后来改变主意还是想找另外一本讲GTD的书,结果找到了这一本《把时间当朋友》。由于之前在KnewOne上买过东西,立即对这本书有了好感。虽然4月份就买了,但一直到7月才开始阅读,然后一发不可收拾,这本书的名字太低调了。让我想起了电影《三傻大闹宝莱坞》,虽然名字很平常,但所展现的内容却是非同一般。

在书里从心理学、科学、哲学多个的角度来描述开启心智的重要性。这个重要性足以影响每个人的生活、工作、思想的方方面面。

通过各种日常生活中的例子,更深入一下展现这些例子背后的问题,而这些问题通常是我们不想去想、不敢去想或者根本就没有想过的。特别是一些很现实的问题,比如:绝大多数人都不“成功”。那所谓的“成功”究竟是什么意思呢?是这个社会定义的“成功”?还是你自己定义的“成功”?还是你所追求的只是别人的成功?

有意思的是,这个问题的答案,在我最近读的一本书《人类简史》里,可以找到一些线索。而且这本书里谈到了“自知”的问题,里面有句话:“平庸的人也有权利快乐”。

这本书里的内容是不能从别人那里理解的,开启心智只能是自己的事。附上摘抄的笔记:

阅读全文 »

无论你是在上班还是在创业,这本书都能帮助你预测公司的未来。书中提到的“3个训练有素”非常具有指导意义:

  1. 训练有素的人
  2. 训练有素的思想
  3. 训练有素的行为

更新笔记整理 2016.10.1

阅读全文 »