How to apply .onHover to individual elements in SwiftUI
Asked Answered
F

3

6

I am trying to animate individual items on mouseover. The issue I am having is that every item gets animated on mouseover of an item instead of just that specific item. Here is what I have:

struct ContentView : View {
    @State var hovered = false
    var body: some View {
        VStack(spacing: 90) {
            ForEach(0..<2) {_ in
                HStack(spacing: 90) {
                    ForEach(0..<4) {_ in
                        Circle().fill(Color.red).frame(width: 50, height: 50)
                            .scaleEffect(self.hovered ? 2.0 : 1.0)
                        .animation(.default)
                        .onHover { hover in
                                print("Mouse hover: \(hover)")
                            self.hovered.toggle()
                        }
                    }
                }
            }
        }
        .frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
    }
}




drawing

Flynt answered 5/5, 2020 at 5:57 Comment(0)
M
8

It needs to change onHover view on per-view base, ie. store some identifier of hovered view.

Here is possible solution. Tested with Xcode 11.4.

demo

struct TestOnHoverInList : View {
    @State var hovered: (Int, Int) = (-1, -1)
    var body: some View {
        VStack(spacing: 90) {
            ForEach(0..<2) {i in
                HStack(spacing: 90) {
                    ForEach(0..<4) {j in
                        Circle().fill(Color.red).frame(width: 50, height: 50)
                        .scaleEffect(self.hovered == (i,j) ? 2.0 : 1.0)
                        .animation(.default)
                        .onHover { hover in
                            print("Mouse hover: \(hover)")
                            if hover {
                                self.hovered = (i, j)    // << here !!
                            } else {
                                self.hovered = (-1, -1)  // reset
                            }
                        }
                    }
                }
            }
        }
        .frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
    }
}
Marandamarasca answered 5/5, 2020 at 6:47 Comment(1)
how can I use this on a LazyVgrid ???Crooks
A
5

Every item currently gets animated because they are all relying on hovered to see if the Circle is hovered over. To fix that, we can make every circle have their own hovered state.

struct CircleView: View {
    @State var hovered = false

    var body: some View {
        Circle().fill(Color.red).frame(width: 50, height: 50)
            .scaleEffect(self.hovered ? 2.0 : 1.0)
        .animation(.default)
        .onHover { hover in
                print("Mouse hover: \(hover)")
            self.hovered.toggle()
        }
    }
}

and in the ForEach we can just call the new CircleView where every Circle has their own source of truth.

struct ContentView : View {
    var body: some View {
        VStack(spacing: 90) {
            ForEach(0..<2) { _ in
                HStack(spacing: 90) {
                    ForEach(0..<4) { _ in
                        CircleView()
                    }
                }
            }
        }
        .frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
    }
}
Angelangela answered 5/5, 2020 at 6:52 Comment(1)
with this method, you would have to hover again so it scales down, it doesn't auto scales down when out of hover...Crooks
F
0

Alternatively, you can create a modifier that allows you to change the View in question when it's hovered:

extension View {
    func onHover<Content: View>(@ViewBuilder _ modify: @escaping (Self) -> Content) -> some View {
        modifier(HoverModifier { modify(self) })
    }
}

private struct HoverModifier<Result: View>: ViewModifier {
    @ViewBuilder let modifier: () -> Result
    @State private var isHovering = false

    func body(content: Content) -> AnyView {
        (isHovering ? modifier().eraseToAnyView() : content.eraseToAnyView())
            .onHover { self.isHovering = $0 }
            .eraseToAnyView()
    }
}

Then each Circle on your example would go something like:

Circle().fill(Color.red).frame(width: 50, height: 50)
    .animation(.default)
    .onHover { view in
        _ = print("Mouse hover")
        view.scaleEffect(2.0)
    }

Fossiliferous answered 27/1, 2023 at 20:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.