SwiftUI - 使用可扩展视图构建 LazyVGrid
Posted
技术标签:
【中文标题】SwiftUI - 使用可扩展视图构建 LazyVGrid【英文标题】:SwiftUI - Constructing a LazyVGrid with expandable views 【发布时间】:2020-07-30 13:27:06 【问题描述】:我正在尝试从一组颜色构建一个两列的二次视图网格,其中一个视图在单击时扩展为四个小视图的大小。
来自 swiftui-lab.com 的 Javier 给了我一个突破,他提出了在 ForEach 中添加 Color.clear 作为“假”视图的想法,以欺骗 VGrid 为扩展视图腾出空间。这适用于网格左侧的框。但是,右侧的框给我带来了无穷无尽的麻烦,因为它们向右扩展并且不会导致 VGrid 正确重新对齐:
我尝试了几种方法,例如交换数组中的颜色、在单击右侧视图之一时旋转整个网格、添加不同数量的 Color.clear 视图 - 到目前为止还没有任何效果。
这是当前代码:
struct ContentView: View
@State private var selectedColor : UIColor? = nil
let colors : [UIColor] = [.red, .yellow, .green, .orange, .blue, .magenta, .purple, .black]
private let padding : CGFloat = 10
var body: some View
GeometryReader proxy in
ScrollView
LazyVGrid(columns: [
GridItem(.fixed(proxy.size.width / 2 - 5), spacing: padding, alignment: .leading),
GridItem(.fixed(proxy.size.width / 2 - 5))
], spacing: padding)
ForEach(0..<colors.count, id: \.self) id in
if selectedColor == colors[id] && id % 2 != 0
Color.clear
RectangleView(proxy: proxy, colors: colors, id: id, selectedColor: selectedColor, padding: padding)
.onTapGesture
withAnimation
if selectedColor == colors[id]
selectedColor = nil
else
selectedColor = colors[id]
if selectedColor == colors[id]
Color.clear
Color.clear
Color.clear
.padding(.all, 10)
矩形视图:
struct RectangleView: View
var proxy: GeometryProxy
var colors : [UIColor]
var id: Int
var selectedColor : UIColor?
var padding : CGFloat
var body: some View
Color(colors[id])
.frame(width: calculateFrame(for: id), height: calculateFrame(for: id))
.clipShape(RoundedRectangle(cornerRadius: 20))
.offset(y: resolveOffset(for: id))
// Used to offset the boxes after the expanded one to compensate for missing padding
func resolveOffset(for id: Int) -> CGFloat
guard let selectedColor = selectedColor, let selectedIndex = colors.firstIndex(of: selectedColor) else return 0
if id > selectedIndex
return -(padding * 2)
return 0
func calculateFrame(for id: Int) -> CGFloat
selectedColor == colors[id] ? proxy.size.width : proxy.size.width / 2 - 5
如果您能指出我做错的方向,我将不胜感激。
附:如果您运行代码,您会注意到最后一个黑匣子也没有按预期运行。这是迄今为止我无法解决的另一个问题。
【问题讨论】:
【参考方案1】:在放弃 LazyVGrid 来完成这项工作后,我有点“破解”了两个简单的 VStack 以包含在 ParallelStackView 中。它缺乏 LazyVGrid 所具有的漂亮的交叉动画,并且只能为两列实现,但可以完成工作 - 有点。这显然与一个优雅的解决方案相去甚远,但我需要一个解决方法,所以对于处理同一问题的任何人,这里是代码(在其包含的类型上作为泛型实现):
struct ParallelStackView<T: Equatable, Content: View>: View
let padding : CGFloat
let elements : [T]
@Binding var currentlySelectedItem : T?
let content : (T) -> Content
@State private var selectedElement : T? = nil
@State private var selectedSecondElement : T? = nil
var body: some View
let (transformedFirstArray, transformedSecondArray) = transformArray(array: elements)
func resolveClearViewHeightForFirstArray(id: Int, for proxy: GeometryProxy) -> CGFloat
transformedSecondArray[id+1] == selectedSecondElement || (transformedSecondArray[1] == selectedSecondElement && id == 0) ? proxy.size.width + padding : 0
func resolveClearViewHeightForSecondArray(id: Int, for proxy: GeometryProxy) -> CGFloat
transformedFirstArray[id+1] == selectedElement || (transformedFirstArray[1] == selectedElement && id == 0) ? proxy.size.width + padding : 0
return GeometryReader proxy in
ScrollView
ZStack(alignment: .topLeading)
VStack(alignment: .leading, spacing: padding / 2)
ForEach(0..<transformedFirstArray.count, id: \.self) id in
if transformedFirstArray[id] == nil
Color.clear.frame(
width: proxy.size.width / 2 - padding / 2,
height: resolveClearViewHeightForFirstArray(id: id, for: proxy))
else
RectangleView(proxy: proxy, elements: transformedFirstArray, id: id, selectedElement: selectedElement, padding: padding, content: content)
.onTapGesture
withAnimation(.spring())
if selectedElement == transformedFirstArray[id]
selectedElement = nil
currentlySelectedItem = nil
else
selectedSecondElement = nil
selectedElement = transformedFirstArray[id]
currentlySelectedItem = selectedElement
VStack(alignment: .leading, spacing: padding / 2)
ForEach(0..<transformedSecondArray.count, id: \.self) id in
if transformedSecondArray[id] == nil
Color.clear.frame(
width: proxy.size.width / 2 - padding / 2,
height: resolveClearViewHeightForSecondArray(id: id, for: proxy))
else
RectangleView(proxy: proxy, elements: transformedSecondArray, id: id, selectedElement: selectedSecondElement, padding: padding, content: content)
.onTapGesture
withAnimation(.spring())
if selectedSecondElement == transformedSecondArray[id]
selectedSecondElement = nil
currentlySelectedItem = nil
else
selectedElement = nil
selectedSecondElement = transformedSecondArray[id]
currentlySelectedItem = selectedSecondElement
.rotation3DEffect(.init(degrees: 180), axis: (x: 0, y: 1, z: 0))
// You need to rotate the second VStack for it to expand in the correct direction (left).
// As now all text would be displayed as mirrored, you have to reverse that rotation "locally"
// with a .rotation3DEffect modifier (see 4 lines above).
.rotate3D()
.offset(x: resolveOffset(for: proxy))
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .topTrailing)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(10)
func resolveOffset(for proxy: GeometryProxy) -> CGFloat
selectedSecondElement == nil ? proxy.size.width / 2 - padding / 2 : proxy.size.width
// Transform the original array to alternately contain nil and real values
// for the Color.clear views. You could just as well use other "default" values
// but I thought nil was quite explicit and makes it easier to understand what
// is going on. Then you split the transformed array into two sub-arrays for
// the VStacks:
func transformArray<T: Equatable>(array: [T]) -> ([T?], [T?])
var arrayTransformed : [T?] = []
array.map element -> (T?, T?) in
return (nil, element)
.forEach
arrayTransformed.append($0.0)
arrayTransformed.append($0.1)
arrayTransformed = arrayTransformed.reversed()
var firstTransformedArray : [T?] = []
var secondTransformedArray : [T?] = []
for i in 0...arrayTransformed.count / 2
guard let nilValue = arrayTransformed.popLast(), let element = arrayTransformed.popLast() else break
if i % 2 == 0
firstTransformedArray += [nilValue, element]
else
secondTransformedArray += [nilValue, element]
return (firstTransformedArray, secondTransformedArray)
struct RectangleView: View
let proxy: GeometryProxy
let elements : [T?]
let id: Int
let selectedElement : T?
let padding : CGFloat
let content : (T) -> Content
var body: some View
content(elements[id]!)
.frame(width: calculateFrame(for: id), height: calculateFrame(for: id))
.clipShape(RoundedRectangle(cornerRadius: 20))
func calculateFrame(for id: Int) -> CGFloat
selectedElement == elements[id] ? proxy.size.width : proxy.size.width / 2 - 5
extension View
func rotate3D() -> some View
modifier(StackRotation())
struct StackRotation: GeometryEffect
func effectValue(size: CGSize) -> ProjectionTransform
let c = CATransform3DIdentity
return ProjectionTransform(CATransform3DRotate(c, .pi, 0, 1, 0))
【讨论】:
以上是关于SwiftUI - 使用可扩展视图构建 LazyVGrid的主要内容,如果未能解决你的问题,请参考以下文章