1. HStack和LazyHStack

  1. 这两者的差异从姓名能够看出来后者是懒加载的,LazyHStack 是在 HStack 上做加载的优化。常用场景

  2. 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)
    
  3. 其他场景下不能运用 LazyHStack 替代 HStack,体现上还是有差异的。

    例如以下想要 leading 20 的作用,LazyHStack 的体现仍然是居中:

    SwiftUI 初次实战—布局总结

    改成运用 HStack 便是想要的作用了:

    SwiftUI 初次实战—布局总结

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,剩下内容均分靠左画四条线:

SwiftUI 初次实战—布局总结

第一想法:已然内部是等距离的,那么运用 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)
)

SwiftUI 初次实战—布局总结

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)
)

即想要的作用如下:

SwiftUI 初次实战—布局总结

思路转化:区分区间等分画线 –> 设置区间 leading 和 trailing,往里增加可撑开的控件

这儿还要注意两次黄色区域巨细的不同,为什么 Spacer能够撑开?首要记住以下规律(这个规律各文章都有说,一定要理解并紧记,后续写布局代码时或遇到任何布局问题时就想一下规律):

布局规律

  1. 父 view 为子 view 供给一个主张的 size,问询子视图的巨细
  2. view 根据自身的特性,回来一个 size
  3. 父 view 根据子 view 回来的 size 为其进行布局

根据规律来理解现象:

  • 图1:HStack 中只要一个 PELine,父视图供给主张 size 并问询子视图巨细,子视图根据自身回来一个 PELine 的宽度。因而图1的单个黄色区域巨细便是 PELine 宽;

  • 图2:HStack 中只要一个 PELine 和 Spacer,此刻父视图供给的 size,子视图在 Spacer 的帮助下撑开了该 size,因而黄色区域是运用了父视图均分的主张 size

4. SwiftUI中的默认距离

VStack/HStack

SwiftUI 初次实战—布局总结

SwiftUI 初次实战—布局总结

准确高度时用 VStack(spacing: 0) {},撑开布局时用 VStack {}。 具体解释下,如例3中,Spacer() 是撑开布局,所以和 Line 无间隔,不用写 HStack(spacing: 0)。

(HStack 同理。)

.padding

.padding 是一个 View 对自己的内边距设置的 modifier (调节器,例如 .frame、.background 等都是 SwiftUI View 的 modifier)

SwiftUI 初次实战—布局总结

5. 方位-offset和position

.offset 相对方位

SwiftUI 初次实战—布局总结

  • .offset 是将 视图以及已经增加的修饰符 都进行全体偏移
  • .offset 后的 background 修饰的是原始方位
  • .offset 接纳 CGPoint 或 CGSize 的作用是一样的,无差异。

.position 绝对方位

SwiftUI 初次实战—布局总结

  • .position 是将视图的 中心 放置在父视图的 分配区域(调查第二个 Text 方位) 上;
  • .position 后的 background 指的是分配区域

6. 初写时重视视图巨细

看个比方:

SwiftUI 初次实战—布局总结

如图:图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 在控制台会打印切换方向的结果。

SwiftUI 初次实战—布局总结

第一步:定义子 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 的对齐行为。

  1. 给能够一组 Stack 中的某个 View 做特别对齐:

SwiftUI 初次实战—布局总结

.alignmentGuide 闭包中的 dim 是 ViewDimensions 类型,能够获取这个 View 的信息,例如宽、高、.leading 等。

  1. 跨层级的 Stack 中的 某个 View 自定义对齐行为:

处理前:

SwiftUI 初次实战—布局总结

处理后:

SwiftUI 初次实战—布局总结

最终:布局总结

  1. 每个 HStack、VStack、ZStack 看作一个 全体
  2. 运用 leading、trailing、top、bottom、Spacer,做 填充布局
  3. 时时紧记心法——布局规律
  4. 运用 .position、.offset 调整方位;
  5. modifier 先后 顺序 是有逻辑的;
  6. 获取 View 信息;
  7. 跨层级对齐。