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

时间:2019-06-13 20:44:32

标签: ios swiftui

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

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

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

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

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

有什么想法吗?在UIKit中,这相当简单。

List View

11 个答案:

答案 0 :(得分:9)

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

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

PreferenceKey是您创建的具有关联类型的类型(可以是符合Equatable的任何结构,这是您存储有关首选项的数据的类型),该类型附加到任何{{1 }},并且在附加时,它会在视图树中冒泡,并且可以使用View在祖先视图中收听。

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

.onPreferenceChange(PreferenceKeyType.self)

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

CenteringView

最后,列表本身!我们有一个@State变量,它是数字“ column”的宽度(在数字不会直接影响代码中其他数字的意义上,它并不是真正的列)。通过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)] ) } } } ,我们可以监听所创建的首选项中的更改,并将最大宽度存储在宽度状态中。绘制完所有数字标签并由GeometryReader读取其宽度之后,宽度将向上传播,并且最大宽度由.onPreferenceChange(CenteringColumnPreference.self)

分配
.frame(width: width)

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

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

有关PreferenceKey和检查视图树的这篇文章极大地帮助了我们:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

答案 1 :(得分:9)

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

您可以通过以下三个主要步骤来实现OP提出的View的代码。


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

}

#2。中间实现(设置宽度相等)

这里的想法是收集代表排名的Text的所有宽度,并将其中最宽的宽度分配给width的{​​{1}}属性。

ContentView

#3。最终实现(重构)

要使代码可重复使用,我们可以将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") } } } 逻辑重构为preference

ViewModifier

结果如下:

答案 2 :(得分:4)

这里有三种静态选择的方法。

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

这是它的外观: simulator1

我们可以根据this answer中建议的最长条目来计算宽度。

动态宽度计算可以通过以下方式完成:

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

结果如下所示: simulator2

答案 3 :(得分:1)

您可以为数字Text视图设置固定宽度。它使此Text组件具有固定的大小。

image

HStack {
        Text(item.number)
            .multilineTextAlignment(.leading)
            .frame(width: 30)
        Text(item.name)
}

此解决方案的缺点是,如果在那里有较长的文本,它将被包裹并以“ ...”结尾,但是在那种情况下,我认为您可以粗略估计出哪个宽度就足够了。 / p>

答案 4 :(得分:1)

您可以只拥有两个Text,然后在Spacer中拥有一个HStackSpacer会将您的Text推到左侧,并且如果Text的任何一个由于其内容的长度而改变大小,一切都会自动调整:

HStack {
    Text("1.")
    Text("John Doe")
    Spacer()
}
.padding()

enter image description here

Text在技术上是居中对齐的,但是由于视图会自动调整大小,并且仅占据其中的文本空间(因为我们没有明确设置框架大小),因此将其推入在Spacer的左侧,它们显示为左对齐。与设置固定宽度相比,这样做的好处是您不必担心文本会被截断。

此外,我在HStack中添加了填充以使其看起来更好,但是如果您要调整Text彼此之间的距离,可以在任意一个上手动设置填充它的侧面。 (您甚至可以设置负填充以使项目彼此之间的距离超出其自然间距)。

修改

没有意识到OP也需要第二个Text垂直对齐。我有办法做到这一点,但是它的“ 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)
    }
}

这是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)")

            }
        }
    }
}

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

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

编辑2 上面的代码生成以下代码:

Code result

答案 5 :(得分:0)

HStack {
            HStack {
                Spacer()
                Text("5.")
            }
            .frame(width: 40)

            Text("Jon Smith")
        }

但这仅适用于固定宽度。 由于.frame(minWidth: 40)

Space()将填满整个视图

.multilineTextAlignment(.leading)对我的测试没有任何影响。

答案 6 :(得分:0)

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

ZStack {
    Text("9999")
        .opacity(0)
        .accessibility(visibility: .hidden)
    Text(id)
}

这很hacky,但至少它支持动态类型?‍♂️

ZStack with placeholder

下面是完整示例! ?

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

答案 7 :(得分:0)

尝试使它工作一整天后,我想出了以下解决方案:

example picture

编辑:Link to Swift Package

import SwiftUI

fileprivate extension Color {
    func exec(block: @escaping ()->Void) -> Self {
        block()
        return self
    }
}

fileprivate class Deiniter {
    let block: ()->Void
    init(block: @escaping ()->Void) {
        self.block = block
    }
    deinit {
       block()
    }
}
struct SameWidthContainer<Content: View>: View {
    private var id: UUID
    private let deiniter: Deiniter
    @ObservedObject private var group: WidthGroup
    private var content: () -> Content
    
