I have two views that both react to .onHover
overlapping in a ZStack
. Currently the effect is triggered in the blue view even if it is overlapped by the red view (see video). How can I make sure that only the view at the top of the ZStack
will react to .onHover
, so that the blue view will not change when the overlapping part of the red view is hovered?
Code:
struct ContentView: View {
var body: some View {
ZStack {
RectangleView(color: .blue)
.frame(width: 300, height: 300)
RectangleView(color: .red)
.frame(width: 150, height: 150)
.offset(x: -150)
}
.frame(width: 600, height: 400)
}
}
struct RectangleView: View {
@State var hover = false
var color: Color
var body: some View {
Rectangle()
.foregroundColor(color)
.overlay(hover ? Color.white.opacity(0.3) : Color.clear)
.onHover { isHovering in
if isHovering {
hover = true
} else {
hover = false
}
}
}
}
CodePudding user response:
Seems like SwiftUI still doesn't have a solution to this approach, but at least you can do that manually.
struct ContentView: View {
@State var hoverStates: [(isHovered: Bool, shouldBeHoverd: Bool)] = Array(repeating: (false, false), count: 2)
var body: some View {
ZStack {
RectangleView(hoverStates: $hoverStates, color: .blue, zIndex: 0)
.frame(width: 300, height: 300)
RectangleView(hoverStates: $hoverStates, color: .red, zIndex: 1)
.frame(width: 150, height: 150)
.offset(x: -150)
}
.frame(width: 600, height: 400)
}
}
struct RectangleView: View {
@Binding var hoverStates: [(isHovered: Bool, shouldBeHoverd: Bool)]
var color: Color
var zIndex = 0
var isHovered: Bool {
hoverStates.indices.contains(zIndex) ? hoverStates[zIndex].isHovered : false
}
var body: some View {
Rectangle()
.foregroundColor(color)
.overlay(isHovered ? Color.white.opacity(0.3) : Color.clear)
.zIndex(Double(zIndex))
.onHover { isHovering in
guard hoverStates.indices.contains(zIndex) else { return }
hoverStates[zIndex].shouldBeHoverd = isHovering
if isHovering {
// hover this view if there are no other hovered views above this one
if zIndex 1 < hoverStates.count {
hoverStates[zIndex].isHovered = hoverStates[(zIndex 1)..<hoverStates.endIndex].allSatisfy { !$0.isHovered }
} else {
hoverStates[zIndex].isHovered = true
}
// unhover all views below this one
for index in 0..<zIndex {
hoverStates[index].isHovered = false
}
} else {
// unhover this view
hoverStates[zIndex].isHovered = false
// hover the first view under cursor which is below this one
if let index = hoverStates.firstIndex(where: { $0.shouldBeHoverd }) {
hoverStates[index].isHovered = true
}
}
}
}
}
The downside of this approach is that you have to set the zIndex
of each view manually and init hoverStates
with the number of the views.
CodePudding user response:
Here is a way that is felixaeble with deferent scenario:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
RectangleView(color: .blue, index: 0)
.frame(width: 300, height: 300)
RectangleView(color: .red, index: 1)
.frame(width: 150, height: 150)
.offset(x: -150)
RectangleView(color: .black, index: 2)
.frame(width: 50, height: 50)
.offset(x: -150)
RectangleView(color: .green, index: 3)
.frame(width: 150, height: 150)
.offset(x: 150, y: 150)
}
.frame(width: 600, height: 600)
}
}
struct RectangleView: View {
let color: Color
let index: Int
@StateObject var observableHoveringObject: ObservableHoveringObject = ObservableHoveringObject.shared
var body: some View {
Rectangle()
.foregroundColor(color)
.overlay(observableHoveringObject.isHovering(index: index) ? Color.white.opacity(0.3) : Color.clear)
.onHover { hoverValue in
observableHoveringObject.sign(index, hoverValue: hoverValue)
}
}
}
class ObservableHoveringObject: ObservableObject {
static let shared: ObservableHoveringObject = ObservableHoveringObject()
@Published var activeHoveringIndex: Int? = nil
func isHovering(index: Int) -> Bool {
return index == activeHoveringIndex
}
private func exited(_ index: Int) { setCollection.remove(index) }
private func entered(_ index: Int) { isHoveringTrafficLightFunction(index: index) }
func sign(_ index: Int, hoverValue: Bool) {
if (hoverValue) { entered(index) }
else { exited(index) }
}
private var setCollection: Set<Int> = Set<Int>() {
didSet {
let filteredSet: Set<Int>.Element? = setCollection.max(by: { (lhs, rhs) in
if (lhs < rhs) { return true }
else { return false }
})
if let unwrappedValue: Int = filteredSet { activeHoveringIndex = unwrappedValue }
else { activeHoveringIndex = nil }
}
}
private func isHoveringTrafficLightFunction(index: Int) {
if let unwrappedActiveHoveringIndex: Int = activeHoveringIndex {
setCollection.insert(index)
if (index >= unwrappedActiveHoveringIndex) { activeHoveringIndex = index }
}
else {
activeHoveringIndex = index
setCollection.insert(index)
}
}
}