skip to Main Content

I have a HStack with Spacers and a Text.

HStack {
    Spacer()
    Text("Some Text")
    Spacer()
}

Now I want the first Spacer to take X% of the available space (excluding the space taken by Text) and the bottom to take the rest.

If I use Geometry Reader, and do something like below to the first Spacer –

Spacer()
    .frame(width: geometry.size.width * (X/100))

It doesn’t take into account the space occupied by the Text.

Is there any way to divide the "available" space between spacers?

2

Answers


  1. What you are looking for are PreferenceKeys. Essentially, they keep track of the sizes of certain views, and then you can use those sizes to compute what you need. They are most commonly used to keep multiple views the same size, even though they would otherwise have different sizes. I am giving you a long and short solution on this:

    Long solution:

    struct SpacerPrefKeyView: View {
        
        @State private var textWidth: CGFloat = 10
        @State private var hStackWidth: CGFloat = 10
        
        let X: CGFloat = 20
    
        var body: some View {
            HStack {
                Spacer()
                    .frame(width: (hStackWidth - textWidth) * (X/100))
                Text("Hello, World!")
                    .background(GeometryReader { geometry in
                        Color.clear.preference(
                            //This sets the preference key value with the width of the background view
                            key: TextWidthPrefKey.self,
                            value: geometry.size.width)
                    })
    
                Spacer()
            }
            .background(GeometryReader { geometry in
                Color.clear.preference(
                    //This sets the preference key value with the width of the background view
                    key: HStackWidthPrefKey.self,
                    value: geometry.size.width)
            })
            .onPreferenceChange(TextWidthPrefKey.self) {
                // This keeps track of the change of the size
                textWidth = $0
            }
            .onPreferenceChange(HStackWidthPrefKey.self) {
                // This keeps track of the change of the size
                hStackWidth = $0
           }
        }
    }
    
    private extension SpacerPrefKeyView {
        struct TextWidthPrefKey: PreferenceKey {
            static let defaultValue: CGFloat = 0
            
            static func reduce(value: inout CGFloat,
                               nextValue: () -> CGFloat) {
                value = max(value, nextValue())
            }
        }
        
        struct HStackWidthPrefKey: PreferenceKey {
            static let defaultValue: CGFloat = 0
            
            static func reduce(value: inout CGFloat,
                               nextValue: () -> CGFloat) {
                value = max(value, nextValue())
            }
        }
    }
    

    The Short Solution thanks to FiveStarBlog and @ramzesenok on Twitter:

    struct SpacerPrefKeyView: View {
        
        @State private var textSize: CGSize = CGSize(width: 10, height: 10)
        @State private var hStackSize: CGSize = CGSize(width: 10, height: 10)
        
        let X: CGFloat = 20
    
        var body: some View {
            HStack {
                Spacer()
                    .frame(width: (hStackSize.width - textSize.width) * (X/100))
                Text("Hello, World!")
                    .copySize(to: $textSize)
                Spacer()
            }
            .copySize(to: $hStackSize)
        }
    }
    

    And use the extension:

    extension View {
        func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
            background(
                GeometryReader { geometryProxy in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
                }
            )
                .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
        }
        
        func copySize(to binding: Binding<CGSize>) -> some View {
            self.readSize { size in
                binding.wrappedValue = size
            }
        }
    }
    

    They do the same thing, but the extension handles it very neatly without the extra code in your view. I posted it so that you could see how PreferenceKeys work.

    Login or Signup to reply.
  2. Here is a decent solution that requires just one GeometryReader.

    For more information, check this article.

    struct MyView: View {
        @State private var offset: CGFloat?
    
        var body: some View {
            HStack {
                Spacer()
                Text("Some Text")
                    .anchorPreference(key: BoundsPreference.self, value: .bounds) { $0 }
                Spacer(minLength: offset)
            }
            .backgroundPreferenceValue(BoundsPreference.self) { preferences in
                GeometryReader { g in
                    preferences.map {
                        Color.clear.preference(key: OffsetPreference.self, value: offset(with: g.size.width - g[$0].width))
                    }
                }
            }
            .onPreferenceChange(OffsetPreference.self) {
                offset = $0
            }
        }
    
        private func offset(with widthRemainder: CGFloat) -> CGFloat {
            widthRemainder * (100.0 - X) / 100.0
        }
    }
    
    private struct BoundsPreference: PreferenceKey {
        typealias Value = Anchor<CGRect>?
        static var defaultValue: Value = nil
        
        static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
            value = nextValue()
        }
    }
    
    private struct OffsetPreference: PreferenceKey {
        typealias Value = CGFloat?
        static var defaultValue: Value = nil
        
        static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
            value = nextValue()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search