1. HStack和LazyHStack
-
这两者的差异从姓名能够看出来后者是懒加载的,LazyHStack 是在 HStack 上做加载的优化。常用场景
-
ScrollView + LazyHStack 控件:
- ScrollView + HStack:未翻滚到的控件也悉数加载了;
- ScrollView + LazyHStack:翻滚到屏幕范围内的才会加载。
(VStack 和 LazyVStack 同理。)
示例代码(LazyHStack 改为 HStack,别离翻滚时看下 print 输出):
ScrollView(.horizontal) { LazyHStack { ForEach(1...10, id: \.self) { count in Text("Count \(count)") .onAppear { print("LazyHStack count: \(count)") } } } } .frame(height: 100) .background(Color.green)
-
其他场景下不能运用 LazyHStack 替代 HStack,体现上还是有差异的。
例如以下想要 leading 20 的作用,LazyHStack 的体现仍然是居中:
改成运用 HStack 便是想要的作用了:
2. 自定义Shape
开发中常需求给 View 设置圆角。SwiftUI 现有的控件不能直接达到作用,运用办法如下:
RoundedRectangle(cornerRadius: 20, style: .circular)
.fill(Color.yellow)
.frame(width: 200, height: 100)
但是咱们常需求设置局部圆角,能够自定义如下形状:
public struct PERoundedRectangle: Shape {
public var radius: CGFloat = .infinity
public var corners: UIRectCorner = .allCorners
public func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
// 运用示例
PERoundedRectangle(radius: 20, corners: [.topLeft, .topRight])
.fill(Color.yellow)
.frame(width: 200, height: 100)
给 View 增加扩展办法更方便运用:
extension View {
public func jk_cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(PERoundedRectangle(radius: radius, corners: corners))
}
}
// 运用示例
Rectangle()
.fill(Color.yellow)
.jk_cornerRadius(20, corners: [.topLeft, .topRight])
.frame(width: 200, height: 100)
再比方常用的画线:
public enum PELineDirection {
case horizontal, vertical
}
public struct PELine: Shape {
public var direction: PELineDirection
public init(direction: PELineDirection) {
self.direction = direction
}
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
if direction == .horizontal {
path.addLine(to: CGPoint(x: rect.width, y: 0))
} else {
path.addLine(to: CGPoint(x: 0, y: rect.height))
}
return path
}
}
// 运用示例
PELine(direction: .vertical)
.stroke(style: StrokeStyle(lineWidth: 1, dash: [3, 3]))
.foregroundColor(.blue)
.frame(width: 1, height: 200)
3. 运用填充防止距离核算
例如这是我想要的作用:粉红色区域内部 leading 20,trailing 10,剩下内容均分靠左
画四条线:
第一想法:已然内部是等距离的,那么运用 HStack(spacing: xSpacing) 填充:
let data = [1, 2, 3, 4]
let yRange = 0..<data.count
let xSpacing = (UIScreen.main.bounds.width - 100 - 30) / CGFloat(data.count)
HStack(alignment: .top, spacing: xSpacing) { // 料想运用宽度为 spacing 撑开的巨细
ForEach(yRange, id: \.self) { idx in
HStack {
PELine(direction: .vertical) // 在撑开的巨细靠左画线
.stroke(style: idx == 0 ? StrokeStyle(lineWidth: 1) : StrokeStyle(lineWidth: 1, dash: [3, 3]))
.foregroundColor(.blue)
.frame(width: 1)
}
.background(Color(uiColor: .yellow))
}
}
.frame(width: UIScreen.main.bounds.width - 100, height: 200)
.padding(.leading, 20)
.padding(.trailing, 10)
.background(
Rectangle()
.fill(.pink)
)
let data = [1, 2, 3, 4]
let yRange = 0..<data.count
HStack { // ① 整个巨细
ForEach(yRange, id: \.self) { idx in
HStack {
PELine(direction: .vertical) // 线是撑不开的
.stroke(style: idx == 0 ? StrokeStyle(lineWidth: 1) : StrokeStyle(lineWidth: 1, dash: [3, 3]))
.foregroundColor(.blue)
.frame(width: 1)
Spacer() // ② 帮助撑开巨细,线才能居该空间的左边
}
.background(Color(uiColor: .yellow))
}
}
.frame(width: UIScreen.main.bounds.width - 100, height: 200)
.padding(.leading, 20) // 内部前后距离
.padding(.trailing, 10)
.background(
Rectangle()
.fill(.pink)
)
即想要的作用如下:
思路转化:区分区间等分画线 –> 设置区间 leading 和 trailing,往里增加可撑开的控件。
这儿还要注意两次黄色区域巨细的不同,为什么 Spacer能够撑开?首要记住以下规律(这个规律各文章都有说,一定要理解并紧记,后续写布局代码时或遇到任何布局问题时就想一下规律):
布局规律
- 父 view 为子 view 供给一个主张的 size,
问询子视图的巨细
;子
view 根据自身的特性,回来一个 size
;- 父 view
根据子 view 回来的 size
为其进行布局。
根据规律来理解现象:
-
图1:HStack 中只要一个 PELine,父视图供给主张 size 并问询子视图巨细,子视图根据自身回来
一个 PELine
的宽度。因而图1的单个黄色区域巨细便是 PELine 宽; -
图2:HStack 中只要一个 PELine 和 Spacer,此刻父视图供给的 size,子视图在
Spacer 的帮助下撑开了
该 size,因而黄色区域是运用了父视图均分
的主张 size。
4. SwiftUI中的默认距离
VStack/HStack
准确高度时用 VStack(spacing: 0) {},撑开布局时用 VStack {}。 具体解释下,如例3中,Spacer() 是撑开布局,所以和 Line 无间隔,不用写 HStack(spacing: 0)。
(HStack 同理。)
.padding
.padding 是一个 View 对自己的内边距设置的 modifier (调节器,例如 .frame、.background 等都是 SwiftUI View 的 modifier):
5. 方位-offset和position
.offset 相对方位
- .offset 是将
视图以及已经增加的修饰符
都进行全体偏移
; - .offset 后的 background 修饰的是
原始方位
; - .offset 接纳 CGPoint 或 CGSize 的作用是一样的,无差异。
.position 绝对方位
- .position 是将视图的
中心
放置在父视图的分配区域(调查第二个 Text 方位)
上; - .position 后的 background 指的是
分配区域
。
6. 初写时重视视图巨细
看个比方:
如图:图1的数据情况下看起来没问题,图2的数据情况下就露出出了问题,图3给当时视图加上灰色线框。灰色线框才是图表自身应该的巨细,实践图1的绘制就发生了错误,超出了实践巨细(原因见上述 Frame 的规则,问题出在 View 布局时子视图的巨细超出了父视图)。
总之,开始运用的时分不熟悉会呈现各种意料之外的 UI 结果,能够加线框或背景色来辅佐咱们刚开始的 SwiftUI 实践。
7. 获取父View巨细-GeometryReader
以上的比方能够看到布局时首要运用填充式布局,尽量防止核算。但是有时分咱们也会用到核算高度,比方例6中的柱形图,需求核算高度来表达当时进展。
SwiftUI 中供给了 GeometryReader(几许读取器)
来获取父 View 的巨细:
struct PEGroupBar: View {
var model: PEGroupModel
var maxValue: Int
var body: some View {
GeometryReader { geometry in
...
let maxHeight = geometry.size.height // 运用父View高度用来核算
VStack { // 第一个柱形
Spacer(minLength: 0)
Rectangle()
.frame(width: 8, height: model.progress / Double(maxValue) * maxHeight)
}
VStack { // 第二个柱形
Spacer(minLength: 0)
Rectangle()
.frame(width: 8, height: model.recommend / Double(maxValue) * maxHeight)
}
}
}
}
8. 跨层级获取某View信息-Preference
Preference(偏好)
供给了在父 View 中跨层级的获取子 View 或更深层级 View 的任何信息。
如图,子 View 中点击“切换Stack方向”按钮,子 View 中的“icon+Hello World!”的方向关系会从横向和纵向来回切换。父 View 在控制台会打印切换方向的结果。
第一步:定义子 View 给父 View 供给信息的类型
:
struct BoolPreferenceKey: PreferenceKey {
typealias Value = Bool // ①例如咱们这儿定义了个Bool类型,也能够是一个size、一个class model等。根据实践场景需求
static var defaultValue = false // ②该类型的默认值
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue() // ③接纳到新值后想做的操作,例如也能够是value+=nextValue()等
}
}
第二步:子 View 传递信息
:
struct AnyLayoutView: View {
@State var isVertical = false
var body: some View {
let layout = isVertical ? AnyLayout(VStackLayout(spacing: 5)) : AnyLayout(HStackLayout(spacing: 10))
layout {
Group {
Image(systemName: "globe")
Text("Hello World!")
}
.font(.largeTitle)
}
.border(Color.blue)
.preference(key: BoolPreferenceKey.self, value: isVertical) // ①把当时的方向信息传递出去
Spacer().frame(height: 30)
Button("Toggle Stack") {
withAnimation(.easeInOut(duration: 1.0)) {
isVertical.toggle()
}
}
}
}
第三步:父 View 接纳信息
:
struct ContentView: View {
@State var isVertical = false
var body: some View {
VStack {
AnyLayoutView() // ①展现子View
}
.onPreferenceChange(BoolPreferenceKey.self) { // ②接纳子View信息
print("AnyLayout is vertical: \($0)")
}
}
}
9. 跨层级对齐AlignmentGuide
AlignmentGuide(对齐指南):配合 Stack 运用,自定义某一个 View 的对齐行为。
- 给能够
一组 Stack 中的某个 View
做特别对齐:
.alignmentGuide 闭包中的 dim 是 ViewDimensions 类型,能够获取这个 View 的信息,例如宽、高、.leading 等。
- 给
跨层级的 Stack 中的 某个 View 自定义
对齐行为:
处理前:
处理后:
最终:布局总结
- 每个 HStack、VStack、ZStack 看作一个
全体
;- 运用 leading、trailing、top、bottom、Spacer,做
填充布局
;- 时时紧记心法——
布局规律
;- 运用 .position、.offset 调整方位;
- modifier 先后 顺序 是有逻辑的;
- 获取 View 信息;
- 跨层级对齐。