使用正确的对齐方式在HStack中对齐两个SwiftUI文本视图

Edw*_*ard 6 ios swiftui

我有一个简单的列表视图,其中包含两行。

每行包含两个文本视图。查看一和查看二。

我想对齐每行的最后一个标签(查看两个),以使名称标签前导对齐并与字体大小无关地保持对齐。

第一个标签(查看一个)也需要对齐。

我尝试在第一个标签(“查看一个”)上设置最小边框宽度,但是它不起作用。设置最小宽度和使文本视图在View One中对齐也似乎是不可能的。

有任何想法吗?在UIKit中,这相当简单。

列表显示

Ima*_*tit 10

在 Swift 5.2 和 iOS 13 中,你可以使用PreferenceKey协议、preference(key:value:)方法和onPreferenceChange(_:perform:)方法来解决这个问题。

您可以View通过 3 个主要步骤来实现OP 提出的代码,如下所示。


#1. 初步实施

import SwiftUI

struct ContentView: View {

    var body: some View {
        NavigationView {
            List {
                HStack {
                    Text("5.")
                    Text("John Smith")
                }
                HStack {
                    Text("20.")
                    Text("Jane Doe")
                }
            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Challenge")
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

#2. 中间实现(设置等宽)

这里的想法是收集Text代表等级的s 的所有宽度,并将其中最宽的分配给 的width属性ContentView

import SwiftUI

struct WidthPreferenceKey: PreferenceKey {

    static var defaultValue: [CGFloat] = []
    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }

}

struct ContentView: View {

    @State private var width: CGFloat? = nil

    var body: some View {
        NavigationView {
            List {
                HStack {
                    Text("5.")
                        .overlay(
                            GeometryReader { proxy in
                                Color.clear
                                    .preference(
                                        key: WidthPreferenceKey.self,
                                        value: [proxy.size.width]
                                    )
                            }
                        )
                        .frame(width: width, alignment: .leading)
                    Text("John Smith")
                }
                HStack {
                    Text("20.")
                        .overlay(
                            GeometryReader { proxy in
                                Color.clear
                                    .preference(
                                        key: WidthPreferenceKey.self,
                                        value: [proxy.size.width]
                                    )
                            }
                        )
                        .frame(width: width, alignment: .leading)
                    Text("Jane Doe")
                }
            }
            .onPreferenceChange(WidthPreferenceKey.self) { widths in
                if let width = widths.max() {
                    self.width = width
                }
            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Challenge")
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

#3. 最终实现(重构)

为了使我们的代码可重用,我们可以将我们的preference逻辑重构为ViewModifier.

import SwiftUI

struct WidthPreferenceKey: PreferenceKey {

    static var defaultValue: [CGFloat] = []
    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }

}

struct EqualWidth: ViewModifier {

    func body(content: Content) -> some View {
        content
            .overlay(
                GeometryReader { proxy in
                    Color.clear
                        .preference(
                            key: WidthPreferenceKey.self,
                            value: [proxy.size.width]
                        )
                }
            )
    }

}

extension View {
    func equalWidth() -> some View {
        modifier(EqualWidth())
    }
}

struct ContentView: View {

    @State private var width: CGFloat? = nil

    var body: some View {
        NavigationView {
            List {
                HStack {
                    Text("5.")
                        .equalWidth()
                        .frame(width: width, alignment: .leading)
                    Text("John Smith")
                }
                HStack {
                    Text("20.")
                        .equalWidth()
                        .frame(width: width, alignment: .leading)
                    Text("Jane Doe")
                }
            }
            .onPreferenceChange(WidthPreferenceKey.self) { widths in
                if let width = widths.max() {
                    self.width = width
                }
            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Challenge")
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

结果如下所示:


Mat*_*ray 8

我找到了一种修复此问题的方法,该方法支持动态类型并且不易破解。答案是使用PreferenceKeysGeometryReader

该解决方案的实质是每个数字Text都将具有一个宽度,该宽度将根据其文本大小来绘制。GeometryReader可以检测到此宽度,然后我们可以将PreferenceKey其冒泡到List本身,在该列表中可以跟踪最大宽度,然后将其分配给每个数字Text的框架宽度。

A PreferenceKey是您创建的具有关联类型的类型(可以是任何与该类型相关的结构Equatable,这是您存储有关首选项的数据的类型),该类型已附加到任何类型,View并且在附加时,它会在视图树中冒泡,并且可以通过使用在祖先视图中收听.onPreferenceChange(PreferenceKeyType.self)

首先,我们将创建我们的PreferenceKey类型及其包含的数据:

struct CenteringColumnPreferenceKey: PreferenceKey {
    typealias Value = [CenteringColumnPreference]

    static var defaultValue: [CenteringColumnPreference] = []

    static func reduce(value: inout [CenteringColumnPreference], nextValue: () -> [CenteringColumnPreference]) {
        value.append(contentsOf: nextValue())
    }
}

struct CenteringColumnPreference: Equatable {
    let width: CGFloat
}
Run Code Online (Sandbox Code Playgroud)

接下来,我们将创建一个名为View的视图CenteringView,该视图将附加到我们要调整大小的背景(在本例中为数字标签)。这将有助于设置首选项,该首选项将通过PreferenceKeys传递此数字标签的首选宽度。

struct CenteringView: View {
    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(
                    key: CenteringColumnPreferenceKey.self,
                    value: [CenteringColumnPreference(width: geometry.frame(in: CoordinateSpace.global).width)]
                )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,列表本身!我们有一个@State变量,它是数字“ column”的宽度(在数字不会直接影响代码中其他数字的意义上,它并不是真正的列)。通过.onPreferenceChange(CenteringColumnPreference.self)侦听首选项中的更改,我们创建了最大宽度并将其存储在宽度状态中。绘制完所有数字标签并由GeometryReader读取其宽度后,宽度将向上传播并通过以下方式分配最大宽度:.frame(width: width)

struct ContentView: View {
    @State private var width: CGFloat? = nil

    var body: some View {
        List {
            HStack {
                Text("1. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(CenteringView())
                Text("John Smith")
            }
            HStack {
                Text("20. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(CenteringView())
                Text("Jane Done")
            }
            HStack {
                Text("2000. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(CenteringView())
                Text("Jax Dax")
            }
        }.onPreferenceChange(CenteringColumnPreferenceKey.self) { preferences in
            for p in preferences {
                let oldWidth = self.width ?? CGFloat.zero
                if p.width > oldWidth {
                    self.width = p.width
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您有多列数据,一种扩展方法是对列进行枚举或为它们建立索引,而@State for width将成为字典,其中每个键都是一列,并.onPreferenceChange与键值进行比较列的最大宽度。

为了显示结果,是打开较大文本时的样子,就像一个超级魅力:)。

关于PreferenceKey和检查视图树的这篇文章对您有很大帮助:https : //swiftui-lab.com/communication-with-the-view-tree-part-1/


Pau*_*l B 7

以下是静态执行此操作的三个选项。

struct ContentView: View {
    @State private var width: CGFloat? = 100

    var body: some View {
        List {
            HStack {
                Text("1. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                // Option 1
                Text("John Smith")
                    .multilineTextAlignment(.leading)
                    //.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
            HStack {
                Text("20. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                // Option 2 (works mostly like option 1)
                Text("Jane Done")
                    .background(Color.green)
                Spacer()
            }
            HStack {
                Text("2000. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                // Option 3 - takes all the rest space to the right
                Text("Jax Dax")
                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是它的外观: 模拟器1

我们可以根据本答案中建议的最长条目计算宽度。

有几个选项可以动态计算宽度。

选项1

import SwiftUI
import Combine

struct WidthGetter: View {
    let widthChanged: PassthroughSubject<CGFloat, Never>
    var body: some View {
        GeometryReader { (g) -> Path in
            print("width: \(g.size.width), height: \(g.size.height)")
            self.widthChanged.send(g.frame(in: .global).width)
            return Path() // could be some other dummy view
        }
    }
}

struct ContentView: View {
    let event = PassthroughSubject<CGFloat, Never>()

    @State private var width: CGFloat?

    var body: some View {
        List {
            HStack {
                Text("1. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .background(WidthGetter(widthChanged: event))
 
                // Option 1
                Text("John Smith")
                    .multilineTextAlignment(.leading)
                    //.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
            HStack {
                Text("20. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .background(WidthGetter(widthChanged: event))
                // Option 2 (works mostly like option 1)
                Text("Jane Done")
                    .background(Color.green)
                Spacer()
            }
            HStack {
                Text("2000. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .background(WidthGetter(widthChanged: event))
                // Option 3 - takes all the rest space to the right
                Text("Jax Dax")
                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
        }.onReceive(event) { (w) in
            print("event ", w)
            if w > (self.width ?? .zero) {
                self.width = w
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

选项 2

import SwiftUI

struct ContentView: View {
    
    @State private var width: CGFloat?
    
    var body: some View {
        List {
            HStack {
                Text("1. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .alignmentGuide(.leading, computeValue: { dimension in
                        self.width = max(self.width ?? 0, dimension.width)
                        return dimension[.leading]
                    })
                
                // Option 1
                Text("John Smith")
                    .multilineTextAlignment(.leading)
                    //.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
            HStack {
                Text("20. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .alignmentGuide(.leading, computeValue: { dimension in
                        self.width = max(self.width ?? 0, dimension.width)
                        return dimension[.leading]
                    })
                // Option 2 (works mostly like option 1)
                Text("Jane Done")
                    .background(Color.green)
                Spacer()
            }
            HStack {
                Text("2000. ")
                    .frame(width: width, alignment: .leading)
                    .lineLimit(1)
                    .background(Color.blue)
                    .alignmentGuide(.leading, computeValue: { dimension in
                        self.width = max(self.width ?? 0, dimension.width)
                        return dimension[.leading]
                    })
                // Option 3 - takes all the rest space to the right
                Text("Jax Dax")
                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .background(Color.green)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果如下所示: 模拟器2


RPa*_*l99 5

您可以只将两个Texts 和 aSpacer放在 an 中HStack。会将Spacer您的Texts 推向左侧,如果任一Texts 由于其内容的长度而改变大小,则所有内容都会自行调整:

HStack {
    Text("1.")
    Text("John Doe")
    Spacer()
}
.padding()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

从技术上讲, sText是居中对齐的,但由于视图会自动调整大小,并且只占用与其中的文本一样多的空间(因为我们没有明确设置框架大小),并且被 推到左侧,因此它们Spacer出现左对齐。与设置固定宽度相比,这样做的好处是您不必担心文本被截断。

另外,我添加了填充以HStack使其看起来更好,但如果您想调整 sText彼此之间的距离,可以手动设置其任意一侧的填充。(您甚至可以设置负填充以使项目彼此之间的距离比其自然间距更近)。

编辑

没有意识到OPText也需要第二个垂直对齐。我有办法做到这一点,但它的“hacky”并且如果没有更多的工作就无法用于更大的字体大小:

这些是数据对象:

class Person {
    var name: String
    var id: Int
    init(name: String, id: Int) {
        self.name = name
        self.id = id
    }
}

class People {
    var people: [Person]
    init(people: [Person]) {
        self.people = people
    }
    func maxIDDigits() -> Int {
        let maxPerson = people.max { (p1, p2) -> Bool in
            p1.id < p2.id
        }
        print(maxPerson!.id)
        let digits = log10(Float(maxPerson!.id)) + 1
        return Int(digits)
    }
    func minTextWidth(fontSize: Int) -> Length {
        print(maxIDDigits())
        print(maxIDDigits() * 30)
        return Length(maxIDDigits() * fontSize)
    }
}
Run Code Online (Sandbox Code Playgroud)

这是View

var people = People(people: [Person(name: "John Doe", id: 1), Person(name: "Alexander Jones", id: 2000), Person(name: "Tom Lee", id: 45)])
var body: some View {   
    List {
        ForEach(people.people.identified(by: \.id)) { person in                
            HStack {
                Text("\(person.id).")
                    .frame(minWidth: self.people.minTextWidth(fontSize: 12), alignment: .leading)
                Text("\(person.name)")

            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要使其适用于多种字体大小,您必须获取字体大小并将其传递到minTextWidth(fontSize:).

再次,我想强调这是“hacky”,可能违反 SwiftUI 原则,但我找不到内置的方法来完成您要求的布局(可能是因为Text不同行中的 s 不与每个行交互)其他,所以他们无法知道如何保持彼此垂直对齐)。

编辑 2 上面的代码生成:

代码结果


mbx*_*Dev 5

我只需要处理这个。依赖固定宽度的解决方案frame不适用于动态类型,因此我无法使用它们。我解决这个问题的方法是,将柔性项(在这种情况下为左侧的数字)放在一个ZStack包含允许的内容最多的占位符的位置,然后将占位符的不透明度设置为0:

ZStack {
    Text("9999")
        .opacity(0)
        .accessibility(visibility: .hidden)
    Text(id)
}
Run Code Online (Sandbox Code Playgroud)

它很hacky,但至少它支持动态类型?

带占位符的ZStack

完整示例如下!

import SwiftUI

struct Person: Identifiable {
    var name: String
    var id: Int
}

struct IDBadge : View {
    var id: Int
    var body: some View {
        ZStack(alignment: .trailing) {
            Text("9999.") // The maximum width dummy value
                .font(.headline)
                .opacity(0)
                .accessibility(visibility: .hidden)
            Text(String(id) + ".")
                .font(.headline)
        }
    }
}

struct ContentView : View {
    var people: [Person]
    var body: some View {
        List(people) { person in
            HStack(alignment: .top) {
                IDBadge(id: person.id)
                Text(person.name)
                    .lineLimit(nil)
            }
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static let people = [Person(name: "John Doe", id: 1), Person(name: "Alexander Jones", id: 2000), Person(name: "Tom Lee", id: 45)]
    static var previews: some View {
        Group {
            ContentView(people: people)
                .previewLayout(.fixed(width: 320.0, height: 150.0))
            ContentView(people: people)
                .environment(\.sizeCategory, .accessibilityMedium)
                .previewLayout(.fixed(width: 320.0, height: 200.0))
        }
    }
}
#endif
Run Code Online (Sandbox Code Playgroud)