我有一个简单的列表视图,其中包含两行。
每行包含两个文本视图。查看一和查看二。
我想对齐每行的最后一个标签(查看两个),以使名称标签前导对齐并与字体大小无关地保持对齐。
第一个标签(查看一个)也需要对齐。
我尝试在第一个标签(“查看一个”)上设置最小边框宽度,但是它不起作用。设置最小宽度和使文本视图在View One中对齐也似乎是不可能的。
有任何想法吗?在UIKit中,这相当简单。
Ima*_*tit 10
在 Swift 5.2 和 iOS 13 中,你可以使用PreferenceKey协议、preference(key:value:)方法和onPreferenceChange(_:perform:)方法来解决这个问题。
您可以View通过 3 个主要步骤来实现OP 提出的代码,如下所示。
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)
这里的想法是收集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)
为了使我们的代码可重用,我们可以将我们的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)
结果如下所示:
我找到了一种修复此问题的方法,该方法支持动态类型并且不易破解。答案是使用PreferenceKeys和GeometryReader!
该解决方案的实质是每个数字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/
以下是静态执行此操作的三个选项。
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
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)
您可以只将两个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 上面的代码生成:
我只需要处理这个。依赖固定宽度的解决方案frame不适用于动态类型,因此我无法使用它们。我解决这个问题的方法是,将柔性项(在这种情况下为左侧的数字)放在一个ZStack包含允许的内容最多的占位符的位置,然后将占位符的不透明度设置为0:
ZStack {
Text("9999")
.opacity(0)
.accessibility(visibility: .hidden)
Text(id)
}
Run Code Online (Sandbox Code Playgroud)
它很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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
3646 次 |
| 最近记录: |