按比例将空 space 分成 HStack/VStack

Divide empty space in HStack/VStack proportionally

我有一个带间隔符和文本的 HStack。

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

现在我希望第一个 Spacer 占用 X% 的可用空间 space(不包括 Text 占用的 space),底部占用其余部分。

如果我使用 Geometry Reader,并对第一个 Spacer -

执行如下操作
Spacer()
    .frame(width: geometry.size.width * (X/100))

没有考虑Text占用的space。

有什么方法可以在 spacers 之间划分“可用”space?

您要找的是PreferenceKeys。本质上,它们会跟踪某些视图的大小,然后您可以使用这些大小来计算您需要的内容。它们最常用于保持多个视图的大小相同,即使它们在其他情况下会有不同的大小。我给你一个长而短的解决方案:

长解:

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 = [=10=]
        }
        .onPreferenceChange(HStackWidthPrefKey.self) {
            // This keeps track of the change of the size
            hStackWidth = [=10=]
       }
    }
}

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())
        }
    }
}

简短的解决方案感谢 FiveStarBlog 和 Twitter 上的@ramzesenok:

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)
    }
}

并使用扩展名:

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
        }
    }
}

他们做同样的事情,但扩展处理得非常巧妙,没有您认为的额外代码。我发布它是为了让您了解 PreferenceKeys 是如何工作的。

这是一个不错的解决方案,只需要一个 GeometryReader

有关详细信息,请查看 this 文章。

struct MyView: View {
    @State private var offset: CGFloat?

    var body: some View {
        HStack {
            Spacer()
            Text("Some Text")
                .anchorPreference(key: BoundsPreference.self, value: .bounds) { [=10=] }
            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[[=10=]].width))
                }
            }
        }
        .onPreferenceChange(OffsetPreference.self) {
            offset = [=10=]
        }
    }

    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()
    }
}