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)
}
}
}


