Home > Software design >  SwiftUI: Prevent .onHover on overlapping views
SwiftUI: Prevent .onHover on overlapping views

Time:08-05

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?

enter image description here

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

enter image description here

  • Related