[SwiftUI-Lab] 让GeometryReader来解决吧

文章源地址:https://swiftui-lab.com/geometryreader-to-the-rescue/

作者: Javier

翻译: Liaoworking

大多数情况下,SwiftUI都会发挥其神奇的布局的特性。但是有时候,我们需要对自定义视图的布局进行更多操作。目前我们有几种工具。第一个需要我们去探索的就是GeometryReader

父级视图想要什么?

当我们创建自定义视图时,一般不用担心旁边视图的布局或size。如果你想要创建一个正方形。只要用一个Rectangle,就会以父级想要的size和position去画出一个正方形。

在下面的示例中,我们有一个frame为150×100的VStack。上面部分是Text,剩余空间都给了MyRectangle()。如图所示都被蓝色颜色填充:

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 {
        Rectangle().fill(Color.blue)
    }
}
image

正如你所看到的,MyRectangle(),不用去设置size,它只有一个任务,就是画矩形。让SwiftUI自己去管理好父级期望的子视图的大小和位置。 这个例子里Vstack就是父级视图。

如果你想要知道更多关于父级视图如何确定子视图的位置和大小。我强烈推荐你看看2019WWDC session 237Building Custom Views With SwiftUI

父级视图会自动为子视图找到合适的尺寸和位置。但是如果你想要自定义绘制一个矩形,大小是父级视图的一半。位置位于父级视图右边距里5像素的视图。
其实也并不复杂,这个时候可以用GeometryReader作为解决方案。

子视图做了什么?

先看看Apple官方文档如何介绍的GeometryReader:

A container view that defines its content as a function of its own size and coordinate space.
一个容器视图,根据其自身大小和坐标空间定义其内容。

这个解释已经算很详细了。

那这句话是什么意思呢? 简单来讲GeometryReader就是另外一种view。惊不惊喜? 在SwiftUI中几乎所有东西都是View。 在下面的例子中,GeometryReader让你定义了它的content。 但是与其他View 不同。你可以拿到一些你在其他View中拿不到的信息。

上面说到想要绘制一个大小是父级视图的一半。位置位于父级视图右边距里5像素的视图。现在我们有了GeometryReader, 这就很简单了

image
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.blue)
            
        }
    }
}

GeometryProxy

上面的例子中,闭包中的geometry是一个GeometryProxy类的变量。我们可以通过Inspecting the View Tree(检查视图树结构)这篇文章去了解更多相关内容。

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

public var size: CGSize { get }
public var safeAreaInsets: EdgeInsets { get }
public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
public subscript<T>(anchor: Anchor<T>) -> T where T : Equatable { get }

size属性是父级视图建议的大小

GeometryProxy 把 safeAreaInsets也暴露给了我们。

frame方法暴露给我们了父级视图建议区域的大小位置,可以通过.local,.global,.named()来获取不同的坐标空间。 .named() 用来获取一个被命名的坐标空间。我们可以通过这个命名来获取其他view坐标空间。 Inspecting the View Tree 这篇文章中有具体的使用方法

最后,我们可以通过下标取值来获取一个锚点<T>。这个是GeometryReader的一个炫酷的功能。但也比较繁琐,我将在second part of Inspecting the View Tree有讲解。看完后就会有一个了解。可以获取视图树中任何子级视图的size和x,y. 是不是很强大,那你得先学啊。

吸收另外一个View的Geometry

GeometryReader 功能已经相当强大,但它如果与 .background().overlay()的modifier相结合使用,功能就会更强大。

在我见过的教程中 background 都是以下面这种形式使用的:Text("hello").background(Color.red)
第一眼看,都会以为Color.red是一个颜色参数,它设置了背景色是红色,其实并不是,Color.red是一个View!它的功能就是把父级视图所建议的区域填充为红色。它的父级就是背景。而且背景修改了Text。所以建议Color.red所填充的区域就是Text("Hello")所在的区域。是不是很优美?

.overlay 修改器也是同样的道理,只是它并不是绘制背景,而是绘制前景而已。

我们已经知道了,我们可以给任意一个view使用.Color()方法还有 .background()方法。下面我们将结合GeometryReader,画一个每个角指定不同的半径的矩形的例子来演示如何利用它们。

image

具体实现如下:

struct ContentView : View {
    var body: some View {
        HStack {
            Text("SwiftUI")
                .foregroundColor(.black).font(.title).padding(15)
                .background(RoundedCorners(color: .green, tr: 30, bl: 30))
            
            Text("Lab")
                .foregroundColor(.black).font(.title).padding(15)
                .background(RoundedCorners(color: .blue, tl: 30, br: 30))
            
        }.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)
        }
    }
}

另外,我们对自定义的Overlay设置透明度为0.5,设置在Text上。
代码如下:

Text("SwiftUI")
    .foregroundColor(.black).font(.title).padding(15)
    .overlay(RoundedCorners(color: .green, tr: 30, bl: 30).opacity(0.5))
Text("Lab")
    .foregroundColor(.black).font(.title).padding(15)
    .overlay(RoundedCorners(color: .blue, tl: 30, br: 30).opacity(0.5))

关于鸡和鸡蛋的问题

当你开始使用GeometryReader, 你就会发现所谓的鸡和鸡蛋的问题。
因为GeometryReader需要给子级试图提供可用空间,它首先需要尽可能多的占用空间。 但是子类可能会设置一个小的空间,这时候GeometryReader还是尽可能的保持大。
一旦子级试图确定了其所需空间, 你可能会被迫缩小GeometryReader。这时候子级试图就会GeometryReader计算出的新的大小做出反应。 一个循环就产生了。

所以 当遇到是子级试图依赖父级试图的大小,还是父级试图依赖于子级试图的大小。 可能GeometryReader并不适合解决你的布局问题。由此,你可以看看我的下一篇文章Preferences and Anchor Preferences.

总结

今天所学的GeometryReader让我们的自定义view知道了它们所需的大小和位置。 我们还学习了获取其他view的geometry。
这只是很第一篇官方并没有提及的讲SwiftUI中的布局工具的文章,接下来我们将会深度研究view的数层次,和子级试图如何把数据向上传递。点我查看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容