SwiftUI Center 无支撑框架的内容对齐

SwiftUI Center Content alignment without supporting frames

我在尝试将单个元素居中以模拟带有关闭按钮的导航模式时遇到问题。 我想在不使用 supporting Rectangle 或 spacers.

的情况下将内容居中

我想要实现的是,每当文本增长时,如果它到达左侧有关闭 xmark 按钮的地方,它应该尝试将自己推到右侧可用的地方 space 直到如果两边都没有可用的space,它会到达右边界并自动包裹。

这里有一些图片:

预期结果 1

预期结果 2

当前解决方案短文本

当前解决方案长文本

我尝试使用长文本和短文本来测试内容行为

目前这是代码的开头,基本上我想避免添加蓝色矩形(通常很清楚)

   struct TestAlignmentSwiftUIView: View {
        var body: some View {
            HStack(spacing: 0) {
                Rectangle().fill(Color.blue).frame(width: 44, height: 44)
    
                Text("aaa eee aaa")
                    .background(Color.red)
                    .padding(5)
    
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            .background(Color.green)
        }
    }

到目前为止我已经尝试过,但如果文本组件中的代码增长,则无法解决问题:

  1. 使用 zstack 将文本和关闭按钮放在一起 彼此顶部,但使用 spacer 将按钮推到一边。它适用于小文本或内容,但如果文本变大则无法缩放
    var body: some View {
        ZStack {
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            Text("aaa eee aaa random long very long text that should wrap without overlapping. long text")
                .background(Color.red)
                .frame(maxWidth: .infinity, alignment: .center)
                .padding(5)
                .opacity(0.7)
        }
        .background(Color.green)
    }
  1. 使用对齐指南: 我会创建我自己的居中对齐指南,然后在我放置内容的 vstack 上使用这个自定义对齐方式,再加上一个假的填充矩形,它应该使内容侧的元素居中。

问题是,据我所知,使用 swiftui 只能对齐一个后代元素,并且不支持元素堆栈上的多个自定义对齐方式。所以我只会让文本居中或侧面按钮对齐,而不是一个与中心对齐,另一个与后缘对齐。如果我在它们之间放一个 spacer ,它只会弄乱创建的对齐方式。如果我尝试使用小文本,它们都会被附加。 继承人的代码。尝试评论该按钮,您会看到它会自行居中或在它们之间添加 spacer。

extension HorizontalAlignment {
    private enum MyAlignment: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            d[HorizontalAlignment.center]
        }
    }
    static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
var body: some View {
        VStack(alignment: .myAlignment, spacing: 0) {
            HStack {
                Text("aaa eee aaa random  ")
                    .background(Color.red)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(5)
                    .alignmentGuide(.myhAlignment, computeValue: { dimension in
                        dimension[HorizontalAlignment.center]
                    })

                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: 44, height: 44)
                        .background(Color.yellow)
                })
            }
            .background(Color.green)
            Rectangle()
                .fill(Color.purple)
                .frame(width: 10, height: 10, alignment: .center)

                .alignmentGuide(.myhAlignment, computeValue: { dimension in
                    dimension[HorizontalAlignment.center]
                })
        }
    }

  1. 尝试结合几何 reader and/or 锚首选项来阅读文本内容的大小和侧边按钮的宽度,并手动应用适当的中心偏移,但它似乎太老套了从来没有像预期的那样没有好的结果

If you're familiar with uikit this problem would be resolved using a centerX on the container with a minor layout priority and a right constraint from the center to the close button, and call it a day. But on swiftui it seems soo hard to handle this simple cases.

到目前为止,我还没有找到不在侧面使用可同时处理长文本和短文本的支撑固定框架的解决方案。如果您尝试使用长文本,那 space 是清晰可见的。它会让用户想知道为什么不使用。

¯\ (ツ)

编辑:在答案中添加了可能的解决方案

根据评论中@Yrb 的建议,这是我想出的缩小蓝色尺寸的方法,因此它将以可用 space 为中心。 我在下面添加了一个假文本并跟踪了大小。如果它超过可用 space 我将取差值并缩小蓝色矩形。 要记住的一件事是隐藏内容如果包含一些文本应该有 linelimit 1,否则它会通过包装自身获得更小的尺寸。

而且我只是假设我知道中心对齐的关闭按钮(或至少一侧)的大小,即使我在编译时不知道,我也可以使用首选项键来在 运行 时获取大小,并使其动态变化。

但目前我认为我得到的结果很好。 但老实说,我希望以后能找到更简单的东西。

@State var text: String = "aaa eee aaa"
@State private var fillerWidth: CGFloat = 44
// i assume i know the max size of the close button or at least one side
private let kCloseButtonWidth: CGFloat = 44 

private struct FakeSizeTitlteContentKey: PreferenceKey {
    static var defaultValue: CGFloat { .zero }
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

var body: some View {
    ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
        GeometryReader { parentGeometry in
            titleContent
                .lineLimit(1) // hidden text must not wrap
                .overlay(GeometryReader { proxyFake in
                    Color.clear.border(Color.black, width: 0.3)
                        .preference(key: FakeSizeTitlteContentKey.self, value: proxyFake.frame(in: .local).width
                        .onPreferenceChange(FakeSizeTitlteContentKey.self) { value in
                            let availableW = parentGeometry.frame(in: .local).width
                            let fillSpace = availableW - value - kCloseButtonWidth * 2
                            fillerWidth = min(kCloseButtonWidth, max(0, fillSpace))
                        }
                })
        }
        .hidden()
        VStack {
            HStack(spacing: 0) {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: fillerWidth, height: 44)
                titleContent
                    .background(Color.green)
                    .multilineTextAlignment(.center)
                    .frame(maxWidth: .infinity, alignment: .center)
                Button(action: {}, label: {
                    Image(systemName: "xmark")
                        .padding(15)
                        .frame(width: kCloseButtonWidth, height: kCloseButtonWidth)
                        .background(Color.yellow)
                })
            }
            .coordinateSpace(name: "fullCont")
            .background(Color.green)
            
            TextEditor(text: $text)
                .frame(maxHeight: 150, alignment: .center)
                .border(Color.black, width: 1)
                .padding(15)
            Spacer()
        }
    }
}

@ViewBuilder var titleContent: some View {
    HStack(spacing: 0) {
        Text(text)
            .background(Color.red)
            .padding(.horizontal, 5)
    }
}