As I get to know SwiftUI more and more deeply, I find that SwiftUI is not as simple as it seems on the surface. At the beginning of learning SwiftUI, what we see is often the most intuitive representation floating on the water. As we dive down, we see those interesting and profound things full of charm. Perhaps features that we thought would be difficult to implement with SwiftUI are now very easy.

For frame, a lot of people think it’s too easy, and anyone who’s done iOS development knows what frame is, what bounds is, but in SwiftUI, it’s almost completely different from what we’re used to with frames. SwiftUI essentially runs on a new set of rules, and for SwiftUI, frame certainly has its own rules.

In the author’s article, he did not explain SwiftUI in the layout of the basic principles, for some readers, there may be a bit difficult, understanding the original text in this article, I will use part of the space, to explain the basic principles of layout in SwiftUI, combined with these principles, back to the frame, will make such a wonder: “So that’s it!!”

What is the frame

In SwiftUI, frame() is a modifier, **modifier does not actually modify view in SwiftUI. ** In most cases, when we apply a modifier to a view, we actually create a new view.

In SwiftUI, views do not have a concept of a frame, but they have a concept of bounds, that is, each view has a range and size, and their bounds cannot be changed manually.

When the frame of a view changes, the size of its child does not necessarily change. For example, when we change the width of a container VStack, the layout of its child may or may not change. We’ll check this out below.

Remember this sentence, each view has its own idea about the size it needs, which is the core idea of the following content.

Behaviors

In SwfitUI, views have different behaviors when calculating their size, which can be divided into 4 categories:

  • Similar to theVstackThey try to keep their internal content as complete as possible, but don’t ask for extra space
  • Similar to theTextThis returns only the size it needs, and if it doesn’t have enough size, it’s smart enough to do something extra, like wrap lines, etc
  • Similar to theShapeThis can be used in any size given
  • There are also views that may be outside the parent control

There are other special exceptions, such as Spacer, whose characteristics depend on which container or axis he belongs to. When he’s in the VStack, he tries to occupy all of the remaining vertical space and zero horizontal space, and in the HStack, he does the opposite.

We’ll see how these different behaviors are manifested in the layout principles in the next section.

Layout principles

Think about my next three sentences:

  • When a view is laid out, its parent view gives a suggested size
  • If the view has a child, ask the child for the proposed size. The child returns a size based on its behavior. If there is no child, return a size based on its behavior
  • Use this size to layout the parent view

Let’s look at a simple example:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")}}Copy the code

The layout process is bottom-up and we calculate the size of the ContentView

  • The parent view of ContentView gives it a suggested size equal to the size of the full screen
  • The ContentVIew takes the size and asks its child, and the Text returns the size it needs
  • Use this size to layout in the parent view

The size of a ContentView is the same as that of a Text:

Let’s add a little more difficulty to this:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(width: 200, height: 100)
            .background(Color.green)
            .frame(width: 400, height: 200)
            .background(Color.orange.opacity(0.5))}}Copy the code

The above code is pretty much representative of any custom view, but remember that when thinking about layout, it’s bottom-up.

So let’s think about the ContentVIew, its parent gives it the suggested size of the entire screen, which we’ll call size0, and it asks its child, and its child is the background at the bottom, which doesn’t know its own size, So he goes ahead and asks his child with size0, his child is a frame, returns width400, height200, so background tells the ContentView that he needs width400, height200, So the final ContentView size is width400, height200.

Obviously, we also calculated the size of the bottom background. Note that the Color inside is also a view, Color itself is a Shape, and the background returns a transparent view

Height: 200, width: 400, height: 200, width: 200, height: 200 100, so the background size is width: 200, height: 100

Width: 200, height: 100, width: 200, height: 100, width: 200, height: 100, height: 100

Let’s take a look at the layout:

Width: 200, height: 100, width: 200, height: 100, width: 200, height: 100, width: 200

After understanding the knowledge of these layouts, we read the article again, there will be no doubt, in the usual development, for the occurrence of strange layout problems, can also know what is the cause of these problems.

Basic usage

The most frequent ways we use frame in development are:

func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)
Copy the code

We wrote a previous article on alignment; [SwiftUI AlignmentGuides] (zhuanlan.zhihu.com/p/145821031), haven’t seen the students must go to have a look at, in SwiftUI, understand the Alignment Guides usage, can let us development effect more efficient.

When we change width or height, most of the time the layout looks exactly the way we expect it to. On the surface, we can set width and height using this method, but actually frame cannot change the size of the view directly.

In the previous section, we demonstrated the three steps of layout. Frame can precisely change the size value of parent or child. When the view asks the child, if it encounters a frame, it will directly use the size returned by the child.

Next we will show a small demo, when we change the width of the parent view, the child view does not necessarily change with the width of the parent view. As you will see, the three steps of the layout again validate these changes.

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

Width: Max (proxy.sie.width, 120), height: Max (proxy.sie.height, 120)), which uses the size limit of the frame when calculating the size, so the effect shown above is exactly what we expect.

Other USES

In addition to the basic usage above, there are the following usages:

func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)
Copy the code

