具有换行和动态高度的 SwiftUI HStack
SwiftUI HStack with wrap and dynamic height
我有这个视图来显示我从 SwiftUI HStack with Wrap 获得的多行文本标签,但是当我将它添加到 VStack 中时,标签与我放在下面的任何其他视图重叠。
标签显示正确,但视图本身的高度未在 VStack 内计算。
如何让这个视图使用 is content 的高度?
import SwiftUI
struct TestWrappedLayout: View {
@State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"]
var body: some View {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.platforms, id: \.self) { platform in
self.item(for: platform)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if platform == self.platforms.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if platform == self.platforms.last! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
}
struct TestWrappedLayout_Previews: PreviewProvider {
static var previews: some View {
TestWrappedLayout()
}
}
示例代码:
struct ExampleTagsView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Platforms:")
TestWrappedLayout()
Text("Other Platforms:")
TestWrappedLayout()
}
}
}
}
struct ExampleTagsView_Previews: PreviewProvider {
static var previews: some View {
ExampleTagsView()
}
}
结果:
我只是设法通过将 GeometryReader 移至 ExampleTagsView 并使用 platforms.first 而不是 last inside .alignmentGuide
来解决这个问题
完整代码:
import SwiftUI
struct ExampleTagsView: View {
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack(alignment: .leading) {
Text("Platforms:")
TestWrappedLayout(geometry: geometry)
Text("Other Platforms:")
TestWrappedLayout(geometry: geometry)
}
}
}
}
}
struct ExampleTagsView_Previews: PreviewProvider {
static var previews: some View {
ExampleTagsView()
}
}
struct TestWrappedLayout: View {
@State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "PlayStation 5", "Ni", "Xct5Box", "PlayStatavtion", "PlvayStation 2", "PlayStatiadfon 3", "PlaySdatation 4", "PlaySdtation 5"]
let geometry: GeometryProxy
var body: some View {
self.generateContent(in: geometry)
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.platforms, id: \.self) { platform in
self.item(for: platform)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if platform == self.platforms.first! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if platform == self.platforms.first! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
}
结果:
好的,这里有一个更通用和改进的变体(针对最初在 SwiftUI HStack with Wrap 中引入的解决方案)
测试 Xcode 11.4 / iOS 13.4
注意:由于视图高度是动态计算的,因此结果在 运行 时间内有效,而不是在预览中
struct TagCloudView: View {
var tags: [String]
@State private var totalHeight
= CGFloat.zero // << variant for ScrollView/List
// = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.tags, id: \.self) { tag in
self.item(for: tag)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct TestTagCloudView : View {
var body: some View {
VStack {
Text("Header").font(.largeTitle)
TagCloudView(tags: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"])
Text("Some other text")
Divider()
Text("Some other cloud")
TagCloudView(tags: ["Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"])
}
}
}
我调整了 Asperi 的解决方案以接受任何类型的视图和模型。以为我会在这里分享。我将它添加到 GitHub Gist 并在此处包含代码。
struct WrappingHStack<Model, V>: View where Model: Hashable, V: View {
typealias ViewGenerator = (Model) -> V
var models: [Model]
var viewGenerator: ViewGenerator
var horizontalSpacing: CGFloat = 2
var verticalSpacing: CGFloat = 0
@State private var totalHeight
= CGFloat.zero // << variant for ScrollView/List
// = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in geometry: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.models, id: \.self) { models in
viewGenerator(models)
.padding(.horizontal, horizontalSpacing)
.padding(.vertical, verticalSpacing)
.alignmentGuide(.leading, computeValue: { dimension in
if (abs(width - dimension.width) > geometry.size.width)
{
width = 0
height -= dimension.height
}
let result = width
if models == self.models.last! {
width = 0 //last item
} else {
width -= dimension.width
}
return result
})
.alignmentGuide(.top, computeValue: {dimension in
let result = height
if models == self.models.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
您可以使用“fixedSize”在一个或两个方向包裹内容:
.fixedSize(horizontal: true, vertical: true)
我有这个视图来显示我从 SwiftUI HStack with Wrap 获得的多行文本标签,但是当我将它添加到 VStack 中时,标签与我放在下面的任何其他视图重叠。 标签显示正确,但视图本身的高度未在 VStack 内计算。 如何让这个视图使用 is content 的高度?
import SwiftUI
struct TestWrappedLayout: View {
@State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"]
var body: some View {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.platforms, id: \.self) { platform in
self.item(for: platform)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if platform == self.platforms.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if platform == self.platforms.last! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
}
struct TestWrappedLayout_Previews: PreviewProvider {
static var previews: some View {
TestWrappedLayout()
}
}
示例代码:
struct ExampleTagsView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Platforms:")
TestWrappedLayout()
Text("Other Platforms:")
TestWrappedLayout()
}
}
}
}
struct ExampleTagsView_Previews: PreviewProvider {
static var previews: some View {
ExampleTagsView()
}
}
结果:
我只是设法通过将 GeometryReader 移至 ExampleTagsView 并使用 platforms.first 而不是 last inside .alignmentGuide
来解决这个问题完整代码:
import SwiftUI
struct ExampleTagsView: View {
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack(alignment: .leading) {
Text("Platforms:")
TestWrappedLayout(geometry: geometry)
Text("Other Platforms:")
TestWrappedLayout(geometry: geometry)
}
}
}
}
}
struct ExampleTagsView_Previews: PreviewProvider {
static var previews: some View {
ExampleTagsView()
}
}
struct TestWrappedLayout: View {
@State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "PlayStation 5", "Ni", "Xct5Box", "PlayStatavtion", "PlvayStation 2", "PlayStatiadfon 3", "PlaySdatation 4", "PlaySdtation 5"]
let geometry: GeometryProxy
var body: some View {
self.generateContent(in: geometry)
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.platforms, id: \.self) { platform in
self.item(for: platform)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if platform == self.platforms.first! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if platform == self.platforms.first! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
}
结果:
好的,这里有一个更通用和改进的变体(针对最初在 SwiftUI HStack with Wrap 中引入的解决方案)
测试 Xcode 11.4 / iOS 13.4
注意:由于视图高度是动态计算的,因此结果在 运行 时间内有效,而不是在预览中
struct TagCloudView: View {
var tags: [String]
@State private var totalHeight
= CGFloat.zero // << variant for ScrollView/List
// = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.tags, id: \.self) { tag in
self.item(for: tag)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct TestTagCloudView : View {
var body: some View {
VStack {
Text("Header").font(.largeTitle)
TagCloudView(tags: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"])
Text("Some other text")
Divider()
Text("Some other cloud")
TagCloudView(tags: ["Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"])
}
}
}
我调整了 Asperi 的解决方案以接受任何类型的视图和模型。以为我会在这里分享。我将它添加到 GitHub Gist 并在此处包含代码。
struct WrappingHStack<Model, V>: View where Model: Hashable, V: View {
typealias ViewGenerator = (Model) -> V
var models: [Model]
var viewGenerator: ViewGenerator
var horizontalSpacing: CGFloat = 2
var verticalSpacing: CGFloat = 0
@State private var totalHeight
= CGFloat.zero // << variant for ScrollView/List
// = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in geometry: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.models, id: \.self) { models in
viewGenerator(models)
.padding(.horizontal, horizontalSpacing)
.padding(.vertical, verticalSpacing)
.alignmentGuide(.leading, computeValue: { dimension in
if (abs(width - dimension.width) > geometry.size.width)
{
width = 0
height -= dimension.height
}
let result = width
if models == self.models.last! {
width = 0 //last item
} else {
width -= dimension.width
}
return result
})
.alignmentGuide(.top, computeValue: {dimension in
let result = height
if models == self.models.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
您可以使用“fixedSize”在一个或两个方向包裹内容:
.fixedSize(horizontal: true, vertical: true)