Helm

SwiftUI Swift Xcode MIT

Helm 是一个声明式的、基于图的 SwiftUI 路由库。它完整地描述了应用中所有的导航流程,并且可以处理复杂的重叠 UI、模态框、深度链接以及更多。

索引

特性

概念

导航图

在 Helm 中,导航规则在图结构中定义,使用片段(fragments)和 Segues(转场)。片段是应用中的动态部分,有些是屏幕,另一些是重叠视图(例如音乐应用中的滑动播放器)。Segues 是有向边,用于指定两个片段之间的规则,例如呈现样式或 auto 标志(更多关于这些请参见下方)。

呈现路径

与传统的路由器不同,Helm 使用有序的边集合来表示路径。这允许查询已呈现的片段以及到达它们所需的步骤,同时支持多层 UI。路径还可以为每个片段分配一个可选的 ID。这些 ID 用于从同一片段呈现动态数据。(例如,在主-细节列表中,.detail 片段将需要当前呈现的项目的 ID。)

转场

转场封装了从一个片段到另一个片段的导航命令。在 Helm 中有 3 种类型的转场

Helm

Helm,主类,在片段之间导航,返回它们的呈现状态和所有可能的转场等等。它遵循 ObservableObject 协议,可以作为注入的 @EnvironmentObject 使用。

Segues(转场)

Segues 是片段之间带有导航规则的有向边

用法

涵盖您在 SwiftUI 应用中可能遇到的大多数场景的完整示例可以在这里找到。

我们首先定义应用中的所有片段。

enum Section: Fragment {
    // the first screen right after the app starts
    case splash

    // the screen that contains the login, register or forgot password fragments
    case gatekeeper
    // the three fragments of the gatekeeper screen
    case login
    case register
    case forgotPass
    
    // and so on ...
}

现在我们有

接下来是导航图。通常我们需要写下每个 segue。

let segues: Set<Segue<Section>> = [
    Segue(from: .splash, to: .gatekeeper),
    Segue(from: .splash, to: .dashboard),
    Segue(from: .gatekeeper, to: .login, auto: true)
    Segue(from: .gatekeeper, to: .register)
    //...
]

但这可能会变得非常冗长,所以,相反,我们可以使用有向边运算符 => 来定义所有边,然后将它们转换为 segues。由于 => 支持一对多、多对一和多对多的连接,我们可以用更少的代码行创建所有边。

let edges = Set<DirectedEdge<Section>>()
    .union(.splash => [.gatekeeper, .dashboard])
    .union([.gatekeeper => .login])
    .union(.login => .register => .forgotPass => .login)
    .union(.login => .forgotPass => .register => .login)
    .union([.login, .register] => .dashboard)
    .union(.dashboard => [.news, .compose])
    .union(.library => .news => .library)

let segues = Set(edges.map { (edge: DirectedEdge<Section>) -> Segue<Section> in
    switch edge {
    case .gatekeeper => .login:
        return Segue(edge, style: .hold, auto: true)
    case .dashboard => .news:
        return Segue(edge, style: .hold, auto: true)
    case .dashboard => .compose:
        return Segue(edge, style: .hold, dismissable: true)
    case .dashboard => .library:
        return Segue(edge, style: .hold)
    default:
        // the default is style: .pass, auto: false, dismissable: false
        return Segue(edge)
    }
})

现在我们有

一旦我们有了 segues,下一步是创建我们的 Helm 实例。可选地,我们也可以传递一个路径,以便在应用程序启动时从入口片段以外的特定片段开始。请注意,入口片段(在本例中为 .splash)始终会被呈现。

try Helm(nav: segues)
// or
try Helm(nav: segues,
         path: [
             .splash => .gatekeeper,
             .gatekeeper => .register
         ])

然后,我们将 Helm 注入到最顶层的视图中

struct RootView: View {
    @StateObject private var _helm: Helm = ...
    
    var body: some View {
        ZStack {
            //...
        }
        .environmentObject(_helm)
    }
}

最后,我们可以使用 Helm 了。请务必查看每个呈现/关闭方法的接口文档,以了解它们之间的区别。

struct DashboardView: View {
    @EnvironmentObject private var _helm: Helm<PlaygroundFragment>

    var body: some View {
        VStack {
            HStack {
                Spacer()
                LargeButton(action: { _helm.present(fragment: .compose) }) {
                    Image(systemName: "plus.square.on.square")
                }
            }
            TabView(selection: _helm.pickPresented([.library, .news, .settings])) {
                LibraryView()
                    .tabItem {
                        Label("Library", systemImage: "book.closed")
                    }
                    .tag(Optional.some(PlaygroundFragment.library))
                NewsView()
                    .tabItem {
                        Label("News", systemImage: "newspaper")
                    }
                    .tag(Optional.some(PlaygroundFragment.news))
                SettingsView()
                    .tabItem {
                        Label("Settings", systemImage: "gearshape.fill")
                    }
                    .tag(Optional.some(PlaygroundFragment.settings))
            }
        }
        .sheet(isPresented: _helm.isPresented(.compose)) {
            ComposeView()
        }
    }
}

错误处理

Helm 的大多数方法都不会抛出错误,而是使用 errors 发布的属性报告错误。这允许与 SwiftUI 处理程序(例如 Button 的 action)无缝集成,同时也使调试和断言变得容易。

_helm.$errors
    .sink {
        assertionFailure($0.description)
    }
    .store(in: &cancellables)

深度链接

呈现路径 (OrderedSet<DirectedEdge<N>>) 已经符合 EncodableDecodable 协议,因此可以轻松地保存和恢复为 JSON 对象。或者,可以将更简单的字符串路径转换为基于图的呈现路径,并使用前者来链接应用程序中的各个部分。

快照测试

能够遍历导航图是 Helm 的最大优势之一。这可以有多种用途,其中快照测试是最重要的。遍历,在每个步骤之后拍摄快照,并将结果与先前保存的快照进行比较。所有这些都可以在几行代码中完成

let transitions = _helm.transitions()
for transition in transitions {
    try helm.navigate(transition: transition)
    // mutate state if needed, take a snapshot, compare it
}

此外,通过使用自定义的转场集合,可以在片段之间进行任意步骤。这可以用于自动记录特定流程的视频(和快照)(对 App Store 宣传材料非常有帮助)。

示例

该软件包包含一个名为 Playground 的额外项目。它将 Helm 与 SwiftUI 集成,包括使用 NavigationView、sheet 模态框、TabView 等。

许可证

MIT 许可证