SwiftUI-构建具有可扩展视图的LazyVGrid
我正在尝试从一组颜色构建一个二次视图的两列网格,其中一个视图在单击时扩展为四个小视图的大小 swiftui-lab.com的Javier给了我一个突破,他在ForEach中添加了Color.clear作为“假”视图,以诱使VGrid为扩展视图腾出空间。这适用于网格左侧的框。然而,右边的框给我带来了无尽的麻烦,因为它们向右边扩展,不会导致VGrid正确重新对齐: 我尝试过几种方法,比如交换数组中的颜色,在单击右侧的一个视图时旋转整个网格,添加不同数量的颜色。清除视图-到目前为止,没有任何方法可以做到这一点 以下是当前代码:SwiftUI-构建具有可扩展视图的LazyVGrid,swiftui,lazyvgrid,Swiftui,Lazyvgrid,我正在尝试从一组颜色构建一个二次视图的两列网格,其中一个视图在单击时扩展为四个小视图的大小 swiftui-lab.com的Javier给了我一个突破,他在ForEach中添加了Color.clear作为“假”视图,以诱使VGrid为扩展视图腾出空间。这适用于网格左侧的框。然而,右边的框给我带来了无尽的麻烦,因为它们向右边扩展,不会导致VGrid正确重新对齐: 我尝试过几种方法,比如交换数组中的颜色,在单击右侧的一个视图时旋转整个网格,添加不同数量的颜色。清除视图-到目前为止,没有任何方法
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)
}
}
如果你能指出我做错了什么,我将非常感激
另外,如果您运行代码,您会注意到最后一个黑框的行为也不符合预期。这是另一个我至今无法解决的问题。在放弃LazyVGrid来完成这项工作后,我有点“黑”了两个简单的vStack以包含在ParallelStackView中。它缺少LazyVGrid所拥有的漂亮的交叉动画,只能在两列中实现,但完成了工作——有点像。这显然与优雅的解决方案相去甚远,但我需要一个变通方法,因此对于处理同一问题的任何人,下面是代码(在其包含的类型上实现为泛型):
struct ParallelStackView:View{
让填充:CGFloat
let元素:[T]
@绑定变量currentlySelectedItem:T?
让内容:(T)->content
@状态私有变量selectedElement:T?=nil
@状态私有变量selectedSecondElement:T?=nil
var body:一些观点{
let(transformedFirstArray,transformedSecondArray)=transformArray(数组:元素)
func resolveClearViewHeightForFirstArray(id:Int,用于代理:GeometryProxy)->CGFloat{
transformedSecondArray[id+1]==selectedSecondElement | | |(transformedSecondArray[1]==selectedSecondElement&&id==0)?proxy.size.width+填充:0
}
func resolveClearViewHeightForSecondArray(id:Int,用于代理:GeometryProxy)->CGFloat{
transformedFirstArray[id+1]==selectedElement | |(transformedFirstArray[1]==selectedElement&&id==0)?proxy.size.width+padding:0
}
返回GeometryReader{proxy in
滚动视图{
ZStack(对齐:。顶部引导){
VStack(对齐:。前导,间距:填充/2){
ForEach(0.([T?],[T?])){
var阵列变换:[T?]=[]
中的array.map{element->(T?,T?)
返回(零,元素)
}弗雷奇先生{
arrayTransformed.append($0.0)
arrayTransformed.append($0.1)
}
arrayTransformed=arrayTransformed.reversed()
var firstTransformedArray:[T?]=[]
var secondTransformedArray:[T?]=[]
对于0中的i…阵列已转换。计数/2{
guard let nilValue=arrayTransformed.popLast(),let element=arrayTransformed.popLast()else{break}
如果i%2==0{
firstTransformedArray+=[nilValue,元素]
}否则{
secondTransformedArray+=[nilValue,元素]
}
}
返回(第一个转换阵列,第二个转换阵列)
}
结构矩形视图:视图{
let代理:GeometryProxy
let元素:[T?]
让id:Int
让我们选择元素:T?
让填充:CGFloat
让内容:(T)->content
var body:一些观点{
内容(元素[id]!)
.frame(宽度:CalculateName(用于:id),高度:CalculateName(用于:id))
.clipShape(圆角半径:20))
}
func calculateFrame(对于id:Int)->CGFloat{
selectedElement==元素[id]?proxy.size.width:proxy.size.width/2-5
}
}
}
扩展视图{
func rotate3D()->一些视图{
修改器(StackRotation())
}
}
结构堆栈旋转:GeometryEffect{
func effectValue(大小:CGSize)->ProjectionTransform{
设c=CATTransferM3DidEntity
返回项目Transform(CATTransferorM3Drotate(c、.pi、0、1、0))
}
}
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
}
}
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))
}
}