如何在包含 ZStacks 的 VStack 上显示视图

Posted

技术标签:

【中文标题】如何在包含 ZStacks 的 VStack 上显示视图【英文标题】:How to bring a view on top of VStack containing ZStacks 【发布时间】:2020-07-09 09:55:46 【问题描述】:

我需要一个视图网格,其中每个项目都根据网格中项目的数量调整大小,并且我需要在点击时展开网格的每个项目。

我最终设法根据需要布置项目(见图 1)并展开它们。

不幸的是,我无法使用 zIndex 正确地将展开的项目置于所有其他视图的前面(参见图 2 和图 3)。

我还尝试将 VStack 和 HStack 都嵌入到 ZStack 中,但没有任何变化。

如何将展开后的项目置于顶部?

下面是我的代码。

struct ContentViewNew: View 
    private let columns: Int = 6
    private let rows: Int = 4
    @ObservedObject var viewModel: ViewModel
    var cancellables = Set<AnyCancellable>()

    init() 
        viewModel = ViewModel(rows: rows, columns: columns)
        viewModel.objectWillChange.sink  _ in
            print("viewModel Changed")
        .store(in: &cancellables)
    

    var body: some View 
        GeometryReader  geometryProxy in

            let hSpacing: CGFloat = 7
            let vSpacing: CGFloat = 7

            let hSize = (geometryProxy.size.width - hSpacing * CGFloat(columns + 1)) / CGFloat(columns)
            let vSize = (geometryProxy.size.height - vSpacing * CGFloat(rows + 1)) / CGFloat(rows)
            let size = min(hSize, vSize)

            VStack 
                ForEach(0 ..< viewModel.rows, id: \.self)  row in
                    Spacer()
                    HStack 
                        Spacer()
                        ForEach(0 ..< viewModel.columns, id: \.self)  column in

                            GeometryReader  widgetProxy in

                                ItemWiew(info: viewModel.getItem(row: row, column: column), size: size, zoomedSize: 0.80 * geometryProxy.size.width)
                                    .offset(x: viewModel.getItem(row: row, column: column).zoomed ? (geometryProxy.size.width / 2.0 - (widgetProxy.frame(in: .global).origin.x + widgetProxy.size.width / 2.0)) : 0,
                                            y: viewModel.getItem(row: row, column: column).zoomed ? geometryProxy.size.height / 2.0 - (widgetProxy.frame(in: .global).origin.y + widgetProxy.size.height / 2.0) : 0)
                                    .onTapGesture 
                                        viewModel.zoom(row: row, column: column)
                                    
                                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                                    .zIndex(viewModel.getItem(row: row, column: column).zoomed ? 10000 : 0)
                                    .background(Color.gray)
                            
                            Spacer()
                        
                    
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                    .background(Color.blue)
                    Spacer()
                
            
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
            .background(Color.yellow)
        
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
        .background(Color.green)
    

struct ItemWiew: View 
    @ObservedObject var info: ItemInfo
    var size: CGFloat

    init(info: ItemInfo, size: CGFloat, zoomedSize: CGFloat) 
        self.info = info
        self.size = size
        if self.info.size == 0 
            self.info.size = size
            self.info.zoomedSize = zoomedSize
        
    

    var body: some View 
        VStack 
            Print("Drawing Widget with size \(self.info.size)")
            Image(systemName: info.symbol)
                .font(.system(size: 30))
                .frame(width: info.size, height: info.size)
                .background(info.color)
                .cornerRadius(10)
        
    

class ItemInfo: ObservableObject, Identifiable 
    var symbol: String
    var color: Color
    var zoomed = false
    @Published var size: CGFloat
    @Published var originalSize: CGFloat
    @Published var zoomedSize: CGFloat

    init(symbol: String, color: Color) 
        self.symbol = symbol
        self.color = color
        size = 0.0
        originalSize = 0.0
        zoomedSize = 0.0
    

    func toggleZoom() 
        if zoomed 
            size = originalSize
            color = .red
         else 
            size = zoomedSize
            color = .white
        
        zoomed.toggle()
    
 
class ViewModel: ObservableObject 
    private var symbols = ["keyboard", "hifispeaker.fill", "printer.fill", "tv.fill", "desktopcomputer", "headphones", "tv.music.note", "mic", "plus.bubble", "video"]
    private var colors: [Color] = [.yellow, .purple, .green]
    @Published var listData = [ItemInfo]()
    var rows = 0
    var columns = 0

    init(rows: Int, columns: Int) 
        self.rows = rows
        self.columns = columns
        for _ in 0 ..< rows 
            for j in 0 ..< columns 
                listData.append(ItemInfo(symbol: symbols[j % symbols.count], color: colors[j % colors.count]))
            
        
    

    func getItem(row: Int, column: Int) -> ItemInfo 
        return listData[columns * row + column]
    

    func zoom(row: Int, column: Int) 
        listData[columns * row + column].toggleZoom()
        objectWillChange.send()
    

