From 78d5f000934f6a36b356411aedaf52c04dc8b0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Silvestrovi=C4=8D?= Date: Sun, 28 Aug 2022 19:26:54 +0300 Subject: [PATCH] Misc fixes (#26) * Impoved file structure * Refactored View extension to be more readable * Small refactors to have less logic in the TooltipModifier * Fixed animation issue #22 * Added zIndex to the config. Fixes #17 * Added width/height functionality to configuraton. Fixes #25 --- Sources/SwiftUITooltip/TooltipModifier.swift | 132 +++++++++++------- Sources/SwiftUITooltip/TooltipSide.swift | 21 --- .../SwiftUITooltip/TooltipViewExtension.swift | 54 ++++++- .../ArrowOnlyTooltipConfig.swift | 5 + .../DefaultTooltipConfig.swift | 5 + .../TooltipConfig.swift | 6 + Sources/SwiftUITooltip/lib/TooltipSide.swift | 33 +++++ .../{ => shapes}/ArrowShape.swift | 6 + 8 files changed, 185 insertions(+), 77 deletions(-) delete mode 100644 Sources/SwiftUITooltip/TooltipSide.swift rename Sources/SwiftUITooltip/{TooltipConfigurations => config}/ArrowOnlyTooltipConfig.swift (89%) rename Sources/SwiftUITooltip/{TooltipConfigurations => config}/DefaultTooltipConfig.swift (89%) rename Sources/SwiftUITooltip/{TooltipConfigurations => config}/TooltipConfig.swift (86%) create mode 100644 Sources/SwiftUITooltip/lib/TooltipSide.swift rename Sources/SwiftUITooltip/{ => shapes}/ArrowShape.swift (79%) diff --git a/Sources/SwiftUITooltip/TooltipModifier.swift b/Sources/SwiftUITooltip/TooltipModifier.swift index 1a767b0..c1c0b38 100644 --- a/Sources/SwiftUITooltip/TooltipModifier.swift +++ b/Sources/SwiftUITooltip/TooltipModifier.swift @@ -28,26 +28,27 @@ struct TooltipModifier: ViewModifier { @State private var contentHeight: CGFloat = 10 @State var animationOffset: CGFloat = 0 + @State var animation: Optional = nil // MARK: - Computed properties - var arrowRotation: Double { Double(config.side.rawValue) * .pi / 4 } - var actualArrowHeight: CGFloat { config.showArrow ? config.arrowHeight : 0 } + var showArrow: Bool { config.showArrow && config.side.shouldShowArrow() } + var actualArrowHeight: CGFloat { self.showArrow ? config.arrowHeight : 0 } var arrowOffsetX: CGFloat { switch config.side { case .bottom, .center, .top: return 0 - case .leading: + case .left: return (contentWidth / 2 + config.arrowHeight / 2) - case .leadingTop, .leadingBottom: + case .topLeft, .bottomLeft: return (contentWidth / 2 + config.arrowHeight / 2 - config.borderRadius / 2 - config.borderWidth / 2) - case .trailing: + case .right: return -(contentWidth / 2 + config.arrowHeight / 2) - case .trailingTop, .trailingBottom: + case .topRight, .bottomRight: return -(contentWidth / 2 + config.arrowHeight / 2 - config.borderRadius / 2 @@ -57,18 +58,18 @@ struct TooltipModifier: ViewModifier { var arrowOffsetY: CGFloat { switch config.side { - case .leading, .center, .trailing: + case .left, .center, .right: return 0 case .top: return (contentHeight / 2 + config.arrowHeight / 2) - case .trailingTop, .leadingTop: + case .topRight, .topLeft: return (contentHeight / 2 + config.arrowHeight / 2 - config.borderRadius / 2 - config.borderWidth / 2) case .bottom: return -(contentHeight / 2 + config.arrowHeight / 2) - case .leadingBottom, .trailingBottom: + case .bottomLeft, .bottomRight: return -(contentHeight / 2 + config.arrowHeight / 2 - config.borderRadius / 2 @@ -80,9 +81,9 @@ struct TooltipModifier: ViewModifier { private func offsetHorizontal(_ g: GeometryProxy) -> CGFloat { switch config.side { - case .leading, .leadingTop, .leadingBottom: + case .left, .topLeft, .bottomLeft: return -(contentWidth + config.margin + actualArrowHeight + animationOffset) - case .trailing, .trailingTop, .trailingBottom: + case .right, .topRight, .bottomRight: return g.size.width + config.margin + actualArrowHeight + animationOffset case .top, .center, .bottom: return (g.size.width - contentWidth) / 2 @@ -91,11 +92,11 @@ struct TooltipModifier: ViewModifier { private func offsetVertical(_ g: GeometryProxy) -> CGFloat { switch config.side { - case .top, .trailingTop, .leadingTop: + case .top, .topRight, .topLeft: return -(contentHeight + config.margin + actualArrowHeight + animationOffset) - case .bottom, .leadingBottom, .trailingBottom: + case .bottom, .bottomLeft, .bottomRight: return g.size.height + config.margin + actualArrowHeight + animationOffset - case .leading, .center, .trailing: + case .left, .center, .right: return (g.size.height - contentHeight) / 2 } } @@ -106,6 +107,7 @@ struct TooltipModifier: ViewModifier { if (config.enableAnimation) { DispatchQueue.main.asyncAfter(deadline: .now() + config.animationTime) { self.animationOffset = config.animationOffset + self.animation = config.animation DispatchQueue.main.asyncAfter(deadline: .now() + config.animationTime*0.1) { self.animationOffset = 0 @@ -121,66 +123,91 @@ struct TooltipModifier: ViewModifier { GeometryReader { g in Text("") .onAppear { - self.contentWidth = g.size.width - self.contentHeight = g.size.height + self.contentWidth = config.width ?? g.size.width + self.contentHeight = config.height ?? g.size.height } } } private var arrowView: some View { - return ArrowShape() - .rotation(Angle(radians: self.arrowRotation)) - .stroke(self.config.borderColor) + guard let arrowAngle = config.side.getArrowAngleRadians() else { + return AnyView(EmptyView()) + } + + return AnyView(ArrowShape() + .rotation(Angle(radians: arrowAngle)) + .stroke(config.borderColor) .background(ArrowShape() .offset(x: 0, y: 1) - .rotation(Angle(radians: self.arrowRotation)) - .frame(width: self.config.arrowWidth+2, height: self.config.arrowHeight+1) - .foregroundColor(self.config.backgroundColor) + .rotation(Angle(radians: arrowAngle)) + .frame(width: config.arrowWidth+2, height: config.arrowHeight+1) + .foregroundColor(config.backgroundColor) - ).frame(width: self.config.arrowWidth, height: self.config.arrowHeight) - .offset(x: self.arrowOffsetX, y: self.arrowOffsetY) + ).frame(width: config.arrowWidth, height: config.arrowHeight) + .offset(x: self.arrowOffsetX, y: self.arrowOffsetY)) } private var arrowCutoutMask: some View { - return ZStack { - Rectangle() - .frame( - width: self.contentWidth + self.config.borderWidth * 2, - height: self.contentHeight + self.config.borderWidth * 2) - .foregroundColor(.white) - Rectangle() - .frame( - width: self.config.arrowWidth, - height: self.config.arrowHeight + self.config.borderWidth) - .rotationEffect(Angle(radians: self.arrowRotation)) - .offset( - x: self.arrowOffsetX, - y: self.arrowOffsetY) - .foregroundColor(.black) + guard let arrowAngle = config.side.getArrowAngleRadians() else { + return AnyView(EmptyView()) } - .compositingGroup() - .luminanceToAlpha() + + return AnyView( + ZStack { + Rectangle() + .frame( + width: self.contentWidth + config.borderWidth * 2, + height: self.contentHeight + config.borderWidth * 2) + .foregroundColor(.white) + Rectangle() + .frame( + width: config.arrowWidth, + height: config.arrowHeight + config.borderWidth) + .rotationEffect(Angle(radians: arrowAngle)) + .offset( + x: self.arrowOffsetX, + y: self.arrowOffsetY) + .foregroundColor(.black) + } + .compositingGroup() + .luminanceToAlpha() + ) } var tooltipBody: some View { GeometryReader { g in ZStack { - RoundedRectangle(cornerRadius: self.config.borderRadius) - .stroke(self.config.borderWidth == 0 ? Color.clear : self.config.borderColor) - .background(RoundedRectangle(cornerRadius: self.config.borderRadius) - .foregroundColor(self.config.backgroundColor)) - .frame(width: self.contentWidth, height: self.contentHeight) + RoundedRectangle(cornerRadius: config.borderRadius) + .stroke(config.borderWidth == 0 ? Color.clear : config.borderColor) + .frame( + minWidth: contentWidth, + idealWidth: contentWidth, + maxWidth: config.width, + minHeight: contentHeight, + idealHeight: contentHeight, + maxHeight: config.height + ) + .background( + RoundedRectangle(cornerRadius: config.borderRadius) + .foregroundColor(config.backgroundColor) + ) .mask(self.arrowCutoutMask) ZStack { content - .padding(self.config.contentPaddingEdgeInsets) - .fixedSize() + .padding(config.contentPaddingEdgeInsets) + .frame( + width: config.width, + height: config.height + ) + .fixedSize(horizontal: config.width == nil, vertical: true) } .background(self.sizeMeasurer) - .overlay(self.arrowView) + .overlay(self.arrowView) } .offset(x: self.offsetHorizontal(g), y: self.offsetVertical(g)) + .animation(self.animation) + .zIndex(config.zIndex) .onAppear { self.dispatchAnimation() } @@ -198,7 +225,12 @@ struct TooltipModifier: ViewModifier { struct Tooltip_Previews: PreviewProvider { static var previews: some View { var config = DefaultTooltipConfig(side: .top) - config.backgroundColor = Color(red: 0.8, green: 0.9, blue: 1) + config.enableAnimation = false +// config.backgroundColor = Color(red: 0.8, green: 0.9, blue: 1) +// config.animationOffset = 10 +// config.animationTime = 1 +// config.width = 120 +// config.height = 80 return VStack { diff --git a/Sources/SwiftUITooltip/TooltipSide.swift b/Sources/SwiftUITooltip/TooltipSide.swift deleted file mode 100644 index 9d75097..0000000 --- a/Sources/SwiftUITooltip/TooltipSide.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// TooltipSide.swift -// -// Created by Antoni Silvestrovic on 24/10/2020. -// Copyright © 2020 Quassum Manus. All rights reserved. -// - -import SwiftUI - -public enum TooltipSide: Int { - case leading = 2 - case center = -1 - case trailing = 6 - case top = 4 - case bottom = 0 - - case leadingTop = 3 - case leadingBottom = 1 - case trailingTop = 5 - case trailingBottom = 7 -} diff --git a/Sources/SwiftUITooltip/TooltipViewExtension.swift b/Sources/SwiftUITooltip/TooltipViewExtension.swift index 2f98c3b..3d42277 100644 --- a/Sources/SwiftUITooltip/TooltipViewExtension.swift +++ b/Sources/SwiftUITooltip/TooltipViewExtension.swift @@ -7,39 +7,81 @@ import SwiftUI +// MARK: - with `enabled: Bool` public extension View { - func tooltip(_ enabled: Bool = true, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { + // Only enable parameter accessible + func tooltip( + _ enabled: Bool = true, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { let config: TooltipConfig = DefaultTooltipConfig.shared return modifier(TooltipModifier(enabled: enabled, config: config, content: content)) } - func tooltip(_ enabled: Bool = true, config: TooltipConfig, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { + // Only enable and config available + func tooltip( + _ enabled: Bool = true, + config: TooltipConfig, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { modifier(TooltipModifier(enabled: enabled, config: config, content: content)) } - func tooltip(_ enabled: Bool = true, side: TooltipSide, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { + // Enable and side are available + func tooltip( + _ enabled: Bool = true, + side: TooltipSide, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { var config = DefaultTooltipConfig.shared config.side = side return modifier(TooltipModifier(enabled: enabled, config: config, content: content)) } - func tooltip(_ enabled: Bool = true, side: TooltipSide, config: TooltipConfig, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { + // Enable, side and config parameters available + func tooltip( + _ enabled: Bool = true, + side: TooltipSide, + config: TooltipConfig, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { var config = config config.side = side return modifier(TooltipModifier(enabled: enabled, config: config, content: content)) } +} - func tooltip(_ side: TooltipSide, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { +// MARK: - Without `enabled: Bool` +public extension View { + // No-parameter tooltip + func tooltip( + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { + let config = DefaultTooltipConfig.shared + + return modifier(TooltipModifier(enabled: true, config: config, content: content)) + } + + // Only side configurable + func tooltip( + _ side: TooltipSide, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { var config = DefaultTooltipConfig.shared config.side = side return modifier(TooltipModifier(enabled: true, config: config, content: content)) } - func tooltip(_ side: TooltipSide, config: TooltipConfig, @ViewBuilder content: @escaping () -> TooltipContent) -> some View { + // Side and config are configurable + func tooltip( + _ side: TooltipSide, + config: TooltipConfig, + @ViewBuilder content: @escaping () -> TooltipContent + ) -> some View { var config = config config.side = side diff --git a/Sources/SwiftUITooltip/TooltipConfigurations/ArrowOnlyTooltipConfig.swift b/Sources/SwiftUITooltip/config/ArrowOnlyTooltipConfig.swift similarity index 89% rename from Sources/SwiftUITooltip/TooltipConfigurations/ArrowOnlyTooltipConfig.swift rename to Sources/SwiftUITooltip/config/ArrowOnlyTooltipConfig.swift index 97c3d70..5edcb0f 100644 --- a/Sources/SwiftUITooltip/TooltipConfigurations/ArrowOnlyTooltipConfig.swift +++ b/Sources/SwiftUITooltip/config/ArrowOnlyTooltipConfig.swift @@ -12,6 +12,10 @@ public struct ArrowOnlyTooltipConfig: TooltipConfig { public var side: TooltipSide = .bottom public var margin: CGFloat = 8 + public var zIndex: Double = 10000 + + public var width: CGFloat? + public var height: CGFloat? public var borderRadius: CGFloat = 8 public var borderWidth: CGFloat = 0 @@ -39,6 +43,7 @@ public struct ArrowOnlyTooltipConfig: TooltipConfig { public var enableAnimation: Bool = false public var animationOffset: CGFloat = 10 public var animationTime: Double = 1 + public var animation: Optional = .easeInOut public var transition: AnyTransition = .opacity diff --git a/Sources/SwiftUITooltip/TooltipConfigurations/DefaultTooltipConfig.swift b/Sources/SwiftUITooltip/config/DefaultTooltipConfig.swift similarity index 89% rename from Sources/SwiftUITooltip/TooltipConfigurations/DefaultTooltipConfig.swift rename to Sources/SwiftUITooltip/config/DefaultTooltipConfig.swift index d41106c..b1f9d1a 100644 --- a/Sources/SwiftUITooltip/TooltipConfigurations/DefaultTooltipConfig.swift +++ b/Sources/SwiftUITooltip/config/DefaultTooltipConfig.swift @@ -12,6 +12,10 @@ public struct DefaultTooltipConfig: TooltipConfig { public var side: TooltipSide = .bottom public var margin: CGFloat = 8 + public var zIndex: Double = 10000 + + public var width: CGFloat? + public var height: CGFloat? public var borderRadius: CGFloat = 8 public var borderWidth: CGFloat = 2 @@ -39,6 +43,7 @@ public struct DefaultTooltipConfig: TooltipConfig { public var enableAnimation: Bool = false public var animationOffset: CGFloat = 10 public var animationTime: Double = 1 + public var animation: Optional = .easeInOut public var transition: AnyTransition = .opacity diff --git a/Sources/SwiftUITooltip/TooltipConfigurations/TooltipConfig.swift b/Sources/SwiftUITooltip/config/TooltipConfig.swift similarity index 86% rename from Sources/SwiftUITooltip/TooltipConfigurations/TooltipConfig.swift rename to Sources/SwiftUITooltip/config/TooltipConfig.swift index 5be92cc..48a18e4 100644 --- a/Sources/SwiftUITooltip/TooltipConfigurations/TooltipConfig.swift +++ b/Sources/SwiftUITooltip/config/TooltipConfig.swift @@ -12,6 +12,11 @@ public protocol TooltipConfig { var side: TooltipSide { get set } var margin: CGFloat { get set } + var zIndex: Double { get set } + + // MARK: - Sizes + var width: CGFloat? { get set } + var height: CGFloat? { get set } // MARK: - Tooltip container @@ -39,6 +44,7 @@ public protocol TooltipConfig { var enableAnimation: Bool { get set } var animationOffset: CGFloat { get set } var animationTime: Double { get set } + var animation: Optional { get set } var transition: AnyTransition { get set } } diff --git a/Sources/SwiftUITooltip/lib/TooltipSide.swift b/Sources/SwiftUITooltip/lib/TooltipSide.swift new file mode 100644 index 0000000..8b71424 --- /dev/null +++ b/Sources/SwiftUITooltip/lib/TooltipSide.swift @@ -0,0 +1,33 @@ +// +// TooltipSide.swift +// +// Created by Antoni Silvestrovic on 24/10/2020. +// Copyright © 2020 Quassum Manus. All rights reserved. +// + +import SwiftUI + + +public enum TooltipSide: Int { + case center = -1 + + case left = 2 + case right = 6 + case top = 4 + case bottom = 0 + + case topLeft = 3 + case topRight = 5 + case bottomLeft = 1 + case bottomRight = 7 + + func getArrowAngleRadians() -> Optional { + if self == .center { return nil } + return Double(self.rawValue) * .pi / 4 + } + + func shouldShowArrow() -> Bool { + if self == .center { return false } + return true + } +} diff --git a/Sources/SwiftUITooltip/ArrowShape.swift b/Sources/SwiftUITooltip/shapes/ArrowShape.swift similarity index 79% rename from Sources/SwiftUITooltip/ArrowShape.swift rename to Sources/SwiftUITooltip/shapes/ArrowShape.swift index bfba7a1..8fa3526 100644 --- a/Sources/SwiftUITooltip/ArrowShape.swift +++ b/Sources/SwiftUITooltip/shapes/ArrowShape.swift @@ -18,3 +18,9 @@ public struct ArrowShape: Shape { return path } } + +struct ArrowShape_Preview: PreviewProvider { + static var previews: some View { + ArrowShape().stroke() + } +}