iOSUI工具

SwiftUI中.frame修饰器的使用

2022-07-07  本文已影响0人  MambaYong

在学习 SwiftUI 的过程中,首先会学到的 modifiers 可能就是 .frame,关·modifiers这里有必要提一下 ,其实SwiftU中的modifies并不是改变View上的某个属性而是用一个带有相关属性的新View来包装原有的View,当然.frame也不例外,具体的modifiers的使用可以见我的另外一篇文章,本篇文章主要弄清楚.frameSwiftUI布局体系中到底扮演着什么重要的角色。

基本View

SwiftUI的布局原则其实很简单:对于层级中的ViewSwiftUI都会提供一个建议尺寸 ,然后View将自己布局在这个建议尺寸的可用空间内,并报告自己的实际尺寸,默认情况下系统将View置于可用空间的中心。但是View的实际尺寸和收到的建议尺寸是会有出入的,比如实际尺寸比建议尺寸大该如何显示?实际尺寸比建议尺寸小呢?有多的空间时该如何分配给各个View呢?这些都和SwiftUI中的基本View有关,下面我们从基本的View入手。

stack

Stack是尽可能的占据更多的空间在满足自己的内容,可不会委屈它们。

Text

Text的行为和UIKit中的差不多,这类View的特点是及其''老实本分'',根据自身内容来占据应有的尺寸,当空间不够时像Text就宁愿改变自己也不会超出父View当初提议的尺寸,比如宽度不够时则截断文本显示,高度不够时就显示单行,他们尽可能的尊重提议的尺寸。

Text("hello there, this is a long line that won't fit parent's size.")
     .border(Color.blue)
     .frame(width: 200, height:30)
     .border(Color.green)
     .font(.title)
     .padding(0)

由于.frame包装后的ViewText提议的尺寸是宽度为200高度为30,但是Text的实际宽度是大于提议尺寸的200的,为了"尊重"提议的尺寸,Text截断自身显示,当然也是可以忽略提议尺寸的,后面会解释到。

Image

默认情况下Image的尺寸是固定的,其会忽略掉布局系统建议的尺寸而总是返回图片的尺寸。要让一个图片 view尺寸可变,或者说,想让它能接受建议尺寸,并将图片适配显示在这个空间里,我们可以在它上面调用 .resizable,默认情况下,这会拉伸图片,让它填满整个建议尺寸的空间 (我们也可以设置成以瓷砖平铺的方式,或者只拉伸图片的某个部分来进行填充),因为大部分的图片应该是需要以固定的高宽比展示的,所以 .aspectRatio 经常被直接搭配在 .resizable 后面组合使用。
.aspectRatio 修饰器会去获取建议尺寸,并且基于给定的高宽比,创建一个能最大限度填满建议尺寸的新的尺寸值。接下来,它会将这个尺寸建议给大小可变的图像 (resizable 的图像会填满整个建议尺寸),并将该尺寸返回给上层View。我们可以选择适配或者填充这个建议尺寸,我们也可以决定是要指定一个高宽比,还是将高宽比留给子View去做决定。

let image = Image(systemName: "ellipsis")
HStack {
   image
   image.resizable()
   image.resizable().aspectRatio(contentMode: .fit)
}

Path

Path类型代表了一组 2D的绘制指令 (和Cocoa 中的 CGPath 类似),它总会将建议的尺寸作为实际尺寸返回,如果所建议的某个方向的值为 nil,那么它返回默认值10

Path { p in
    p.move(to: CGPoint(x: 50, y: 0))
    p.addLines([
      CGPoint(x: 100, y: 75),
      CGPoint(x: 0, y: 75),
      CGPoint(x: 50, y: 0)
  ])
}

可能上面的话你不太理解,那么我在写的详细点,下面的这段代码你能想到为什么是这个效果吗?

 var body: some View {
    VStack(spacing:0) {
           Text("hello there, this is a long line that won't fit parent's size.")
           .frame(width: 200, height:30)
           .border(Color.green)
           .font(.title)
           .padding(0)
           Path { p in
                p.move(to: CGPoint(x: 50, y: 0))
                p.addLines([
                  CGPoint(x: 100, y: 75),
                  CGPoint(x: 0, y: 75),
                  CGPoint(x: 50, y: 0)
              ])
            }
    }
     .border(Color.red)
 }

布局流程解析:

