在SwiftUI中自定义一个百分比Layout

前言

  1. SwiftUI的原生布局中没有提供相对布局的Layout。但是像聊天气泡这类的View基本上不会沾满容器的宽。一般会设置为占比容器宽度的80%
  2. 幸运的是SwiftUI提供了Layout布局协议,让我们可以自定义一种布局

SwiftUI中的布局原理如下图所示

  1. 容器给子视图一个建议的Size
  2. 子视图渲染并返回它的Size
  3. 容器根据对齐规则,将子视图放置到对应位置

构建相对布局

实现布局Layout

通过实现Layout协议,我们构建相对布局。通过提供宽高的最大占比,来调整传递给子视图的建议大小

swift 复制代码
// 自定义相对布局,注意只能包裹1个子视图
// 通过将布局定义为fileprivate并为View添加一个扩展,我们可以保证使用的时候只有一个子视图
fileprivate struct RelativeSizeLayout: Layout {
    // width相对百分比0~1
    var relativeWidth: Double
    // height相对百分比0~1
    var relativeHeight: Double
    
    // 这是布局的第一步和第二步。父容器传递proposal下来,我们作为自视图将Size返回
    func sizeThatFits(
        proposal: ProposedViewSize, 
        subviews: Subviews, 
        cache: inout ()
    ) -> CGSize {
        assert(subviews.count == 1, "expects a single subview")
        let resizedProposal = ProposedViewSize(
            width: proposal.width.map { $0 * relativeWidth },
            height: proposal.height.map { $0 * relativeHeight }
        )
        return subviews[0].sizeThatFits(resizedProposal)
    }

    // 在这个方法里对子视图进行布局
    func placeSubviews(
        in bounds: CGRect, 
        proposal: ProposedViewSize, 
        subviews: Subviews, 
        cache: inout ()
    ) {
        assert(subviews.count == 1, "expects a single subview")
        let resizedProposal = ProposedViewSize(
            width: proposal.width.map { $0 * relativeWidth },
            height: proposal.height.map { $0 * relativeHeight }
        )
        subviews[0].place(
            at: CGPoint(x: bounds.midX, y: bounds.midY), 
            anchor: .center, 
            proposal: resizedProposal
        )
    }
}

给 View 扩展一个便利的使用方法

swift 复制代码
extension View {
    /// Proposes a percentage of its received proposed size to `self`.
    public func relativeProposed(width: Double = 1, 
                                height: Double = 1) -> some View {
        RelativeSizeLayout(relativeWidth: width,
                          relativeHeight: height) {
            // Wrap content view in a container to make sure the layout only
            // receives a single subview. Because views are lists!
            VStack { // alternatively: `_UnaryViewAdaptor(self)`
                self
            }
        }
    }
}

使用示例

swift 复制代码
let alignment: Alignment = message.sender == .me ? .trailing : .leading
chatBubble
    // 这里我们可以设置气泡的最大宽度,然后再应用我们的relativeProposed。这样可以保证气泡先限制在400以内,然后再走我们的自定义Layout
    // .frame(maxWidth: 400)
    .relativeProposed(width: 0.8)
    .frame(maxWidth: .infinity, alignment: alignment)

说明

  1. 我们自定义的Layout只是将proposalSize进行了百分比计算后传递给了子视图,实际上最后布局的时候子视图的大小依然是子视图返回的size。
  2. SwiftUI中每应用一次modifier,你可以认为是在原有视图上加了一层父视图。而整个布局是从最顶层的屏幕大小开始逐层往下建议尺寸。所以在relativeProposed之前就已经限制了大小的modifier会先决定它的size。就和上面.frame(maxWidth: 400) .relativeProposed(width: 0.8)一样,会先限制最大400,然后才是父容器80%

资料

oleb.net/2023/swiftu...

相关推荐
大熊猫侯佩9 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩9 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(四)
数据库·swiftui·apple watch
MaoJiu1 天前
Flutter造轮子系列:flutter_permission_kit
flutter·swiftui
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩1 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
大熊猫侯佩2 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩2 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩2 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple