Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Using GeometryReader Without Modifying The View Size

Tags:

swift

swiftui

I have a header view which extends its background to be under the status bar using edgesIgnoringSafeArea. To align the content/subviews of the header view correctly, I need the safeAreaInsets from GeometryReader. However, when using GeometryReader, my view doesn't have a fitted size anymore.

Code without using GeometryReader

struct MyView : View {
    var body: some View {
        VStack(alignment: .leading) {
            CustomView()
        }
        .padding(.horizontal)
        .padding(.bottom, 64)
        .background(Color.blue)
    }
}

Preview

Expected

Code using GeometryReader

struct MyView : View {
    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .leading) {
                CustomView()
            }
            .padding(.horizontal)
            .padding(.top, geometry.safeAreaInsets.top)
            .padding(.bottom, 64)
            .background(Color.blue)
            .fixedSize()
        }
    }
}

Preview

enter image description here

Is there a way to use GeometryReader without modifying the underlying view size?

like image 487
Infinity Avatar asked Jul 02 '19 19:07

Infinity


1 Answers

I tried with the previewLayout and I see what you mean. However, I think the behavior is as expected. The definition of .sizeThatFits is:

Fit the container (A) to the size of the preview (B) when offered the size of the device (C) on which the preview is running.

I intercalated some letters to define each part and make it more clear:

A = the final size of the preview.

B = The size of what you are modifying with .previewLayout(). In the first case, it's the VStack. But in the second case, it's the GeometryReader.

C = The size of the screen of the device.

Both views act differently, because VStack is not greedy, and only takes what it needs. GeometryReader, on the other side, tries to have it all, because it does not know what its child will want to use. If the child wants to use less, it can do it, but it has to start by being offered everything.

Perhaps if you edit your question to explain exactly what you would like to accomplish, I can refine my answer a little.

If you would like GeometryReader to report the size of the VStack. you may do so by putting it inside a .background modifier. But again, I am not sure what's the goal, so maybe that's a no go.

I have written an article about the different uses of GeometryReader. Here's the link, in case it helps: https://swiftui-lab.com/geometryreader-to-the-rescue/


UPDATE

Ok, with your additional explanation, here you have a working solution. Note that the Preview will not work, because safeInsets are reported as zero. On the simulator, however, it works fine:

As you will see, I use view preferences. They are not explained anywhere, but I am currently writing an article about them that I will post soon.

It may all look too verbose, but if you find yourself using it too often, you can encapsulate it inside a custom modifier.

enter image description here

import SwiftUI

struct InsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }

    typealias Value = CGFloat
}

struct InsetGetter: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle().preference(key: InsetPreferenceKey.self, value: geometry.safeAreaInsets.top)
        }
    }
}

struct ContentView : View {
    var body: some View {
        MyView()

    }
}

struct MyView : View {
    @State private var topInset: CGFloat = 0

    var body: some View {

        VStack {
            CustomView(inset: topInset)
                .padding(.horizontal)
                .padding(.bottom, 64)
                .padding(.top, topInset)
                .background(Color.blue)
                .background(InsetGetter())
                .edgesIgnoringSafeArea(.all)
                .onPreferenceChange(InsetPreferenceKey.self) { self.topInset = $0 }

            Spacer()
        }

    }
}

struct CustomView: View {
    let inset: CGFloat

    var body: some View {
        VStack {
            HStack {
                Text("C \(inset)").color(.white).fontWeight(.bold).font(.title)
                Spacer()
            }

            HStack {
                Text("A").color(.white)
                Text("B").color(.white)
                Spacer()
            }
        }

    }
}
like image 81
kontiki Avatar answered Sep 18 '22 18:09

kontiki