diff --git a/EasyTipView.xcodeproj/project.pbxproj b/EasyTipView.xcodeproj/project.pbxproj index e2c1abd5..82e75581 100644 --- a/EasyTipView.xcodeproj/project.pbxproj +++ b/EasyTipView.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1310EC961D0C53800000E71E /* EasyTipView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1310EC8B1D0C537F0000E71E /* EasyTipView.framework */; }; 13FB32A91D0C53E5001ACE20 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FB32A61D0C53E2001ACE20 /* Tests.swift */; }; + 3D0E3EA525FA2CC600899BB7 /* TipViewHighlightingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0E3EA425FA2CC600899BB7 /* TipViewHighlightingBackground.swift */; }; 3DEF6DAB23A39E4F007B8C3C /* EasyTipView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DEF6DA823A39E4F007B8C3C /* EasyTipView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3DEF6DAC23A39E4F007B8C3C /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEF6DA923A39E4F007B8C3C /* UIKitExtensions.swift */; }; 3DEF6DAD23A39E4F007B8C3C /* EasyTipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEF6DAA23A39E4F007B8C3C /* EasyTipView.swift */; }; @@ -30,6 +31,7 @@ 13FB32A11D0C53CB001ACE20 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sources/EasyTipView/info.plist; sourceTree = SOURCE_ROOT; }; 13FB32A51D0C53E2001ACE20 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; }; 13FB32A61D0C53E2001ACE20 /* Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tests.swift; path = Tests/EasyTipViewTests/Tests.swift; sourceTree = SOURCE_ROOT; }; + 3D0E3EA425FA2CC600899BB7 /* TipViewHighlightingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipViewHighlightingBackground.swift; sourceTree = ""; }; 3DEF6DA823A39E4F007B8C3C /* EasyTipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EasyTipView.h; sourceTree = ""; }; 3DEF6DA923A39E4F007B8C3C /* UIKitExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; 3DEF6DAA23A39E4F007B8C3C /* EasyTipView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyTipView.swift; sourceTree = ""; }; @@ -96,6 +98,7 @@ 3DEF6DA823A39E4F007B8C3C /* EasyTipView.h */, 3DEF6DAA23A39E4F007B8C3C /* EasyTipView.swift */, 3DEF6DA923A39E4F007B8C3C /* UIKitExtensions.swift */, + 3D0E3EA425FA2CC600899BB7 /* TipViewHighlightingBackground.swift */, ); path = EasyTipView; sourceTree = ""; @@ -220,6 +223,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D0E3EA525FA2CC600899BB7 /* TipViewHighlightingBackground.swift in Sources */, 3DEF6DAD23A39E4F007B8C3C /* EasyTipView.swift in Sources */, 3DEF6DAC23A39E4F007B8C3C /* UIKitExtensions.swift in Sources */, ); diff --git a/Example/EasyTipView/Base.lproj/Main.storyboard b/Example/EasyTipView/Base.lproj/Main.storyboard index a361efcc..5765f415 100644 --- a/Example/EasyTipView/Base.lproj/Main.storyboard +++ b/Example/EasyTipView/Base.lproj/Main.storyboard @@ -141,7 +141,7 @@ + @@ -218,11 +239,13 @@ + + - + @@ -236,6 +259,7 @@ + @@ -265,4 +289,8 @@ + + + + diff --git a/Example/EasyTipView/ViewController.swift b/Example/EasyTipView/ViewController.swift index 9aeebffe..1d9871d1 100644 --- a/Example/EasyTipView/ViewController.swift +++ b/Example/EasyTipView/ViewController.swift @@ -37,6 +37,7 @@ class ViewController: UIViewController, EasyTipViewDelegate { @IBOutlet weak var buttonE: UIButton! @IBOutlet weak var buttonF: UIButton! @IBOutlet weak var buttonG: UIButton! + @IBOutlet weak var buttonH: UIButton! weak var tipView: EasyTipView? @@ -211,6 +212,27 @@ class ViewController: UIViewController, EasyTipViewDelegate { EasyTipView.show(forView: self.buttonG, contentView: contentView, preferences: preferences) + + case buttonH: + + var preferences = EasyTipView.Preferences() + preferences.drawing.backgroundColor = buttonH.backgroundColor! + preferences.drawing.foregroundColor = UIColor.white + preferences.drawing.textAlignment = NSTextAlignment.center + + preferences.drawing.arrowPosition = .top + preferences.animating.showInitialAlpha = 0 + preferences.animating.showDuration = 0.7 + preferences.animating.dismissDuration = 0.7 + preferences.animating.dismissOnTap = true + + preferences.positioning.maxWidth = 150 + preferences.positioning.bubbleInsets = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 4) + + preferences.highlighting.showsOverlay = true + + let view = EasyTipView(text: "Tip view with highlighting overlay", preferences: preferences) + view.show(forView: buttonH, withinSuperview: self.navigationController?.view!) default: diff --git a/README.md b/README.md index 30040b8d..592f82f9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Description - [x] Automatic orientation change adjustments. - [x] Fully customizable appearance (custom content view or simply just text - including `NSAttributedString` - see the Example app). - [x] Fully customizable presentation and dismissal animations. +- [x] Optional highlighting overlay. Installation @@ -134,10 +135,11 @@ tipView.dismiss() ``` Customizing the appearance -------------- -In order to customize the `EasyTipView` appearance and behavior, you can play with the `Preferences` structure which encapsulates all the customizable properties of the ``EasyTipView``. These preferences have been split into three structures: +In order to customize the `EasyTipView` appearance and behavior, you can play with the `Preferences` structure which encapsulates all the customizable properties of the ``EasyTipView``. These preferences have been split into four structures: * ```Drawing``` - encapsulates customizable properties specifying how ```EastTipView``` will be drawn on screen. * ```Positioning``` - encapsulates customizable properties specifying where ```EasyTipView``` will be drawn within its own bounds. * ```Animating``` - encapsulates customizable properties specifying how ```EasyTipView``` will animate on and off screen. +* ```Highlighting``` - encapsulates customizable properties specifying if and how ```EasyTipView``` will show a highlighting overlay. | `Drawing ` attribute | Description | |----------|-------------| @@ -177,6 +179,17 @@ In order to customize the `EasyTipView` appearance and behavior, you can play wi |`dismissDuration`|Dismiss animation duration.| |`dismissOnTap`|Prevents view from dismissing on tap if it is set to false. (Default value is true.)| +| `Highlighting ` attribute | Description | +|----------|-------------| +|`showsOverlay`| Wether or not to display a highlighting overlay. (Default value is false.) | +|`overlayColor`| The color of the highlighting background. | +|`circleColor`| The background color of the highlighting circle. | +|`circleMargin`| The margin of the highlighting circle to the corner of the view the tip view is attached to.This property only takes effect if `circleRadius` is nil. | +|`circleRadius`| The radius of the highlighting circle. If this property has a non-nil value the `circleMargin` property is ignored.| + +
+ + Customising the presentation or dismissal animations -------------- diff --git a/Sources/EasyTipView/EasyTipView.swift b/Sources/EasyTipView/EasyTipView.swift index 3d3e1bed..f23fff23 100644 --- a/Sources/EasyTipView/EasyTipView.swift +++ b/Sources/EasyTipView/EasyTipView.swift @@ -163,6 +163,11 @@ public extension EasyTipView { transform = initialTransform alpha = initialAlpha + if preferences.highlighting.showsOverlay { + overlay.viewToHighlight = view + superview.addSubview(overlay) + } + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) addGestureRecognizer(tap) @@ -174,8 +179,18 @@ public extension EasyTipView { } if animated { - UIView.animate(withDuration: preferences.animating.showDuration, delay: 0, usingSpringWithDamping: damping, initialSpringVelocity: velocity, options: [.curveEaseInOut], animations: animations, completion: nil) - }else{ + UIView.animate(withDuration: preferences.animating.showDuration, + delay: 0, + usingSpringWithDamping: damping, + initialSpringVelocity: velocity, + options: [.curveEaseInOut], + animations: animations, + completion: nil) + + UIView.animate(withDuration: preferences.animating.showDuration * 0.2) { + self.overlay.alpha = 1 + } + } else { animations() } } @@ -199,6 +214,13 @@ public extension EasyTipView { self.removeFromSuperview() self.transform = CGAffineTransform.identity } + + UIView.animate(withDuration: preferences.animating.dismissDuration * 0.2) { + self.overlay.alpha = 0 + } completion: { _ in + self.overlay.removeFromSuperview() + } + } } @@ -256,9 +278,18 @@ open class EasyTipView: UIView { public var dismissOnTap = true } + public struct Highlighting { + public var showsOverlay = false + public var overlayColor = UIColor.black.withAlphaComponent(0.7) + public var circleColor: UIColor? = nil + public var circleMargin = CGFloat(4) + public var circleRadius: CGFloat? = nil + } + public var drawing = Drawing() public var positioning = Positioning() public var animating = Animating() + public var highlighting = Highlighting() public var hasBorder : Bool { return drawing.borderWidth > 0 && drawing.borderColor != UIColor.clear } @@ -313,6 +344,17 @@ open class EasyTipView: UIView { fileprivate(set) open var preferences: Preferences private let content: Content + fileprivate lazy var overlay: TipViewHighlightingBackground = { + let background = TipViewHighlightingBackground(frame: UIScreen.main.bounds) + background.backgroundColor = preferences.highlighting.overlayColor + background.highlightingBackground = preferences.highlighting.circleColor + background.alpha = 0 + background.circleRadius = preferences.highlighting.circleRadius + background.circleMargin = preferences.highlighting.circleMargin + background.tapAction = { [weak self] in self?.handleTap() } + return background + }() + // MARK: - Lazy variables - fileprivate lazy var contentSize: CGSize = { @@ -427,6 +469,7 @@ open class EasyTipView: UIView { , presentingView != nil else { return } UIView.animate(withDuration: 0.3) { + self.overlay.frame = UIScreen.main.bounds self.arrange(withinSuperview: sview) self.setNeedsDisplay() } diff --git a/Sources/EasyTipView/TipViewHighlightingBackground.swift b/Sources/EasyTipView/TipViewHighlightingBackground.swift new file mode 100644 index 00000000..1b35bd25 --- /dev/null +++ b/Sources/EasyTipView/TipViewHighlightingBackground.swift @@ -0,0 +1,124 @@ +// +// TipViewHighlightingBackground.swift +// EasyTipView +// +// Created by Jan Lottermoser on 11.03.21. +// Copyright © 2021 teodorpatras. All rights reserved. +// + +import UIKit + +final class TipViewHighlightingBackground: UIView { + + // MARK: - Public interface + + /// The view around which the highlighting will be shown + var viewToHighlight: UIView? + + /// A closure to execute when the view is tapped + var tapAction: (() -> Void)? + + /// The default margin of the highlighting circle to the frame of `viewToHighlight. + /// This property only takes effect if `circleRadius` is nil. + var circleMargin: CGFloat = 4 + + /// The radius of the highlighting circle. + /// If this property has a non-nil value the `circleMargin` property is ignored. + var circleRadius: CGFloat? + + /// The background color of the highlighting circle. + /// If this property is nil the backgound will not be colored differently. + var highlightingBackground: UIColor? + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + contentMode = .redraw + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGesture) + } + + // MARK: - User input + @objc private func handleTap() { + tapAction?() + } + + // MARK: - Drawing and layout + + public override func draw(_ rect: CGRect) { + super.draw(rect) + + guard let viewToHighlight = viewToHighlight else { return } + + // add a mask with a cicle hole in the position of the viewToHighlight + let mask = CAShapeLayer() + let path = CGMutablePath() + + // for the radius of the circle either take the provided `circleRadius` value + // or compute a radius so the circle has `circleMargin` distance from the corner of the view + let viewFrame = viewToHighlight.superview?.convert(viewToHighlight.frame, to: self) ?? .zero + let width = viewFrame.width / 2 + let height = viewFrame.height / 2 + let distanceToEdge = ((width * width) + (height * height)).squareRoot() + let radius = circleRadius ?? distanceToEdge + circleMargin + + path.addArc(center: viewFrame.center, + radius: radius, + startAngle: 0, + endAngle: 2 * CGFloat.pi, + clockwise: true) + + path.addRect(bounds) + + mask.path = path + mask.fillRule = .evenOdd + self.layer.mask = mask + + addCircleBackground(radius: radius) + } + + private lazy var circleBackground = UIView() + + private func addCircleBackground(radius: CGFloat) { + guard let color = highlightingBackground, let viewToHighlight = viewToHighlight else { return } + + circleBackground.bounds = CGRect(origin: .zero, size: CGSize(width: 2 * radius, height: 2 * radius)) + circleBackground.center = viewToHighlight.center + + let mask = CAShapeLayer() + let path = CGMutablePath() + + path.addEllipse(in: circleBackground.bounds) + mask.path = path + mask.fillRule = .evenOdd + circleBackground.layer.mask = mask + + circleBackground.backgroundColor = color + + if circleBackground.superview == nil { + viewToHighlight.superview?.insertSubview(circleBackground, belowSubview: viewToHighlight) + } + } + + public override func removeFromSuperview() { + super.removeFromSuperview() + circleBackground.removeFromSuperview() + } + + override var alpha: CGFloat { + didSet { + circleBackground.alpha = alpha + } + } +} diff --git a/assets/animation_highlight.gif b/assets/animation_highlight.gif new file mode 100644 index 00000000..0d179ca7 Binary files /dev/null and b/assets/animation_highlight.gif differ