translation
Swiftui-lab.com/geometryrea…


For more content, please follow the public account “Swift Garden”.


Like articles? How about a 🔺💛➕ triple? Follow the column, follow me 🚀🚀🚀

Most of the time, SwiftUI works its layout magic 🧚♀️, life is so wonderful for us 🏖. However, there are many times when we need more control over our custom views. At these times, we have several tools 🛠 available. The first one we need to explore is GeometryReader.

What does the parent view want?

When you code a custom view, you usually don’t need to worry about its surroundings or its size. For example, suppose you want to create a view that draws rectangles. All you have to do is draw a Rectangle. It is drawn at the size and location specified by the parent view.

In the following example, we have a VStack of size 150×100, which first places a Text at the top, and then the rest of the space is taken up by MyRectangle(). This view is so compliant that it draws exactly the blue we gave it. A pixel not much, a pixel not much:



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)
    }
}Copy the code

As you can see, the view MyRectangle() doesn’t care about size. It only does one thing, draw a rectangle, and SwiftUI will figure out where and at what size its parent view needs to place it. In this case, the VStack that contains it is such a parent view.

If you want to learn more about how parents decide the size and location of their children, I highly recommend you watch 2019 WWDC Session 237 (Building Custom Views with SwiftUI).

In many cases, that will be enough. However, as mentioned in Session 237, the parent view recommends size and location, but it decides exactly how and where the child view is drawn. If the subview is not satisfied with the advice provided to it, it ignores the advice.

For example, if you want your custom view to draw a rectangle half the size suggested by the parent view, and you want it to be placed 5 points from the right edge, do it, and no one can stop you 😝.

So how do we code here? Actually, this is not complicated, let GeometryReader solve it.


What does the subview do?

Let’s take a look at Apple’s documentation for GeometryReader:

A container view that defines its content as a function of its own size and coordinate space.

This explanation is quite detailed compared to the other documentation, which is almost always “No overview available.”

So what does that mean? Simply put, GeometryReader is another view. Surprised? In SwiftUI, almost everything is a view. In the example below, GeometryReader lets you define its contents. But unlike other views. You can take information that you can’t get in other views and do your own thing with it.

As mentioned above, we want to draw a view that is half the size of the parent view’s suggested size and place it 5 points from the right edge of the suggested area. With GeometryReader, this is simple:



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)
            
        }
    }
}Copy the code


GeometryProxy

If you look closely at the example above, you will see a closure that contains the geometry variable, which is of type GeometryProxy. You can learn more about that in this article about Inspecting the View Tree.

There are two computational properties in GeometryProxy class, a method, and a getter for subscripts.

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 }Copy the code

The first attribute is straightforward. As you can see in the example, the size attribute is the recommended size of the parent view. We can cash it in or we can’t. Remember, it depends on the subview.

GeometryProxy also exposed the safeAreaInsets to us.

The frame method exposes the rectangle of the suggested region of our parent view and can get different coordinate Spaces by passing in.local,.global,.named(). Local and global coordinate Spaces without explanation,.named() is used to get a named coordinate space. We can get the coordinate space of other views by name. This article about Inspecting the View Tree shows how to use it.

Finally, we can obtain an anchor point by subscript value. Piqued your curiosity, didn’t it? 🤩 This is a really cool feature of GeometryReader, but it’s also a clunky topic that I cover in Part 2 of Inspecting the View Tree. For now, what you need to know is that with subscripts, you can get the size and position of any subview in the view tree. Is it powerful? Be sure to come back and learn more about it.


Absorb the Geometry of another view

The GeometryReader feature is already very powerful, but it would be even more powerful if it were used in combination with.background() or.overlay() modifier.

In most tutorials I’ve seen, background is used in the simplest way. For example: Text(“hello”).background(color.red). At first glance, we might think that color. red is a Color parameter, but it is not. Color.red is another view! Its only function is to fill in the area suggested to it by the parent view in red. Because the parent view is the background, and the background changes the Text, it is recommended that the area filled with color. red be the area occupied by Text(“hello”). Isn’t it elegant?

Overlay Modifier works the same way, except instead of drawing the background to the modified view, it is drawn on top of it.

You may be aware that you can cram any view into.background(), not just Color(). To demonstrate how we can take advantage of this, we will combine GeometryReader and draw a rectangular background while specifying different fillet radii for the corners of the rectangle:



The specific implementation is as follows:

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
                
                // Ensure that the fillet radius does not exceed the limit
                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)
        }
    }
}Copy the code

In addition, we could apply the same view, but with 0.5 transparency, on top of the text, using.overlay() :



The code is as follows:

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))Copy the code


It’s a chicken and egg problem

When you start using GeometryReader, you quickly find a chicken and egg problem. Because GeometryReader needs to provide all available space for the subviews, it first needs to take up as much space as possible. Then, as we know, the subview might decide to use less space. As a result, GeometryReader was kept as large as possible.

You might think that once the subview has determined the required space, you can force the GeometryReader to shrink accordingly. But this causes the child view to recalculate the new size based on GeometryReader… A cycle is created.

So which came first? Does the child depend on the size of the parent, or does the parent depend on the size of the child? But when you face a problem like this, you may need to step back and rethink your solution, and creativity is the key to getting out of this predicament. Who knows, maybe GeometryReader is not the right place to solve this layout problem. Fortunately, GeometryReader is not the only layout tool available. You can find an example in my next article Preferences and Anchor Preferences.


conclusion

Today we learned that GeometryReader can be used to build custom views that are aware of their size and location on the screen. We also learned some useful hints about “stealing” geometric information from other views for our use.

This is just the first of many articles to explain SwiftUI’s layout tools that aren’t officially mentioned. Next, we’ll dive into the view hierarchy and see how subviews pass data up.



My official account here is Swift and computer programming related articles, as well as excellent foreign article translation, welcome to follow ~