SwiftUI

GeometryReader的使用

GeometryReader是一个容器视图,根据其自身大小和坐标空间定义其内容。

/// A container view that defines its content as a function of its own
/// size and coordinate space. Returns a flexible preferred size to its
/// parent layout.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct GeometryReader<Content> : View where Content : View {

    public var content: (GeometryProxy) -> Content

    @inlinable public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = Never
}

如果你想要自定义绘制一个矩形,大小是父级视图的一半。位置位于父级视图右边距里5像素的视图。 此时就可以使用GeometryReader。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello There!")
            MyRectangle()

        }.frame(width: 150, height: 100).border(Color.black)
    }
}

struct MyRectangle: View {
    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .path(in: CGRect(x: geometry.size.width + 5,
                                 y: 0,
                                 width: geometry.size.width / 2.0,
                                 height: geometry.size.height / 2.0))
                .fill(Color.red)
        }
    }
}

GeometryProxy

GeometryReader尾随闭包类型是:

(GeometryProxy) -> Content

GeometryProxy类中有两个计算型属性,一个方法,和一个下标取值:

/// Acts as a proxy for access to the size and coordinate space (for
/// anchor resolution) of the container view.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct GeometryProxy {

    /// The size of the container view.
    ///父级视图建议的大小
    public var size: CGSize { get }

    /// Resolves the value of `anchor` to the container view.
    ///通过下标取值来获取一个锚点
    public subscript<T>(anchor: Anchor<T>) -> T { get }

    /// The safe area inset of the container view.
    ///容器视图safeAreaInsets
    public var safeAreaInsets: EdgeInsets { get }

    /// The container view's bounds rectangle converted to a defined
    /// coordinate space.
    ///父级视图建议区域的大小位置
    public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
}

父级视图会自动安排GeometryReader的视图大小,如下例:

struct ContentView: View {
    var body: some View {
        VStack {
            GeometryReader { geo in
                Text("Hello, World!")
                    .frame(width: geo.size.width * 0.9, height: 40)
                    .background(Color.red)
            }.background(Color.green)

            Text("More text")
                .background(Color.blue)
        }
    }
}

可以看到,“More text”被推到了底部,而GeometryReader占据了剩余的所有空间,但是这是一个首选大小,而不是绝对大小,这意味着它仍然依赖于它的父级。

接下来重点来了:

public func frame(in coordinateSpace: CoordinateSpace) -> CGRect

frame方法暴露给我们了父级视图建议区域的大小位置,可以通过 .local / .global / .named() 来获取不同的坐标空间。

如下例:

struct OuterView: View {
    var body: some View {
        VStack {
            Text("Top")
            InnerView()
                .background(Color.green)
            Text("Bottom")
        }
    }
}

struct InnerView: View {
    var body: some View {
        HStack {
            Text("Left")
            GeometryReader { geo in
                Text("Center")
                    .background(Color.blue)
                    .onTapGesture {
                        print("Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
                        print("Custom center: \(geo.frame(in: .named("Custom")).midX) x \(geo.frame(in: .named("Custom")).midY)")
                        print("Local center: \(geo.frame(in: .local).midX) x \(geo.frame(in: .local).midY)")
                    }
            }
            .background(Color.orange)
            Text("Right")
        }
    }
}

struct ContentView: View {
    var body: some View {
        OuterView()
            .background(Color.red)
            .coordinateSpace(name: "Custom")
    }
}

输出为:

Global center: 182.5 x 345.75
Custom center: 182.5 x 325.75
Local center: 144.5 x 292.75

3个点输出的坐标都不同,是因为它们的坐标系参照点是不同的:

  • .global的(0,0)是整个屏幕的左上角,包括了safe area
  • OuterView通过修饰符“.coordinateSpace(name: “Custom”)”自定义了坐标空间,.named()的(0,0)是OuterView左上角,不包括safe area
  • .local的(0,0)是InnerView左上角

你想使用哪个坐标空间取决于你想回答的问题:

  • 想知道这个视图在屏幕上的什么位置?使用全局空间。
  • 想知道这个视图相对于它的父视图的位置吗?使用本地空间。
  • 如何知道这个视图相对于其他视图的位置?使用自定义空间。

下面我们使用GeometryReader画一个每个角指定不同的半径的矩形。

struct ContentView: View {
    var body: some View {
        HStack {
            Text("SwiftUI")
                .foregroundColor(.black).font(.title).padding(15)
                .background(RoundedCorners(color: .green, tr: 30, bl: 30))
            // .overlay(RoundedCorners(color: .green, tr: 30, bl: 30).opacity(0.5))
            Text("Devler")
                .foregroundColor(.black).font(.title).padding(15)
                .background(RoundedCorners(color: .blue, tl: 30, br: 30))
            // .overlay(RoundedCorners(color: .blue, tl: 30, br: 30).opacity(0.5))

        }.padding(20).border(Color.gray).shadow(radius: 3)
    }
}

struct RoundedCorners: View {
    var color: Color = .black
    var tl: CGFloat = 0.0
    var tr: CGFloat = 0.0
    var bl: CGFloat = 0.0
    var br: CGFloat = 0.0

    var body: some View {
        GeometryReader { geometry in
            Path { path in

                let w = geometry.size.width
                let h = geometry.size.height

                // We make sure the redius does not exceed the bounds dimensions
                let tr = min(min(self.tr, h / 2), w / 2)
                let tl = min(min(self.tl, h / 2), w / 2)
                let bl = min(min(self.bl, h / 2), w / 2)
                let br = min(min(self.br, h / 2), w / 2)

                path.move(to: CGPoint(x: w / 2.0, y: 0))
                path.addLine(to: CGPoint(x: w - tr, y: 0))
                path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
                path.addLine(to: CGPoint(x: w, y: h - br))
                path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
                path.addLine(to: CGPoint(x: bl, y: h))
                path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
                path.addLine(to: CGPoint(x: 0, y: tl))
                path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
            }
            .fill(self.color)
        }
    }
}