持续创作,加速成长!这是我参加「日新计划 10 月更文挑战」的第7天,点击检查活动详情

前语

SwiftUI 的各种仓库是许多结构中最基本的布局东西,能够让咱们界说组视图,这些组视图能够按照水平、笔直或掩盖视图对齐。

当涉及到水平缓笔直的变体时( HStackVStack ),咱们需求在这两者之间动态的切换。举个比如,假设咱们正在构建一个 app 其中包含 LoginActionsView ,一个让用户登录时在列表中挑选操作的类:

struct LoginActionsView: View {
    ...
    var body: some View {
        VStack {
            Button("Login") { ... }
            Button("Reset password") { ... }
            Button("Create account") { ... }
        }
        .buttonStyle(ActionButtonStyle())
    }
}
struct ActionButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .fixedSize()
            .frame(maxWidth: .infinity)
            .padding()
            .foregroundColor(.white)
            .background(Color.blue)
            .cornerRadius(10)
    }
}

以上代码中,咱们用到了 fixedSize 避免按钮文本被截断,这仅是在咱们坚信给定的内容视图不会比视图自身更大的状况。想了解更多信息,能够检查我的文章 – SwiftUI 布局体系第三章

目前,咱们的按钮是笔直摆放的,而且填满了水平线上的可用空间(你能够用以上示例代码预览按钮的样子),尽管这在竖向的 iPhone 上看起来很好,但假设咱们现在想要在横向形式下让 UI 横向摆放。

GeometryReader 能完成吗?

一种方法是用 GeometryReader 丈量当时可用空间,并依据宽度是否大于其高度,能够挑选运用 HStackVStack 来烘托内容。

尽管能够在 LoginActionsView 中放入该逻辑,但咱们期望以后能复用代码,因而需求重新创建一个专门的视图,作为一个独立的组件来完成动态仓库的切换逻辑。

为了使代码可用性更高,咱们不会硬编码让两个仓库变体运用对齐或距离什么的。相反,让咱们像 SwiftUI 相同,对这些特点参数化,一起设定结构所运用的默认值 — 就像这样:

struct DynamicStack<Content: View>: View {
    var horizontalAlignment = HorizontalAlignment.center
var verticalAlignment = VerticalAlignment.center
var spacing: CGFloat?
    @ViewBuilder var content: () -> Content
    var body: some View {
        GeometryReader { proxy in
            Group {
                if proxy.size.width > proxy.size.height {
                    HStack(
                        alignment: verticalAlignment,
                        spacing: spacing,
                        content: content
                    )
                } else {
                    VStack(
                        alignment: horizontalAlignment,
                        spacing: spacing,
                        content: content
                    )
                }
            }
        }
    }
}

由于咱们使新的 DynamicStack 运用了与 HStackVStack 相同的 API ,现在能够在 LoginActionsView 中直接将曾经的 VStack 换成新的自界说的实例:

struct LoginActionsView: View {
    ...
    var body: some View {
        DynamicStack {
            Button("Login") { ... }
            Button("Reset password") { ... }
            Button("Create account") { ... }
        }
        .buttonStyle(ActionButtonStyle())
    }
}

优异!但是,就像上面的代码展示的那样,运用 GeometeryReader 来展示动态切换有一个适当明显的缺点,在几何图形阅读器中总是会填充水平缓笔直方向的一切可用空间(以便丈量实际空间)。在咱们的比如中,LoginActionsView 不再仅仅水平方向的摆放,它现在也能移动到屏幕的顶部。

尽管咱们也有许多办法能处理这些问题(例如运用相似在这篇 Q&A 中用来使多个视图具有相同宽度和高度的技术),但真实的问题是当咱们要动态的确认方向时,丈量可用空间是否是一个好的办法。

一个运用尺寸类的比如

相反,让咱们运用 Apple 的尺寸类体系来决定 DynamicStack 应该在底层运用 HStack 还是 VStack 。这样做的优点不仅仅是在引进 GeometeryReader 之前保存相同紧凑的布局,而且会使 DynamicStack 在开端的时候以一种和体系组件相似的方法在一切设备和方向上构建。

为了调查当时水平方向的尺寸,咱们需求用到 SwiftUI 环境体系 — 经过在 DynamicStack 中声明 @Environment – 符号特点(带有 horizontalSizeClass 关键路径),将会使咱们在视图内容中切换到当时 sizeClass 的值:

struct DynamicStack<Content: View>: View {
    ...
    @Environment(\.horizontalSizeClass) private var sizeClass
    var body: some View {
        switch sizeClass {
        case .regular:
            hStack
        case .compact, .none:
            vStack
        @unknown default:
            vStack
        }
    }
}
private extension DynamicStack {
    var hStack: some View {
        HStack(
            alignment: verticalAlignment,
            spacing: spacing,
            content: content
        )
    }
    var vStack: some View {
        VStack(
            alignment: horizontalAlignment,
            spacing: spacing,
            content: content
        )
    }
}

