GeometryReader in SwiftUI

GeometryReader 作用

在 SwiftUI 布局中,parent view 和 child view 的联系是松散的:child view 无法获取到 parent view 的尺寸信息。那么当 child view 的大小位置需要根据 parent view 的大小决定,就无法实现了。比如现在需要一个 rectangle 的大小是 parent view 的一半

struct ContentView: View {
    
    var body: some View {
        VStack{
            Rectangle()
                .frame(width: vstack.width / 2, height: vstack.height / 2)
        }
        .frame(width: 300, height: 300)
        .background(Color.yellow)
    }
}
复制代码

想要实现上面的需求,就需要用到 GeometryReader

GeometryReader 是什么

GeometryReader 本身也是一个 view,但是它和其他 view 不同的地方在于它暴露出了一个对象,Rectangle 通过这个对象可以获取到 parent view 的大小。(对于 RectangleGeometryReader 就是 parent view)

var body: some View {
    VStack{
        Rectangle()
            .frame(width: 150, height: 150)
    }
    .frame(width: 300, height: 300)
    .background(Color.yellow)
}
         
复制代码

上面的代码,我们使用 GeomertyReader 替换 VStack

struct ContentView: View {
    
    var body: some View {
        GeometryReader{ proxy in
            Rectangle()
                .frame(width: 150, height: 150)
        }
        .frame(width: 300, height: 300)
        .background(Color.yellow)
    }
}
复制代码

对比两个代码,唯一的区别在于 GeometryReader 暴露出了 proxy 对象(proxy 类型为 GeometryProxy,这个对理解文章影响不大)。这里我对于 swift 不熟练的同学再仔细讲下:

  1. VStackGeometryReader 两个结构体的 init 方法都需要一个闭包函数做参数。
  2. GeometryReaderinit 的参数:闭包函数,这个闭包函数本身也需要一个 proxy 对象做参数。

那么,从暴露出来的 proxy 对象中,我们可以获取到 GeometryReader view 的大小。

struct ContentView: View {
    
    var body: some View {
        GeometryReader{ proxy in
            Rectangle()
                .frame(width: proxy.size.width / 2, height: proxy.size.height / 2)   //这里是重点
        }
        .frame(width: 300, height: 300)
        .background(Color.yellow)
    }
}
复制代码

上面的代码中,通过 proxy.size 可以获取到 GeometryReader 的 size。

进一步扩展,我们可以把 GeometryReader 放入 VStack

struct ContentView: View {
    
    var body: some View {
        VStack {
            GeometryReader{ proxy in
                Rectangle()
                    .frame(width: proxy.size.width / 2, height: proxy.size.height / 2)
            }
            
        }.frame(width: 300, height: 300)
        .background(Color.yellow)
    }
}
复制代码

这个代码和最开始的代码相比,只是在 VStackRectangle 之间引入 GeometryReader,之后利用 proxy 对象提供的 size 就可以获取到 VStack的大小,Rectangle 就可以设置成 VStack 大小的一半了。

上面有一个细节忽略了:仔细看代码,Rectangleproxy 中获取到的 size 应该是 GeometryReader 的大小,但是由于 GeometryReaderVStack 的大小是一样的,所以就等同于获取到 VStack 的大小。

图片.png

到这里,我们知道了可以通过 GeometryReaderproxy 获取到 parent view 的大小。除了获取到 parent view 的大小,child view 还可以获取到自己在 parent view 区域内的坐标,也就是「我在爸爸的哪个地方」。看下面代码:

struct ContentView: View {
    
    var body: some View {
        VStack {
            GeometryReader{ proxy in
                Text("I'm in (x: \(proxy.frame(in: .local).minX), y: \(proxy.frame(in: .local).minY))")
                    .font(.title2)
            }
        }
        .frame(height: 200)
        .background(Color.yellow)
    }
}
复制代码

运行结果如下:
图片.png

proxy 提供了 frame(in:) 方法,通过这个方法的返回值,Rectangle 可以知道自己在 VStack 中的坐标。(同样,这里 Text 实际 parent view 是 GeometryReader,不过 GeometryReaderVStack 大小位置一样,这里就直接这么说了)。

VStack 区域是黄色部分,以左上角为原点建立坐标系,看到 Text 正好在黄色区域的左上角,起始坐标是 (0, 0),这和 frame(in:) 的返回值验证上了。

注意到 frame(in:) 有一个参数,并且我们上面传递的是 .local,实际上 frame(in:) 可以传递 3 个不同的参数:

  1. frame(in: .local),这个参数正如上面所演示的,child view 可以获取到「以 parent view 区域为坐标系的位置」。
  2. frame(in: .global),使用这个参数会返回 child view 在整个屏幕的位置。
  3. frame(in: .named()),自定义坐标参考系,返回以自定义坐标系的位置。

其中 1 和 2 还能理解,但是 3 是什么意思?这么想一下:现在使用 .local.global 可以获取在 parent view 和 整个屏幕中的位置,那如果希望 child view 获取在「grandfather view」的位置呢?这个时候就可以使用 frame(in: .named()) 了。具体作用如下:

  1. 在 child view 的任意直系祖先中使用 .coordinateSpace(name: "custom") 自定义坐标系。
  2. child view 中使用 proxy.frame(in :.named("custom")) 就可以获取到在该坐标系中的位置。

具体代码如下

struct ContentView: View {
    
    var body: some View {
        HStack{
            VStack {
                VStack{
                    GeometryReader{proxy in
                        Text("I'm in (x: \(Int(proxy.frame(in: .named("custom")).minX)), y: \(Int(proxy.frame(in: .named("custom")).minY)))")
                    }
                }.frame(width: 200, height: 200)
                .background(Color.yellow)
            }
            .frame(width: 300, height: 300)
            .background(Color.blue)
            .coordinateSpace(name: "custom")  //自定义一个坐标系
        }
    }
}
复制代码

运行可以看到,Text 坐标是 (50,50),这个坐标正是相对蓝色区域的坐标系,蓝色区域为 HStack,是 Text 的 grandfather view。

图片.png

但是要注意一点:frame(in: .named()) 一定是 child view 的直系祖先,类似叔叔之类的是不行的,获取到的坐标是 .global 类型的。代码如下

struct ContentView: View {
    
    var body: some View {
        HStack{
            
            VStack{
                
            }.frame(width: 150, height: 150)
            .background(Color.blue)
            .coordinateSpace(name: "custom")
            
            VStack{
                GeometryReader{proxy in
                    Text("I'm in (x: \(Int(proxy.frame(in: .global).minX)), y: \(Int(proxy.frame(in: .named("custom")).minY)))")
                }
            }.frame(width: 200, height: 200)
            .background(Color.yellow)
            
        }
    }
}
复制代码

总结一下:

  1. 使用 GeometryReader 暴露出来的 proxy.width && proxy.height 可以获取到 parent view 的大小。
  2. 使用 GeometryReader 暴露出来的 proxy.frame(in:) 方法可以获取 child view 的位置。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享