如果Path所建议的某个方向的值为nil,那么它返回的默认值为10,采用.fixedSize可以把建议尺寸设置为nil

 var body: some View {
    VStack(spacing:0) {
           Text("hello there, this is a long line that won't fit parent's size.")
           .frame(width: 200, height:30)
           .border(Color.green)
           .font(.title)
           .padding(0)
           Path { p in
                p.move(to: CGPoint(x: 50, y: 0))
                p.addLines([
                  CGPoint(x: 100, y: 75),
                  CGPoint(x: 0, y: 75),
                  CGPoint(x: 50, y: 0)
              ])
            }
              .fixedSize(horizontal: false, vertical: true)
    }
     .border(Color.red)
 }

上面的代码只是利用fixedsize忽略了Path竖直方向的建议尺寸,那么Path在此方向的上报尺寸则会忽略父View的建议尺寸和直接返回10,所以VStack收到的Path的高度尺寸为10,所以红色边框的区域在高度方向会如下所示,Path绘制在了VStack的外面。

Shape

Path一样 Shape也总会将建议的尺寸进行返回,在某个方向上建议尺寸为nil时返回默认值10Shape会尽可能的将自身绘制在建议尺寸甚至填满建议尺寸,像是 RectangleCircleEllipseCapsule 这些内建的形状,会将它们自身绘制在建议尺寸中。那些没有高宽比约束的形状,像是 Rectangle,会选择填满整个可用的空间,在布局过程中,Shape 会接收到 path(in:) 的调用,其中的 rect 参数所包含的尺寸正是建议尺寸。

struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
    return Path { p in
          p.move(to: CGPoint(x: rect.midX, y: rect.minY))
          p.addLines([
          CGPoint(x: rect.maxX, y: rect.maxY),
          CGPoint(x: rect.minX, y: rect.maxY),
          CGPoint(x: rect.midX, y: rect.minY)
      ])
     }
   }
}

Frame的使用

方式一

在使用Frame的过程中很容易以为Frame设置的多大,View就会占据多少空间,其实Frame改变的只是建议尺寸,具体View的实际尺寸是要看View怎么来绘制自己的,上面我们已经介绍了基础的View,可以发现基础View在收到建议尺寸时会有不同的表现形式,有的是尊重建议尺寸,有的则填充建议尺寸,还有的甚至可以在建议尺寸外面绘制自己。

Frame一共有二个初始化的方法:

在使用时参数是可变的,可以只设置width或者height,也可以同时设置,其中关于aligment的内容请看我的另外一篇文章。

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

代码解读

上面的整个布局流程是一层层由外到内,然后由内到外确定的,这种思想和UIKit是有很大的不同的,只有清理的知道SwiftUI内部的布局流程,才能不会对有些实际出来的布局效果和自己所想有所出入时感到惊讶,上面的例子请好好体会,因为接下来会进阶到更难的。

方式二

上面的例子使用的是Frame的初始化方法1,对于初始化方法2则是指定最小,理想和最大尺寸,其中我们在传值时minimumidealmaximum必须按照升序的方式传入,不然会报错,我们可以将任意参数留空,这样它们会使用默认的nil值,某个方向上的最小值和最大值将作为建议尺寸和返回尺寸的钳位,举例来说,当我们对最大宽度进行了配置时,.frame 修饰器会去检查被建议的宽度,如果这个被建议的宽度超过了设置的最大宽度,那么它只会将最大宽度建议给它的子 view。类似地,如果子view返回了一个比最大宽度更大的宽度,那么这个结果也将被钳至最大宽度值,当在某个方向设置了fixedsize时,此时会采用ideal尺寸进行布局。

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)
                }
            }
              .border(Color.orange).frame(height:proxy.size.height)
        }.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))
    }
}

上面这个例子是一个综合的例子,模拟的是类似Text的效果,当空间足够时则显示7个小方格,当空间不够时小方格尽可能的显示,并将颜色变成红色,下面是效果图,建议再看代码截图前,自己尝试理下上述代码的布局流程。

代码解读

总结

本文主要是介绍了SwiftUI的布局思想,并介绍了几种常见的基本View的布局原则,同时介绍了.frame修饰器的内部实现原理,同时利用实际例子进行了演示,只有理解SwiftUI的布局思想,才不会对意想不到的布局效果感到意外。

上一篇 下一篇

猜你喜欢

热点阅读