Obviously, these parameters can be divided into three groups:

  • MinWidth, idealWidth, maxWidth
  • MinHeight, idealHeight, maxHeight
  • alignment

The last group has been clearly explained in other articles. The first group and the second group are basically the same in principle. Let’s focus on the first group to make a detailed explanation.

When assigning values to minWidth, idealWidth, and maxWidth, we must follow the numerical increment rule, otherwise xcode will give an error message.

MinWidth is the smallest width, idealWidth is the most appropriate width, and maxWidth is the largest width. Usually if we use this method, we only need to consider minWidth and maxWidth.

When calculating size, they follow the following process:

In fact, if you understand the layout of the three principles, then understand the process is very simple, frame modifier by calculating minWidth, maxWidth and child size, you can look at the above rules return a size, The view uses this size as its size in the parent view.

Let’s look at a few examples:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(minWidth: 40, maxWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}
Copy the code

In the code above, we set both minWidth and maxWidth, and the background size returns 400:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(minWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}
Copy the code

If only minWidth is set, then background size returns 400:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}

Copy the code

As long as maxWidth is set, background returns the value of maxWidth.

All you need to do is write your own code and experiment with the various scenarios in this process. In short, follow the three principles of layout mentioned above to understand layout.

Fixed Size Views

FixedSize () ‘we must have seen. FixedSize ()’ this modifier, on the surface, he seems to be used in Text, used to fix the width of Text, I believe many students should be this idea, in this section, we will thoroughly understand what it is exactly a thing.

func fixedSize(a) -> some View
func fixedSize(horizontal: Bool, vertical: Bool) -> some View
Copy the code

In SwiftUI, any View can be modifer. After applying this modifier, the layout system will return the corresponding idealWIdth or idealHeight when returning size.

Let’s start with a piece of code:

struct ContentView: View {
    var body: some View {
        Text("This text is quite long, after a certain number of words, it is more than one line!!")
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
Copy the code

According to the three layout principles, the width of the green frame is 200 and the height is 100, and the parent view with the blue frame provides 200 width and 100 height. Its child and text return the size of the basket frame under the restriction of 200 width and 100 height, so the size of the basket frame and text are the same. This result accords with that of our analysis.

Let’s modify the code:

struct ContentView: View {
    var body: some View {
        Text("This text is quite long, after a certain number of words, it is more than one line!!")
            .fixedSize(horizontal: true, vertical: false)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
Copy the code

When a. Border (color.blue) is asked for the child’s size, the child will return its idealWidth. We did not give a specified idealWidth. Each view has its own idealWidth inside it.

So let’s verify that we give it an explicit idealWidth:

struct ContentView: View {
    var body: some View {
        Text("This text is quite long, after a certain number of words, it is more than one line!!")
            .frame(idealWidth: 300)
            .fixedSize(horizontal: true, vertical: false)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
Copy the code

Therefore, when we want to fix the size of a certain axis of a view, fixedSize modifier is a sharp tool.

application

I wrote a small demo to demonstrate fixedSize, below is the complete code:

struct ExampleView: View {@State private var width: CGFloat = 150
    @State private var fixedSize: Bool = true
    
    var body: some View {
        GeometryReader { proxy in
            
            VStack {
                Spacer(a)VStack {
                    LittleSquares(total: 7)
                        .border(Color.green)
                        .fixedSize(horizontal: self.fixedSize, vertical: false)
                }
                .frame(width: self.width)
                .border(Color.primary)
                .background(MyGradient())
                
                Spacer(a)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)
                }
            }
        }.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))}}Copy the code

The running effect is as follows:

The code above is actually very simple. If idealWidth is used to hold the width of the view, the width of the view will not change, which can be useful in some scenarios.

The core code in the above example is:

.frame(idealWidth: (5 + self.sqSize) * CGFloat(self.total), maxWidth: (5 + self.sqSize) * CGFloat(self.total))
Copy the code

Layout Priority

In SwiftUI, the default layout priority of all views is 0. For views at the same level, the system will layout them in order. When using.layourPriority() to change the layout priority, the system will layout the views with higher priority first.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("The moon shines before my bed, and I think it's frost on the ground.")
                .background(Color.green)
            Text("Look up at the moon, look down and think of home.")
                .background(Color.blue)
        }
        .frame(width: 100, height: 100)}}Copy the code

As you can see, these two texts have the same priority, so they split the layout space equally. Let’s give the second text a higher priority:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("The moon shines before my bed, and I think it's frost on the ground.")
                .background(Color.green)
            Text("Look up at the moon, look down and think of home.")
                .background(Color.blue)
                .layoutPriority(1)
        }
        .frame(width: 100, height: 100)}}Copy the code

You can clearly see that the layout of the second text takes precedence. In line with our expectations.

conclusion

In this article, we have covered the use of frame, fixedSize, and layoutPriority. To understand the use of frame, fixedSize, and layoutPriority, you must understand three principles of layout:

  • The parent view provides a suggested size
  • The view calculates a size based on its characteristics and its child
  • Use this size to layout in the parent view

Note: the above content referred to the website https://swiftui-lab.com/frame-behaviors/, if any infringement, immediately deleted.