我有一个简单的列表视图,其中包含两行。
每行包含两个文本视图。查看一个和查看两个。
我想对齐每行的最后一个标签(查看两个),以使名称标签前导对齐并与字体大小无关地保持对齐。
第一个标签(查看一个标签)也需要对齐。
我尝试在第一个标签(“查看一个”)上设置最小边框宽度,但是它不起作用。设置最小宽度和使文本视图在View One中对齐也似乎是不可能的。
有什么想法吗?在UIKit中,这相当简单。
答案 0 :(得分:9)
我找到了一种解决此问题的方法,该方法支持动态类型并且不易破解。答案是使用PreferenceKeys和GeometryReader!
此解决方案的实质是每个数字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
的代码。
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")
}
}
}
这里的想法是收集代表排名的Text
的所有宽度,并将其中最宽的宽度分配给width
的{{1}}属性。
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")
}
}
}
逻辑重构为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)
}
}
}
}
我们可以根据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
}
}
}
}
答案 3 :(得分:1)
您可以为数字Text
视图设置固定宽度。它使此Text
组件具有固定的大小。
HStack {
Text(item.number)
.multilineTextAlignment(.leading)
.frame(width: 30)
Text(item.name)
}
此解决方案的缺点是,如果在那里有较长的文本,它将被包裹并以“ ...”结尾,但是在那种情况下,我认为您可以粗略估计出哪个宽度就足够了。 / p>
答案 4 :(得分:1)
您可以只拥有两个Text
,然后在Spacer
中拥有一个HStack
。 Spacer
会将您的Text
推到左侧,并且如果Text
的任何一个由于其内容的长度而改变大小,一切都会自动调整:
HStack {
Text("1.")
Text("John Doe")
Spacer()
}
.padding()
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 上面的代码生成以下代码:
答案 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,但至少它支持动态类型?♂️
下面是完整示例! ?
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)
尝试使它工作一整天后,我想出了以下解决方案:
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)
答案 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")
}
}
答案 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()
}
}
我希望这会在以后的测试版中得到解决...