【问题标题】:SwiftUI - Constructing a LazyVGrid with expandable viewsSwiftUI - 使用可扩展视图构建 LazyVGrid
【发布时间】: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
    }
}

如果您能指出我做错的方向,我将不胜感激。

附:如果您运行代码,您会注意到最后一个黑匣子也没有按预期运行。这是迄今为止我无法解决的另一个问题。

【问题讨论】:

    标签: swiftui lazyvgrid


    【解决方案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))
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2022-10-14
      • 2020-10-13
      • 2021-09-20
      • 1970-01-01
      • 2020-04-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-12
      相关资源
      最近更新 更多