SwiftUI之深入解析Frame Behaviors

一、Frame 简介

  • 当开始使用 SwiftUI 时,可能接触到的第一个修饰符是 frame(width:height:alignment),定义 frame 是 SwiftUI 最具挑战性的任务之一,当我们使用修饰符(如 .frame().)时,会发生很多事情。
  • SwiftUI 中的修饰符实际上并不修改视图,大多数情况下,当在视图上应用修饰符时,会创建一个新视图,该视图围绕着被“modified”的视图,需将包装器视图视为 frame。虽然视图没有框架(至少在 UIKit/AppKit 意义上没有),但它们确实有边界,这些边界不能直接操作,它们是视图及其父视图的紧急属性。
  • 其实 SwiftU 中的 modifies 并不是改变 View 上的某个属性而是用一个带有相关属性的新 View 来包装原有的 View,当然 .frame 也不例外。frame 可能对其子元素的大小有影响,也可能没有。例如,有些扩展到占据整个框架,有些则不会。接下来,我们着重分析 .frame(), .fixedSize() 和 .layoutPriority().。
  • 有时间,可以将 views 有趣地分为:generous、well-behaved、selfish 和 badly-behaved,使用这些术语可能会导致大家认为其中一些类型可能是不受欢迎的。这不是我的意图,只是想提供一个有趣的心理画面,以方便记住它们。但为了清楚起见,我将省略这些名称,只描述其行为。
  • 可以发现通过它们对所提供空间的反应来对观点进行分组:
    • Views 将只占用尽可能多的空间,以适应自己的内容,如 Stack containers。
    • Views 只会获取它们所需要的,如果提供给它们的空间不足以画出所有的内容,它们会尽最大努力尊重提供的空间。Text views 是一个混合的例子:如果没有足够的水平空间,它们将截断或换行文本。然而,无论框架有多小,至少会显示一行文本。
    • Views 将增长以填满所有提供的空间(但不能再多一个像素),形状通常是一个很好的例子,比如 Rectangle()。
    • Views 甚至可能决定在父视图提供的区域之外绘制的视图,一些自定义视图可以使用这种方法。
  • 请注意,Views 在每个轴上的行为可能不同。例如,VStack 中的 Spacer 可能会在垂直方向上占用所有空间,但不会在水平方向上占用任何空间。话虽如此,在某些情况下,一个轴的行为会受到另一个轴的影响。Text views 视图就是这样一个例子,因为它的高度可能取决于建议的宽度。
  • 通过使用 .frame() 修饰符,没有直接改变行为,而是改变它们工作的上下文,从而导致不同的行为。

二、基本的 Frame

  • 使用 frame() 的最基本方式:
func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)
  • 在我之前的博客 SwiftUI之深入解析Alignment Guides的超实用实战教程 中分析了对齐参数,可以帮你了解关于对齐参数如何影响布局的信息。当使用上面的这个方法时,看起来是在强制视图的宽度和高度,这通常是所达到的视觉效果。然而,事实并非如此,我们真正在做的是改变报价的大小,视图将如何处理它,将取决于视图本身,大多数视图将调整到新的提供的大小,这可能导致错误地假设强制视图的大小。我们没有。
  • 如下所示,使用 .frame() 来改变提供给子进程的内容,如果它小于 120,子视图仍然不会接受它,然而蓝色边框确实显示了框架(即在本文开头讨论的包装器),它确实被强制设置为新的大小:

SwiftUI之深入解析Frame Behaviors_第1张图片

struct ExampleView: View {
    @State private var width: CGFloat = 50
    
    var body: some View {
        VStack {
            SubView()
                .frame(width: self.width, height: 120)
                .border(Color.blue, width: 2)
            
            Text("Offered Width \(Int(width))")
            Slider(value: $width, in: 0...200, step: 1)
        }
    }
}


struct SubView: View {
    var body: some View {
        GeometryReader { proxy in
            Rectangle()
                .fill(Color.yellow.opacity(0.7))
                .frame(width: max(proxy.size.width, 120), height: max(proxy.size.height, 120))
        }
    }
}
  • 请注意,这些参数可以不指定或设置为 nil,在这种情况下,子进程将接收 original offer (axis),就好像根本没有调用 frame() 一样。大多数情况下,使用这个版本的 frame() 修饰符。

三、Frame 行为改变

  • 另一个 frame() 修饰符是这样的:
func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)
  • 来分析一下,有三组参数,分别是影响宽度、高度的,还有对齐参数。在另外两组中,一组处理宽度,另一组处理高度,还要注意,参数可以不指定,在这种情况下,我们将观察到不同的行为。
  • 每组有三个参数:minimum、ideal 和 maximum,必须按升序给出这些值,或者不指定,即最小参数不能大于理想参数,理想参数不能大于最大参数。在过去,不遵守这些指导方针会导致崩溃,现在 SwiftUI 将产生一些“未指定和合理”的结果,但仍然会记录一条消息(“指定的矛盾框架约束”),可以让我们知道做错了什么。这个方法是做什么的呢?它将视图定位在具有指定宽度和高度约束的不可见框架中,详情如下:
    • 建议的框架子尺寸将是建议的框架尺寸,受任何指定的约束,建议中任何未指定的尺寸由相应的理想尺寸替换(如果指定)。
    • 框架在任何维度上的大小都遵循这个逻辑:

SwiftUI之深入解析Frame Behaviors_第2张图片

四、Views 固定大小

  • .fixedSize() 方法有两个版本:
func fixedSize() -> some View
func fixedSize(horizontal: Bool, vertical: Bool) -> some View
  • 第一个选项只是一种快速的呼叫方式:
fixedSize(horizontal: true, vertical: true)
  • 当为给定的 axis 设置固定的尺寸时,视图将被提供其理想尺寸(即在 .frame() 修饰符的 idealWidth/idealHeight 参数中指定的尺寸)。注意,视图总是有一个理想的维度,但是可以调用 .frame(idealWidth:idealHeight) 来创建一个类似的视图,但是具有不同的理想维度。一个具有有趣的理想维度的视图是 Text 视图,文本视图使用文本字符串和字体,以便得出理想的大小。
  • 如下所示,截断文本,由于父元素不够大,绿色边框表示框架的边界,蓝色边框表示文本的边界:
struct ExampleView: View {
    var body: some View {
        Text("hello there, this is a long line that won't fit parent's size.")
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
  • 然而,如果让文本视图使用尽可能多的宽度需要,这是结果:
struct ExampleView: View {
    var body: some View {
        Text("hello there, this is a long line that won't fit parent's size.")
            .fixedSize(horizontal: true, vertical: false)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
  • 如下所示的动画显示了使用 fixedSize 和不指定 fixedSize 之间的区别,当将大小设置为固定时,文本视图可以展开并显示其所有的荣耀:

SwiftUI之深入解析Frame Behaviors_第3张图片

五、示例

  • 在下面的示例中,我们将复制 Text 行为,但是使用自定义视图:

SwiftUI之深入解析Frame Behaviors_第4张图片

  • 这哭将其命名为 LittleSquares,它将接收一个参数,其中包含要在单行中绘制的正方形数量,所有方块的大小为 20×20,颜色为绿色。但是,当父视图提供的宽度限制视图时,希望只绘制尽可能多的正方形,并将颜色更改为红色,以指示缺少正方形,这相当于截断文本视图并在末尾显示省略号(…)字符。此外,我们希望 litlesquares 视图的增长不要超出实际需要的范围。最后,视图必须设置它自己的理想宽度,所以当父视图使用 .fixedsize() 时,SwiftUI 知道它需要增长多少。
  • 为了解决所有这些问题,只需要确保使用 GeometryReader 来获取已提供的大小。通过代理宽度,可以知道可以画多少个正方形,如果少于总数,将它们涂成红色,否则用绿色。如果以前使用过 GeometryReader,你就知道它喜欢使用所有可用的空间,更合适的说法是:它在两个方向上都可以无限调整大小,这就是需要限制它的原因。注意,理想宽度和最大宽度是相等的,不希望视图超出其理想大小。
struct LittleSquares: View {
    let total: Int
    
    var body: some View {
        GeometryReader { proxy in

            // draw our view here

        }.frame(idealWidth: ???, maxWidth: ???)
    }
}
  • 完整的代码如下:
struct ExampleView: View {
    @State private var width: CGFloat = 150
    @State private var fixedSize: Bool = true
    
    var body: some View {
        GeometryReader { proxy in
            
            VStack {
                Spacer()
                
                VStack {
                    LittleSquares(total: 7)
                        .border(Color.green)
                        .fixedSize(horizontal: self.fixedSize, vertical: false)
                }
                .frame(width: self.width)
                .border(Color.primary)
                .background(MyGradient())
                
                Spacer()
                
                Form {
                    Slider(value: self.$width, in: 0...proxy.size.width)
                    Toggle(isOn: self.$fixedSize) { Text("Fixed Width") }
                }
            }
        }.padding(.top, 140)
    }
}

struct LittleSquares: View {
    let sqSize: CGFloat = 20
    let total: Int
    
    var body: some View {
        GeometryReader { proxy in
            HStack(spacing: 5) {
                ForEach(0..<self.maxSquares(proxy), id: \.self) { _ in
                    RoundedRectangle(cornerRadius: 5).frame(width: self.sqSize, height: self.sqSize)
                        .foregroundColor(self.allFit(proxy) ? .green : .red)
                }
            }
        }.frame(idealWidth: (5 + self.sqSize) * CGFloat(self.total), maxWidth: (5 + self.sqSize) * CGFloat(self.total))
    }

    func maxSquares(_ proxy: GeometryProxy) -> Int {
        return min(Int(proxy.size.width / (sqSize + 5)), total)
    }
    
    func allFit(_ proxy: GeometryProxy) -> Bool {
        return maxSquares(proxy) == total
    }
}

struct MyGradient: View {
    var body: some View {
        LinearGradient(gradient: Gradient(colors: [Color.red.opacity(0.1), Color.green.opacity(0.1)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1))
    }
}

六、Layout 优先

  • 可能您以前可能使用过 .layourpriority() 方法,大多数时候,当事情不顺利时,会把它作为快速解决办法。暂停一下,分析一下它是做什么的以及它是如何做的。当多个 multiple siblings 竞争空间时,父节点将可用空间除以兄弟姐妹的数量并将其提供给第一个孩子,然后它将扣除它所占用的空间,并继续进行下一个孩子。
  • 这个简单的方法可以通过使用 .layoutpriority() 方法改变兄弟节点的优先级来改变,它只有一个参数,用来决定父节点如何对空间进行优先排序。所有视图的默认布局优先级为零,为了计算子视图将提供多少空间,父视图将遵循与前面类似的逻辑,但根据优先级对视图进行分组,并首先将空间分配给优先级较高的视图。

七、总结

  • frame 方法是在 SwiftUI 中使用的第一个修饰符之一,但正因为如此,我们通常不能完全理解它。一旦获得了一些经验,就有必要重新审视它并理解它所提供的每一个参数。

你可能感兴趣的:(SwiftUI,SwiftUI,Frame,Behaviors,MostBasicFrame,Fixed,Size,View,Practical,Case)