SwiftUI如何避免导航硬编码?大型生产级应用架构问询
我正在为一款大型、可投入生产的SwiftUI应用设计架构,却反复卡在同一个问题上——这暴露出SwiftUI的一处重大设计缺陷。至今仍没人能给出一套完整可用、适用于生产环境的解决方案:如何在SwiftUI中实现包含导航功能的可复用视图?
由于SwiftUI的NavigationLink与视图强绑定,这种方式根本无法在大型应用中规模化落地。NavigationLink在小型示例里用着没问题,但一旦想在单个应用里复用多个视图,甚至跨模块(比如iOS、WatchOS等平台复用视图),麻烦就来了。
设计痛点很明确:NavigationLink被硬编码到视图中,比如写死NavigationLink(destination: MyCustomView(item: item))。但如果包含这个NavigationLink的视图需要复用,就不能硬编码目标视图,必须有一套机制来动态提供目标视图。
我之前就这个问题提问过,得到了一个看似可行的方向——通过注入目标链接到可复用视图中(参考SwiftUI MVVM Coordinator/Router/NavigationLink的思路)。这个想法大体能走通,但遗憾的是没法扩展到实际生产应用里。当我有多个可复用页面时,逻辑会陷入死循环:比如可复用视图ViewA需要预配置好目标视图ViewB,可ViewB又需要预配置好目标视图ViewC,那我得先给ViewB注入ViewC,再把ViewB注入ViewA,以此类推。但这时候需要传递的数据还没准备好,整个结构直接失效。
我还试过用Environment作为依赖注入机制来注入NavigationLink的目标视图,但这更像是临时凑活的权宜之计,根本不是适合大型应用的可扩展方案。到最后我们会把Environment用在所有场景,而且Environment只能在视图内部使用(没法在独立的Coordinator或ViewModel里用),这会导致结构混乱——毕竟业务逻辑(比如ViewModel代码)要和视图分离,导航也得和视图分离(比如用Coordinator模式)。
在UIKit里这事儿是能解决的,因为我们能访问视图背后的UIViewController和UINavigationController。UIKit的MVC模式本来就有概念混杂的问题,甚至被戏称为“Massive-View-Controller(巨型视图控制器)”而非正经的“Model-View-Controller”。现在类似的问题在SwiftUI里不仅延续了,在我看来还更严重:导航与视图强耦合,完全没法解耦。所以带导航功能的视图根本没法复用。UIKit里能解决的问题,在SwiftUI里我找不到合理的方案。遗憾的是苹果从来没给出过这类架构问题的官方解决方案,只给了一些小型示例应用。我真心希望有人能证明我错了,展示一套适用于大型生产级应用的清晰设计模式。
后续更新
2020年6月(赏金到期前)
赏金快到期了,遗憾的是还是没人能给出可行的示例。如果找不到其他解决方案,我会发起新的赏金并附上链接。感谢所有人的贡献!
2020年6月18日(苹果官方回复)
我收到了苹果关于此问题的回复,他们提出了一套解耦视图与模型的方案:
enum Destination { case viewA case viewB case viewC } struct Thing: Identifiable { var title: String var destination: Destination // … 省略其他内容 … } struct ContentView { var things: [Thing] var body: some View { List(things) { NavigationLink($0.title, destination: destination(for: $0)) } } @ViewBuilder func destination(for thing: Thing) -> some View { switch thing.destination { case .viewA: return ViewA(thing) case .viewB: return ViewB(thing) case .viewC: return ViewC(thing) } } }
我的回复是:
感谢反馈。但如你所见,视图中仍存在强耦合问题。现在ContentView需要知晓所有可导航到的视图(ViewA、ViewB、ViewC)。正如我所说,这在小型示例应用中可行,但无法扩展到大型生产级应用。想象一下,我在GitHub的一个项目中创建了一个自定义视图,然后将其导入我的应用中。这个自定义视图对其可导航到的其他视图一无所知,因为这些视图是我的应用所特有的。我希望我把问题解释得更清楚了。我认为解决此问题的唯一清晰方案是像UIKit那样分离导航与视图(例如使用UINavigationController)。谢谢,Darko
所以这个问题依然没有清晰可行的解决方案,只能期待WWDC 2020能带来转机。
2021年9月更新
使用AnyView并不是解决这个问题的通用好方案。在大型应用里,基本上所有视图都得设计成可复用的,这意味着AnyView会被到处用。我曾和两位苹果开发者交流过,他们明确告诉我AnyView的性能远不如原生View,只能在特殊情况下使用。根本原因是AnyView的类型没法在编译时解析,必须在堆上分配,这会带来额外的性能开销。
2022年6月更新(WWDC带来转机)
苹果在WWDC上推出了全新的SwiftUI NavigationStack。NavigationStack通过使用.navigationDestination修饰符,允许将目标视图与当前可见视图分离,终于实现了一套清晰的Coordinator模式。感谢苹果倾听用户的需求!
内容的提问来源于stack exchange,提问作者Darko