经过以上操作,LoginActionsView 将能够在惯例的尺寸烘托时动态切换成水平布局(例如在大尺寸的 iPhone 运用横屏,或者全屏 iPad 上的任一方向),而其它一切尺寸的配置运用笔直布局。一切这些仍然运用紧凑笔直布局,它运用的空间不超越烘托其内容所需的空间。

运用布局协议

尽管咱们最后已经用了非常棒的处理计划,能够在一切支持 SwiftUI iOS 版本中运用,但也让咱们来探究一下在 iOS 16 中引进的一些新的布局东西(在写这篇文章时,它作为 Xcode 14 的一部分仍在测验阶段)

其中一个东西是新的 Layout 协议,它既能让咱们创建完整的自界说布局,直接集成到 SwiftUI 的布局体系中,一起也提供给咱们一种更丝滑更动画的方法在各种布局之间动态切换 。

这都是由于事实证明 Layout 不仅仅是咱们第三方开发者的 APIApple 也让 SwiftUI 自己的布局容器运用这个新协议 。所以,与其直接运用 HStack VStack 作为容器视图,不如将它们作为契合 Layout 的实例,运用 AnyLayout 类型进行包装 — 就像这样:

private extension DynamicStack {
    var currentLayout: AnyLayout {
        switch sizeClass {
        case .regular, .none:
            return horizontalLayout
        case .compact:
            return verticalLayout
        @unknown default:
            return verticalLayout
        }
    }
    var horizontalLayout: AnyLayout {
        AnyLayout(HStack(
            alignment: verticalAlignment,
            spacing: spacing
        ))
    }
    var verticalLayout: AnyLayout {
        AnyLayout(VStack(
            alignment: horizontalAlignment,
            spacing: spacing
        ))
    }
}

以上的操作是可行的,由于当 HStackVStack 的内容类型是 EmptyView 时,它们都契合新的 Layout 协议(当内容为空时便是这种状况),让咱们来看一下SwiftUI 的 公共接口

struct DynamicStack<Content: View>: View {
    ...
    var body: some View {
        currentLayout(content)
    }
}

留意:由于回归, Xcode 14 beta 3 中省略了以上条件的一致性,依据 SwiftUI 团队的 Matt Ricketson 的说法,能够直接运用底层的 _HStackLayout_VStackLayout 类型作为临时的处理办法。并期望能在未来测验版本中修正。

现在咱们能经过运用新的 currentLayout 处理运用什么布局,现在咱们来更新 body 的完成,简略调用从该特点回来的 AnyLayout ,就像函数相同 — 像这样:

struct DynamicStack<Content: View>: View {
    ...
    var body: some View {
        currentLayout(content)
    }
}

咱们之所以能像一个函数相同调用布局办法(尽管它实际上是一个结构)是由于 Layout 协议运用了 Swift ”像函数相同调用“ 的特性

那么咱们之前的计划和上面根据布局的计划有什么差异呢?关键的差异在于(除了后者需求 iOS 16 )切换布局能够保存正在烘托的底层视图的标识,而在 HStackVStack 之间切换就不会这样。这样做会令动画更流畅,例如在切换设备方向时,咱们也有或许在履行此类更改时获得小幅的功能提高(由于 SwiftUI 总是在其视图层次结构为静态时尽或许表现最佳)

挑选合适的视图

但咱们还没有结束,由于 iOS 16 也给了咱们其他有趣的新的布局东西,它有或许也能用于完成 DynamicStack — 一种全新的视图类型,名字叫做 ViewThatFits 。就像字面意思相同,这种新的容器将会在咱们初始化时传递的候选列表中,根据当时上下文挑选出最优视图。

在咱们的比如中,这意味着咱们能一起把 HStackVStack 传递给它,而且代表咱们在它们中心自动切换。

struct DynamicStack<Content: View>: View {
    ...
    var body: some View {
        ViewThatFits {
            HStack(
                alignment: verticalAlignment,
                spacing: spacing,
                content: content
            )
            VStack(
                alignment: horizontalAlignment,
                spacing: spacing,
                content: content
            )
        }
    }
}

留意:在这种状况下,咱们首先放置 HStack 是很重要的,由于 VStack 或许总是合适的,即便在咱们期望布局是横向的状况下(例如 iPad 的全屏形式)。相同重要的是要指出,上述根据 ViewThatFits 的技术将会一直测验 HStack ,即便在用紧凑尺寸烘托布局时也是如此,只要在 HStack 不适合时才会挑选根据VStack 的布局。

结语

以上便是经过四种不同的方法完成 DynamicStack 视图,它能够依据当时内容在 HStackVStack 之间动态切换。

关于咱们

咱们是由 Swift 爱好者共同保护,咱们会共享以 Swift 实战、SwiftUI、Swift 基础为核心的技术内容,也收拾搜集优异的学习资料。