草庐IT

SwiftUI 之 HStack 和 VStack 的切换

Swift君 2023-03-28 原文

前言

SwiftUI 的各种堆栈是许多框架中最基本的布局工具,能够让我们定义组视图,这些组视图可以按照水平、垂直或覆盖视图对齐。

当涉及到水平和垂直的变体时( HStack 和 VStack ),我们需要在这两者之间动态的切换。举个例子,假如我们正在构建一个 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 能实现吗?

一种方式是用 GeometryReader 测量当前可用空间,并根据宽度是否大于其高度,可以选择使用 HStack 或  VStack 来渲染内容。

虽然可以在 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 使用了与 HStack  和 VStack 相同的 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 不仅仅是我们第三方开发者的 API ,Apple 也让 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
))
}
}
以上的操作是可行的,因为当 HStack 和 VStack 的内容类型是 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 )切换布局可以保留正在渲染的底层视图的标识,而在 HStack 和 VStack 之间切换就不会这样。这样做会令动画更流畅,例如在切换设备方向时,我们也有可能在执行此类更改时获得小幅的性能提升(因为 SwiftUI 总是在其视图层次结构为静态时尽可能表现最佳)。

选择合适的视图

但我们还没有结束,因为 iOS 16 也给了我们其他有趣的新的布局工具,它有可能也能用于实现 DynamicStack  — 一种全新的视图类型,名字叫做 ViewThatFits 。就像字面意思一样,这种新的容器将会在我们初始化时传递的候选列表中,基于当前上下文挑选出最优视图。

在我们的例子中,这意味着我们能同时把 HStack 和 VStack 传递给它,并且代表我们在它们中间自动切换。

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 视图,它可以根据当前内容在 HStack 和 VStack 之间动态切换。

有关SwiftUI 之 HStack 和 VStack 的切换的更多相关文章

  1. ruby-on-rails - Ruby on Rails with Haml - 如何从 erb 切换 - 2

    我正在从erb文件切换到HAML。我将hamlgem添加到我的系统中。我创建了app/views/layouts/application.html.haml文件。我应该只删除application.html.erb文件吗?此外,仍然有/public/index.html文件被呈现为默认页面。我想创建自己的默认index.html.haml页面。我应该把它放在哪里以及如何使系统呈现该文件而不是默认索引文件?谢谢! 最佳答案 是的,您可以删除任何已转换为HAML的View的ERB版本。至于你的另一个问题,删除public/index/h

  2. ruby-on-rails - 语言环境不在 Rails 4 中切换 - 2

    我的Rails应用程序在rails4.0.2上,我在使用locale变量和params[:locale]切换翻译时遇到问题官方railsguide.我在mysite有一个单页网站.我的国际化路线:scope"(:locale)",locale:/en|de/do#myrouteshereend我的应用程序Controllerbefore_filter:set_localedefset_localeI18n.locale=params[:locale]||I18n.default_locale#Rails.application.routes.default_url_options[:l

  3. jquery - 在 Rails 中从原型(prototype)切换到 jquery,助手呢? - 2

    我目前从prototype切换到jquery主要是为了支持简单的ajax文件上传。我使用:https://github.com/indirect/jquery-rails95%的javascript代码是由railshelper编写的,例如:-remote_function-render:updatedo|page|-page.replace_html'id',:partial=>'content'-page['form']['name']=something-page.visual_effect:highlight,'head_success'...我知道我必须为Jquery重写5%

  4. 微信小程序切换云环境 - 2

    在开发微信小程序的时候,我们可能需要开发环境和测试环境,或者其他环境,下面是切换环境的方法。首先需要明确的是:前端的页面代码是不区分环境的,环境的区分指的是云函数、云数据库、云存储这些。1、更改云函数的使用云环境这里我们从cloud1更改为test-cloud,这个改完是没有用的,因为在前端代码指定了使用的云环境。cloudfunctions文件和miniprogram文件虽然都在一个目录下,但是这两个没有直接联系。2、在evList.js中添加自己云环境evList.js存储了云环境列表,这里把test-cloud加到这个列表里,需要填写envId和alias,参照cloud1写就行。3、更

  5. ruby-on-rails - 如何在 Controller Action 中切换 bool 属性 - 2

    我有一个操作可以简单地将#active属性切换到相反的bool状态:如果@blog.active==true然后更新它到非事件如果@blog.active==false然后更新它到事件我在Controller中获得了以下自定义操作,但必须有一些Rails方法才能更优雅地执行此操作:classBlogsController是否有一种Rails方法可以将bool属性更新为相反的bool状态? 最佳答案 ActiveRecord具有执行此操作的toggle和toggle!方法。请记住,toggle!方法会跳过验证检查。classBlogs

  6. ruby - 切换到 cucumber , capybara 中的弹出窗口 - 2

    在RSpec中我可以使用这样的代码切换到弹出窗口,link,我怎么能在cucumber步骤中做这样的事情?login_window=page.driver.find_window('PPA_identity_window')main_window=page.driver.find_window('')#Weusethistoexecutethenextinstructionsinthepopupwindowpage.within_window(login_window)do#Normallyfillintheformandloginfill_in'email',:with=>""fil

  7. ruby - 在 Capistrano 3 中永久切换用户(单独授权和部署) - 2

    我们在服务器管理中有以下模式-所有用户都有自己的用户,但部署完全由特殊部署用户执行,没有直接登录的可能性。我们在Capistrano2.x中使用了这个方法:default_run_options[:shell]="sudo-udeploybash"$capstagedeploy-suser=thisisme我知道Capistrano3.x有直接切换用户的方法:task:installdoonroles(:all)doas:deploydoexecute:whoamiendendend但是这段代码会填充所有任务,默认任务不会继承deploy用户。是否可以直接设置登录用户而无需将此代码拖到

  8. ruby - 如何在命令行上切换 cocoapods(或其他 gem)版本? - 2

    有一些答案说使用gem'cocoapods','0.33.1'或gem'pod','0.33.1但这行不通。错误:执行gem时...(Gem::CommandLineError)未知命令cocoapods,在一个项目中我需要使用0.33.1,但在另一个项目中需要使用0.34.4。如何在命令行快速切换cocoapods版本?我不想使用gem安装或卸载。我将rbenv与ruby​​版本2.0.0p0一起使用。继续使用rbenv是首选,但只有在没有其他选择的情况下,我才能切换到rvm或纯ruby​​。谢谢。 最佳答案 您可以执行以下操作以

  9. ruby - 切换 bool 变量的最佳方法是什么? - 2

    在true和false之间进行变量切换的最佳方法是什么?一个明显的方法是初始化一个变量foo:foo=false并做:foo=foo.!每次我想切换的时候。但是当变量名很长时,这会变得冗长。有没有更简单的方法来做到这一点(通过使用诸如语法糖、原始类之类的东西)?特别是,我想知道是否有一种方法可以通过只给它一个方法来切换:foo.some_method 最佳答案 您可以使用XOR运算符。foo^=truefoo=falsefoo^=true#=>truefoo^=true#=>false

  10. ruby - 无法安装metric_fu,如何切换到simplecov? - 2

    我尝试在我的Ruby1.9环境中安装metric_fu,但由于以下问题而失败:$geminstallmetric_fu...Fetching:rcov-1.0.0.gem(100%)Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingmetric_fu:ERROR:Failedtobuildgemnativeextension./Users/xxx/.rvm/rubies/ruby-1.9.2-p290/bin/rubyextconf.rb****Ruby1.9isnotsupported.Please

随机推荐