mac 的 SwiftUI 形状的悬停效果
Hover effect for SwiftUI Shapes for the mac
我想为 mac 的 SwiftUI 形状创建一个悬停效果,它考虑了实际形状。
onHover
修饰符不好,因为它在鼠标进入框架时触发,而不是 SwiftUI Shape
。 hoverEffect
不适用于 Mac。
如何创建考虑底层形状的悬停效果?
import SwiftUI
struct HoverViewModifier : ViewModifier {
@State private var hovered = false
func body(content: Content) -> some View {
content
.foregroundColor(hovered ? .accentColor : .primary)
.onHover { isHovered in
self.hovered = isHovered
}
}
}
struct MyStar : Shape {
func path(in rect : CGRect) -> Path {
let points = [
(50, 0), (61, 33), (97, 34), (69, 56), (79, 90), (50, 70), (20, 90), (30, 56), ( 2, 34), (38, 33)
].map { (x : CGFloat, y : CGFloat) in
CGPoint(x: x * rect.width / 100, y: y * rect.height / 100)
}
var path = Path()
path.addLines(points)
return path
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().modifier(HoverViewModifier())
MyStar().modifier(HoverViewModifier())
}.frame(width: 200, height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这篇优秀的文章 https://swiftui-lab.com/a-powerful-combo/ 演示了如何跟踪鼠标移动。
为此,作者使用 NSViewRepresentable
和 NSHostingView
来访问 AppKit 的 NSView
.
我们可以 return 一个布尔值来指示鼠标是否在 Shape
的 path
内,而不是 return 鼠标位置。
由于我们的内容是 Shape
而不是 View
,因此需要相应地调整 where 子句:where Content : Shape
.
该解决方案使用 NSTrackingArea
和 .mouseMoved
选项。添加 .mouseEnteredAndExited
选项是有意义的,以确保当鼠标离开视图时恢复 non-hovered 状态。
要将 Shape
一方面连接到 TrackingAreaView
,另一方面连接到 HoverViewModifier
,可以创建 Shape
的扩展。此外,HooverViewModifier
中的 @State
必须替换为 @ObservedObject
才能从 Shape 访问它。相应的 ObservableObject
可以简单地看起来像这样:
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
扩展可能如下所示:
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
因此,调用必须稍作更改:
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
一个完整的例子与问题中的改编代码结合开头提到的文章中的稍微扩展的例子可能看起来像这样:
import SwiftUI
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
struct HoverViewModifier : ViewModifier {
@ObservedObject var hoverModel = HoverModel()
func body(content: Content) -> some View {
content
.foregroundColor(hoverModel.hovered ? .accentColor : .primary)
}
}
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
struct MyStar : Shape {
func path(in rect : CGRect) -> Path {
let points = [
(50, 0), (61, 33), (97, 34), (69, 56), (79, 90), (50, 70), (20, 90), (30, 56), ( 2, 34), (38, 33)
].map { (x : CGFloat, y : CGFloat) in
CGPoint(x: x * rect.width / 100, y: y * rect.height / 100)
}
var path = Path()
path.addLines(points)
return path
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
}.frame(width: 200, height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct TrackingAreaView<Content>: View where Content : Shape {
let insideShape: (Bool) -> Void
let content: () -> Content
init(insideShape: @escaping (Bool) -> Void, @ViewBuilder content: @escaping () -> Content) {
self.insideShape = insideShape
self.content = content
}
var body: some View {
TrackingAreaRepresentable(insideShape: insideShape, content: self.content())
}
}
struct TrackingAreaRepresentable<Content>: NSViewRepresentable where Content: Shape {
let insideShape: (Bool) -> Void
let content: Content
func makeNSView(context: Context) -> NSHostingView<Content> {
return TrackingNSHostingView(insideShape: insideShape, rootView: self.content)
}
func updateNSView(_ nsView: NSHostingView<Content>, context: Context) {
}
}
class TrackingNSHostingView<Content>: NSHostingView<Content> where Content : Shape {
let insideShape: (Bool) -> Void
var path = Path()
init(insideShape: @escaping (Bool) -> Void, rootView: Content) {
self.insideShape = insideShape
super.init(rootView: rootView)
setupTrackingArea()
}
override func layout() {
super.layout()
self.path = rootView.path(in: self.bounds)
}
required init(rootView: Content) {
fatalError("init(rootView:) has not been implemented")
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupTrackingArea() {
let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeAlways, .inVisibleRect]
self.addTrackingArea(NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil))
}
override func mouseExited(with event: NSEvent) {
self.insideShape(false)
}
override func mouseMoved(with event: NSEvent) {
return self.checkInside(with: event)
}
override func mouseEntered(with event: NSEvent) {
return self.checkInside(with: event)
}
private func checkInside(with event: NSEvent) {
let inside = path.contains(self.convert(event.locationInWindow, from: nil))
self.insideShape(inside)
}
}
演示
我能够从@stephan-schlecht的回答中去掉 ObservableObject
并制作一个类似于 built-in onHover
的 onHoverInside
修饰符像这样:
extension Shape {
func onHoverInside(action: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: action) { self }
}
}
struct MyHoveredShape<Content> : View where Content : Shape {
@State private var hovered : Bool = false
let shape : Content
var body: some View {
shape
.onHoverInside { isHoveredInside in
hovered = isHoveredInside
}
.foregroundColor(hovered ? .accentColor : .primary)
}
}
extension Shape {
func myHoverModifier() -> some View {
return MyHoveredShape(shape: self)
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().myHoverModifier()
MyStar().myHoverModifier()
}.frame(width: 200, height: 100)
}
}
接下来就看他的回答了
我想为 mac 的 SwiftUI 形状创建一个悬停效果,它考虑了实际形状。
onHover
修饰符不好,因为它在鼠标进入框架时触发,而不是 SwiftUI Shape
。 hoverEffect
不适用于 Mac。
如何创建考虑底层形状的悬停效果?
import SwiftUI
struct HoverViewModifier : ViewModifier {
@State private var hovered = false
func body(content: Content) -> some View {
content
.foregroundColor(hovered ? .accentColor : .primary)
.onHover { isHovered in
self.hovered = isHovered
}
}
}
struct MyStar : Shape {
func path(in rect : CGRect) -> Path {
let points = [
(50, 0), (61, 33), (97, 34), (69, 56), (79, 90), (50, 70), (20, 90), (30, 56), ( 2, 34), (38, 33)
].map { (x : CGFloat, y : CGFloat) in
CGPoint(x: x * rect.width / 100, y: y * rect.height / 100)
}
var path = Path()
path.addLines(points)
return path
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().modifier(HoverViewModifier())
MyStar().modifier(HoverViewModifier())
}.frame(width: 200, height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这篇优秀的文章 https://swiftui-lab.com/a-powerful-combo/ 演示了如何跟踪鼠标移动。
为此,作者使用 NSViewRepresentable
和 NSHostingView
来访问 AppKit 的 NSView
.
我们可以 return 一个布尔值来指示鼠标是否在 Shape
的 path
内,而不是 return 鼠标位置。
由于我们的内容是 Shape
而不是 View
,因此需要相应地调整 where 子句:where Content : Shape
.
该解决方案使用 NSTrackingArea
和 .mouseMoved
选项。添加 .mouseEnteredAndExited
选项是有意义的,以确保当鼠标离开视图时恢复 non-hovered 状态。
要将 Shape
一方面连接到 TrackingAreaView
,另一方面连接到 HoverViewModifier
,可以创建 Shape
的扩展。此外,HooverViewModifier
中的 @State
必须替换为 @ObservedObject
才能从 Shape 访问它。相应的 ObservableObject
可以简单地看起来像这样:
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
扩展可能如下所示:
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
因此,调用必须稍作更改:
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
一个完整的例子与问题中的改编代码结合开头提到的文章中的稍微扩展的例子可能看起来像这样:
import SwiftUI
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
struct HoverViewModifier : ViewModifier {
@ObservedObject var hoverModel = HoverModel()
func body(content: Content) -> some View {
content
.foregroundColor(hoverModel.hovered ? .accentColor : .primary)
}
}
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
struct MyStar : Shape {
func path(in rect : CGRect) -> Path {
let points = [
(50, 0), (61, 33), (97, 34), (69, 56), (79, 90), (50, 70), (20, 90), (30, 56), ( 2, 34), (38, 33)
].map { (x : CGFloat, y : CGFloat) in
CGPoint(x: x * rect.width / 100, y: y * rect.height / 100)
}
var path = Path()
path.addLines(points)
return path
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
}.frame(width: 200, height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct TrackingAreaView<Content>: View where Content : Shape {
let insideShape: (Bool) -> Void
let content: () -> Content
init(insideShape: @escaping (Bool) -> Void, @ViewBuilder content: @escaping () -> Content) {
self.insideShape = insideShape
self.content = content
}
var body: some View {
TrackingAreaRepresentable(insideShape: insideShape, content: self.content())
}
}
struct TrackingAreaRepresentable<Content>: NSViewRepresentable where Content: Shape {
let insideShape: (Bool) -> Void
let content: Content
func makeNSView(context: Context) -> NSHostingView<Content> {
return TrackingNSHostingView(insideShape: insideShape, rootView: self.content)
}
func updateNSView(_ nsView: NSHostingView<Content>, context: Context) {
}
}
class TrackingNSHostingView<Content>: NSHostingView<Content> where Content : Shape {
let insideShape: (Bool) -> Void
var path = Path()
init(insideShape: @escaping (Bool) -> Void, rootView: Content) {
self.insideShape = insideShape
super.init(rootView: rootView)
setupTrackingArea()
}
override func layout() {
super.layout()
self.path = rootView.path(in: self.bounds)
}
required init(rootView: Content) {
fatalError("init(rootView:) has not been implemented")
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupTrackingArea() {
let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeAlways, .inVisibleRect]
self.addTrackingArea(NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil))
}
override func mouseExited(with event: NSEvent) {
self.insideShape(false)
}
override func mouseMoved(with event: NSEvent) {
return self.checkInside(with: event)
}
override func mouseEntered(with event: NSEvent) {
return self.checkInside(with: event)
}
private func checkInside(with event: NSEvent) {
let inside = path.contains(self.convert(event.locationInWindow, from: nil))
self.insideShape(inside)
}
}
演示
我能够从@stephan-schlecht的回答中去掉 ObservableObject
并制作一个类似于 built-in onHover
的 onHoverInside
修饰符像这样:
extension Shape {
func onHoverInside(action: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: action) { self }
}
}
struct MyHoveredShape<Content> : View where Content : Shape {
@State private var hovered : Bool = false
let shape : Content
var body: some View {
shape
.onHoverInside { isHoveredInside in
hovered = isHoveredInside
}
.foregroundColor(hovered ? .accentColor : .primary)
}
}
extension Shape {
func myHoverModifier() -> some View {
return MyHoveredShape(shape: self)
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().myHoverModifier()
MyStar().myHoverModifier()
}.frame(width: 200, height: 100)
}
}
接下来就看他的回答了