【问题讨论】:

【参考方案1】:

您发布了很多代码。我试着把它简化一点。大多数情况下,您过度使用了 size/zoomedSize/originalSize 属性。

首先您可以将ItemInfo 设为结构并删除所有与size 相关的属性:

struct ItemInfo 
    var symbol: String
    var color: Color

    init(symbol: String, color: Color) 
        self.symbol = symbol
        self.color = color
    

然后通过删除所有 size 相关属性再次简化您的 ViewModel

class ViewModel: ObservableObject 
    private var symbols = ["keyboard", "hifispeaker.fill", "printer.fill", "tv.fill", "desktopcomputer", "headphones", "tv.music.note", "mic", "plus.bubble", "video"]
    private var colors: [Color] = [.yellow, .purple, .green]
    @Published var listData = [ItemInfo]()
    let rows: Int
    let columns: Int

    init(rows: Int, columns: Int) 
        self.rows = rows
        self.columns = columns
        for _ in 0 ..< rows 
            for j in 0 ..< columns 
                listData.append(ItemInfo(symbol: symbols[j % symbols.count], color: colors[j % colors.count]))
            
        
    

    func getItem(row: Int, column: Int) -> ItemInfo 
        return listData[columns * row + column]
    

然后更新您的ItemView(再次从外部视图中删除所有与size相关的属性并直接使用GeometryReader):

struct ItemWiew: View 
    let itemInfo: ItemInfo

    var body: some View 
        GeometryReader  proxy in
            self.imageView(proxy: proxy)
        
    

    // extracted to another function as you can't use `let` inside a `GeometryReader` closure
    func imageView(proxy: GeometryProxy) -> some View 
        let sideLength = min(proxy.size.width, proxy.size.height) // to make it fill all the space but remain a square
        return Image(systemName: itemInfo.symbol)
            .font(.system(size: 30))
            .frame(maxWidth: sideLength, maxHeight: sideLength)
            .background(itemInfo.color)
            .cornerRadius(10)
    

现在你可以更新ContentView

struct ContentView: View 
    private let columns: Int = 6
    private let rows: Int = 4

    @ObservedObject var viewModel: ViewModel

    // zoomed item (nil if no item is zoomed)
    @State var zoomedItem: ItemInfo?

    init() 
        viewModel = ViewModel(rows: rows, columns: columns)
    

    var body: some View 
        ZStack 
            gridView
            zoomedItemView
        
    

    var gridView: some View 
        let spacing: CGFloat = 7
        return VStack(spacing: spacing) 
            ForEach(0 ..< viewModel.rows, id: \.self)  rowIndex in
                self.rowView(rowIndex: rowIndex)
            
        
        .padding(.all, spacing)
    

    func rowView(rowIndex: Int) -> some View 
        let spacing: CGFloat = 7
        return HStack(spacing: spacing) 
            ForEach(0 ..< viewModel.columns, id: \.self)  columnIndex in
                ItemWiew(itemInfo: self.viewModel.getItem(row: rowIndex, column: columnIndex))
                    .onTapGesture 
                        // set zoomed item on tap gesture
                        self.zoomedItem = self.viewModel.getItem(row: rowIndex, column: columnIndex)
                    
            
        
    

最后在zoomedItemView 中,我重用了ItemView,但您可以为缩放项目创建其他视图:

extension ContentView 
    var zoomedItemView: some View 
        Group 
            if zoomedItem != nil 
                ItemWiew(itemInfo: zoomedItem!)
                    .onTapGesture 
                        self.zoomedItem = nil
                    
            
        
        .padding()
    

注意:为简单起见,我将ItemInfo 设为结构。如果您不打算在 zoomedView 内修改它并将更改应用到网格,建议您这样做。但是如果由于某种原因您需要它是一个 class 和一个 ObservableObject,您可以轻松地恢复您的原始声明:

class ItemInfo: ObservableObject, Identifiable  ... 

未选择任何项目:

使用缩放项目:

【讨论】:

非常感谢,不是为了解决问题,而是为了让我的代码更加简单易读。我学到了很多。干得好!

以上是关于如何在包含 ZStacks 的 VStack 上显示视图的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:在带有 Spacers 的 VStack 中平均分配多个 VStack

如何使矩形的高度与 VStack 的高度相同

如何使用 SwiftUI 在 VStack 中显示我的 AVPlayer

如何在swiftui中将vstack移动到屏幕顶部?

如何在 VStack 中为子视图排列底部文本

即使大于高度,如何将 VStack 对齐到顶部?