解析SwiftUI布局细节(一)入门
前言
在前面的文章中谈了谈对SwiftUI的基本的认识,以及用我们最常见的TB+NA的方式搭建了一个很基本的场景来帮助认识了一下SwiftUI,具体的文章可以在SwiftUI分类部分查找,这篇我准备在写UI的时候从SwiftUI角度我们具体的应该怎样去做,或者说是用SwiftUI我们该从什么角度去解析一个页面。以及对SwiftUI里面的其中一些细节知识做一下分析总结。
以前我们用UIKit写一个列表页的时候我们的步骤可能是下面这样的:
1、创建视图控制器
2、大概解析一下UI,该创建头部的创建头部视图,该写CollectionViewCell或者TableViewCell的我们会做一个基本的分类,规划一下我们需要几个类型的Cell等等
3、把它们进行一个组装,处理相应的各种代理或者事件回调等等
4、处理数据和视图进行数据对接
可能我们大部分都是这样的一个基本的流程,当然还有些涉及到复杂点的业务我们会从单元测试开始等等的会有些许差异,但SwiftUI的重点是对UI的处理,所以我们的重点就单纯说说UI部分,那大家可以这样想,我们用SwiftUI做的时候该怎样去开始呢,用SwiftUI做的时候流程还会和我们使用UIKit处理的时候还一样吗?在实现的细节方面又会有哪些差距呢?带着这样一个小小的思考我们进行下面的总结。
SwiftUI我们怎么做以及细节分析
前面文章我有提过一点就是View,SwiftUI最大的区别除了声明式的UI之外我自己觉得最大的需要我们理解的点就是View,所有的你能看到的基本单位都成了View,没有了控制器这个概念,这点需要我们转过这个弯,不然容易绕进去。
我们从一个具体的实际页面开始梳理一下用SwiftUI实际写UI的时候一些基本的知识,就如我们Demo中的我的页面举例:
我们首先得认识一下它俩:VStack (竖直) HStack (横向)
它们俩我最能接受的方式就是把他们理解成容器(受Cocos影响),一个纵向 (vertical) 容器,一个横向(horizontal)容器,它们前面的V和H也就是这两单词的首字母,提醒一下你要是记不住的话可以记这一点。H(heng) 剩下的V就是纵向的,所有的iOS方向属性几乎都是这样,加深记忆的一个方式而已,但能保证你以后绝不会再搞混淆! 当然这个横向和纵向也是相对你手机屏幕的是竖直还是水平的,不是绝对的,这个理解一下也容易!由于这两里面的东西几乎都是一样的,我们就针对一个VStack进行具体的分析,先看看它的源码:
/// A view that arranges its children in a vertical line. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct VStack<Content> : View where Content : View { /// Creates an instance with the given spacing and horizontal alignment. /// /// - Parameters: /// - alignment: The guide for aligning the subviews in this stack. It has /// the same horizontal screen coordinate for all children. /// - spacing: The distance between adjacent subviews, or `nil` if you /// want the stack to choose a default distance for each pair of /// subviews. /// - content: A view builder that creates the content of this stack. @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. public typealias Body = Never }
我们解释一下它初始化的方法参数:
1、首先我们要认识到VStack是一个结构体
2、alignment: HorizontalAlignment 我们可以看到它有一个默认的居中对齐值,它控制的就是容器里面的子视图的对齐方式,这个可以自己体验下。
3、spacing: CGFloat? = nil 这是个可选类型的参数,它控制的是容器里面子视图之间的间距。
4、@ViewBuilder content: () -> Content 这是一个很有意思的东西,很值得我们仔细的说说,因为我们在后面会经常使用到这个@ViewBuilder,要暂时不管它那这个参数就只剩下content: () -> Content部分,这个闭包相信都能理解,一个比较简单的闭包,对Content 的约束都在声明VStack的时候说的比较清楚。那他和普通的闭包区别也就在@ViewBuilder上,我们就把重点转移到对@ViewBuilder的理解上了。
下面是关于ViewBuilder的定义:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @_functionBuilder public struct ViewBuilder { /// Builds an empty view from a block containing no statements. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view through unmodified. /// /// An example of a single view written as a child view is /// `{ Text("Hello") }`. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View }
这里面最值得注意点就是这个 @_functionBuilder 修饰符,_functionBuilder实质上能对函数进行一次处理,具体的我们可以看看下面的例子:
/// 用_functionBuilder修饰TestBuilder /// 就像用_functionBuilder修饰了ViewBuilder一样 /// 我们就用TestBuilder看看它的实际效果 @_functionBuilder struct TestBuilder { /// String... 参数 数量可变,你可以传入任意数量的参数 /// - Parameter items: items description /// - Returns: description static func buildBlock(_ items: String...) -> [String] { return items } } /// 然后我们有这样一个方法 /// @TestBuilder模拟@ViewBuilder /// - Parameter content: content description func testBuilder(@TestBuilder _ content:() -> [String]){ print(content()) } /// 然后我们调用的时候 self.testBuilder { "1" "2" "3" "4" }
随后的打印结果就是 ["1", "2", "3", "4"]
那下面我们理解一下这个例子,在整个显式的调用中,我们似乎是没有用到buildBlock函数的,那要是我们在定义TestBuilder的时候要是不定义buildBlock是不是也可以,当然是不行的,这个在具体的例子中可以试试,在调用的时候就会报错,告诉你没有buildBlock函数,这个函数的具体的作用,我们在对它的注释中能找到答案。
Builds an empty view from a block containing no statements.
可以简单翻译成-从不包含任何语句的块中生成空视图。那我们就明白了,它的作用感觉类似初始化的样子,要没有它就显然是不行的。
还有上面我们调用的时候为什么要写成列的形式,能不能写成"1" "2" "3" "4" 这种形式呢?肯定是不行的,这个你也可以自己尝试一下。
我们要再往深入挖掘一下,因为后面还有个问题需要我们注意,在ViewBuilder的最后一个Extension中的buildBlock的代码是这样的
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View }
由于它里面最多能接收10个View,所以在我们常见的Stack中也就最多能接收到是个子视图,这点需要我们注意,不要到时候写的超过十个了然后一头雾水不知道是啥错误。接着我们肯定会疑惑,那就没有办法写是个以上的子视图了吗?答案当然是不是,肯定可以,具体的可以通过Group或者ForEach来实现,我们就不在往下深究了,这个问题可以自己看看!
不知道看到这大家对ViewBuilder应该有了一些认识了吧,我会在后面的参考文章中具体的在给几个例子地址,大家可以再仔细的看看,我们就看我们Demo中的一个使用,他具体的一个场景是这样的,在登录页面,我想加一个点击除了输入框之外收起键盘的操作,我们具体的实现方法其实就是在最底层添加了一个View,然后在它上面添加了点击的手势,具体得我们看看代码:
/// 定义一个常见的背景View struct Background<Content: View>: View { private var content: Content init(@ViewBuilder content: @escaping () -> Content) { self.content = content() } var body: some View { Color.white .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) .overlay(content) } } /// UIApplication 的扩展 extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } /// 具体的使用就是下面这样,这样就达到了我们的目的,中间的代码我隐藏起来了,代码在BaseLoginView中可以查看到 /// var body: some View { Background { /// 里面具体的视图内容 }.onTapGesture { self.endEditing() } }
这样我相信就基本把这个比较重要的@ViewBuilder给说清楚了,这个VStack或者HStack也就应该慢慢的再理解了。
理解了之后我们也就能总结一下我们用SwiftUI写UI时候的一个简单逻辑
1、创建好你需要的SwiftUI文件
2、规划好你的视图层级,比如说是不是嵌套的NavigationView里面,然后开始规划Stack,看具体的是需要规划成几个你需要的Stack
3、再往下就是里面具体的各种控件View了,我打算把他们放到下一篇再做一个具体的总结
下一篇我们就说说SwiftUI关于View跳转的方式,以及传值注意点、View位置设置、大小缩放等等的属性的使用
参考文章: