问题描述
我需要一个视图网格,其中每个项目都根据网格中的项目数量调整大小,并且我需要在点击时展开网格的每个项目.
I need a view grid where each item resizes depending on the number of items in the grid and I need to expand each item of the grid when tapped.
我最终设法根据需要布置了这些项目(见图 1)并展开了它们.
I eventually managed to layout out the items as required (see Fig 1) and to expand them.
不幸的是,我无法使用 zIndex 将展开的项目正确地置于所有其他视图的前面(参见图 2 和图 3).
Unfortunately I am not able to properly bring the expanded item in front of all other views (see Fig 2 and Fig 3) using zIndex.
我也尝试将 VStack 和 HStack 嵌入到 ZStack 中,但没有任何变化.
I also tried to embed both VStack and HStack into a ZStack but nothing changes.
如何将展开的项目置于顶部?
How can I bring the expanded item on top?
下面是我的代码.
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()
}
}
推荐答案
您发布了很多代码.我试图简化它.大多数情况下,您过度使用了 size/zoomedSize/originalSize 属性.
There is a lot of code you posted. I tried to simplify it a bit. Mostly you overused size/zoomedSize/originalSize properties.
首先你可以将 ItemInfo
变成一个结构体并移除所有与 size 相关的属性:
First you can make ItemInfo
a struct and remove all size related properties:
struct ItemInfo {
var symbol: String
var color: Color
init(symbol: String, color: Color) {
self.symbol = symbol
self.color = color
}
}
然后通过删除所有 size 相关属性再次简化您的 ViewModel
:
Then simplify your ViewModel
again by removing all size related properties:
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
):
Then update your ItemView
(again remove all size related properties from outer views and use a GeometryReader
directly):
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
,但您可以为缩放的项目创建一些其他视图:
Lastly in the zoomedItemView
I reused the ItemView
but you can create some other view just for the zoomed item:
extension ContentView {
var zoomedItemView: some View {
Group {
if zoomedItem != nil {
ItemWiew(itemInfo: zoomedItem!)
.onTapGesture {
self.zoomedItem = nil
}
}
}
.padding()
}
}
注意:为了简单起见,我将 ItemInfo
设为一个结构体.如果您不打算在 zoomedView
内修改它并将更改应用于网格,则建议使用此方法.但是如果由于某种原因你需要它是一个 class 和一个 ObservableObject
你可以很容易地恢复你的原始声明:
Note: for simplicity I made ItemInfo
a struct. This is recommended if you don't plan to modify it inside the zoomedView
and apply changes to the grid. But if for some reason you need it to be a class and an ObservableObject
you can easily restore your original declaration:
class ItemInfo: ObservableObject, Identifiable { ... }
未选择任何项目:
使用缩放项目:
这篇关于如何在包含 ZStack 的 VStack 顶部显示视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!