一、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
func frame ( width: CGFloat ? = nil , height: CGFloat ? = nil , alignment: Alignment = . center)
在我之前的博客 SwiftUI之深入解析Alignment Guides的超实用实战教程 中分析了对齐参数,可以帮你了解关于对齐参数如何影响布局的信息。当使用上面的这个方法时,看起来是在强制视图的宽度和高度,这通常是所达到的视觉效果。然而,事实并非如此,我们真正在做的是改变报价的大小,视图将如何处理它,将取决于视图本身,大多数视图将调整到新的提供的大小,这可能导致错误地假设强制视图的大小。我们没有。 如下所示,使用 .frame() 来改变提供给子进程的内容,如果它小于 120,子视图仍然不会接受它,然而蓝色边框确实显示了框架(即在本文开头讨论的包装器),它确实被强制设置为新的大小:
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 行为改变
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 将产生一些“未指定和合理”的结果,但仍然会记录一条消息(“指定的矛盾框架约束”),可以让我们知道做错了什么。这个方法是做什么的呢?它将视图定位在具有指定宽度和高度约束的不可见框架中,详情如下:
建议的框架子尺寸将是建议的框架尺寸,受任何指定的约束,建议中任何未指定的尺寸由相应的理想尺寸替换(如果指定)。
四、Views 固定大小
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 之间的区别,当将大小设置为固定时,文本视图可以展开并显示其所有的荣耀:
五、示例
在下面的示例中,我们将复制 Text 行为,但是使用自定义视图:
这哭将其命名为 LittleSquares,它将接收一个参数,其中包含要在单行中绘制的正方形数量,所有方块的大小为 20×20,颜色为绿色。但是,当父视图提供的宽度限制视图时,希望只绘制尽可能多的正方形,并将颜色更改为红色,以指示缺少正方形,这相当于截断文本视图并在末尾显示省略号(…)字符。此外,我们希望 litlesquares 视图的增长不要超出实际需要的范围。最后,视图必须设置它自己的理想宽度,所以当父视图使用 .fixedsize() 时,SwiftUI 知道它需要增长多少。 为了解决所有这些问题,只需要确保使用 GeometryReader 来获取已提供的大小。通过代理宽度,可以知道可以画多少个正方形,如果少于总数,将它们涂成红色,否则用绿色。如果以前使用过 GeometryReader,你就知道它喜欢使用所有可用的空间,更合适的说法是:它在两个方向上都可以无限调整大小,这就是需要限制它的原因。注意,理想宽度和最大宽度是相等的,不希望视图超出其理想大小。
struct LittleSquares : View {
let total: Int
var body: some View {
GeometryReader { proxy in
} . 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 中使用的第一个修饰符之一,但正因为如此,我们通常不能完全理解它。一旦获得了一些经验,就有必要重新审视它并理解它所提供的每一个参数。