    init(group: WidthGroup, content: @escaping ()-> Content) {
        self.group = group
        self.content = content
        
        let id = UUID()
        self.id = id
        

        WidthGroup.widths[group.id]?[id] = 100.0
        self.deiniter = Deiniter() {
            WidthGroup.widths[group.id]?.removeValue(forKey: id)
        }
    }
    
    
    var body: some View {
        ZStack(alignment: .leading) {
            Rectangle()
                .frame(width: self.group.width, height: 1)
                .foregroundColor(.clear)
            
            content()
                .overlay(
                    GeometryReader { proxy in
                        Color.clear
                            .exec {
                                WidthGroup.widths[self.group.id]?[self.id] = proxy.size.width
                                let newWidth = WidthGroup.widths[group.id]?.values.max() ?? 0
                                if newWidth != self.group.width {
                                    self.group.width = newWidth
                                }
                            }
                    }
                )
        }
    }
}

class WidthGroup: ObservableObject {

    static var widths: [UUID: [UUID: CGFloat]] = [:]
    @Published var width: CGFloat = 0.0
    
    let id: UUID
    
    init() {
        let id = UUID()
        self.id = id
        
        WidthGroup.widths[id] = [:]
    }
    
    deinit {
        WidthGroup.widths.removeValue(forKey: id)
    }
}

struct SameWidthText_Previews: PreviewProvider {
    
    private static let GROUP = WidthGroup()
    
    static var previews: some View {
        Group {
            SameWidthContainer(group: Self.GROUP) {
                Text("One")
            }
                
            SameWidthContainer(group: Self.GROUP) {
                Text("Two")
            }
            SameWidthContainer(group: Self.GROUP)  {
                Text("Three")
            }
        }
    }
}

然后像这样使用它:

struct SomeView: View {
    
    @State private var group1 = WidthGroup()
    @State private var group2 = WidthGroup()
    
    var body: some View {
        VStack() {
            ForEach(9..<12) { index in
                HStack {
                    SameWidthContainer(group: group1) {
                        Text("All these will have same width in group 1 \(index)")
                    }
                    
                    Text("Some other text")
                    
                    SameWidthContainer(group: group2) {
                        Text("All these will have same width in group 2 \(index)")
                    }
                }
            }
        }
    }
}

如果其中一个视图增大或缩小,则同一组中的所有视图都将随之增大/缩小。我只是想让它工作,所以我没有做太多尝试。 有点骇客,但是,嘿,除了骇客,这似乎不是另一种方式。

答案 8 :(得分:0)

如果您可以限制1行:

Group {
    HStack {
        VStack(alignment: .trailing) {
            Text("Vehicle:")
            Text("Lot:")
            Text("Zone:")
            Text("Location:")
            Text("Price:")
        }
        VStack(alignment: .leading) {
            Text("vehicle")
            Text("lot")
            Text("zone")
            Text("location")
            Text("price")
        }
    }
    .lineLimit(1)
    .font(.footnote)
    .foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)

enter image description here

答案 9 :(得分:0)

Xcode 12.5

如果您知道要偏移第二个视图的量,那么您可以将两个视图放在前导对齐的 ZStack 中,并在第二个视图上使用 .padding(.horizontal, amount) 修饰符来偏移它。< /p>

var body: some View {
    NavigationView {
        List {
            ForEach(persons) { person in
                ZStack(alignment: .leading) {
                    Text(person.number)
                    Text(person.name)
                        .padding(.horizontal, 30)
                }
            }
        }
        .navigationTitle("Challenge")
    }
}

enter image description here

答案 10 :(得分:-1)

我认为正确的方法是使用HorizontalAlignment。像这样:

extension HorizontalAlignment {
    private enum LeadingName : AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length { d[.leading] }
    }
    static let leadingName = HorizontalAlignment(LeadingName.self)
}

List (people.identified(by: \.id)) {person in
    HStack {
        Text("\(person.id)")
        Text("\(person.name)").alignmentGuide(.leadingName) {d in d[.leading]}
    }
}

但是我无法正常工作。

我找不到带有列表的任何示例。列表似乎不支持对齐(还可以吗?)

我可以使它与VStack一起工作,并提供硬编码值,例如:

        VStack (alignment: .leadingName ) {
        HStack {
            Text("1.")
            Text("John Doe").alignmentGuide(.leadingName) {d in d[.leading]}
            Spacer()
        }
        HStack {
            Text("2000.")
            Text("Alexander Jones").alignmentGuide(.leadingName) {d in d[.leading]}
            Spacer()
        }
        HStack {
            Text("45.")
            Text("Tom Lee").alignmentGuide(.leadingName) {d in d[.leading]}
            Spacer()
        }
    }

我希望这会在以后的测试版中得到解决...