From f5551844d35f9e7d91e666fce470987c741af9e8 Mon Sep 17 00:00:00 2001 From: Michael Platov Date: Fri, 1 May 2020 18:21:47 +0300 Subject: [PATCH 1/4] allow attributed strings for description --- .../BubbleShowCase/BubbleShowCase.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift index 76defd7..ac245c4 100644 --- a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift +++ b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift @@ -171,6 +171,15 @@ public class BubbleShowCase: UIView { } } + /// Text displayed as the show case description. + public var attributedDescriptionText: NSAttributedString? { + didSet { + descriptionLabel?.attributedText = attributedDescriptionText + setNeedsLayout() + setNeedsDisplay() + } + } + /// screenshot container bckgroundcolor public var sreenshotContainerBackground: UIColor = UIColor.white @@ -1214,8 +1223,14 @@ public class BubbleShowCase: UIView { let descriptionLabel = UILabel() self.descriptionLabel = descriptionLabel descriptionLabel.numberOfLines = 0 - descriptionLabel.text = descriptionText - descriptionLabel.font = descriptionFont + if attributedDescriptionText != nil { + descriptionLabel.attributedText = attributedDescriptionText + } + else { + descriptionLabel.text = descriptionText + descriptionLabel.font = descriptionFont + } + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false bubble.addSubview(descriptionLabel) From 54cdda3181a71660be49f455e193221ec2713596 Mon Sep 17 00:00:00 2001 From: Michael Platov Date: Fri, 1 May 2020 18:23:04 +0300 Subject: [PATCH 2/4] fix flickering when devices rotates --- .../BubbleShowCase/BubbleShowCase.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift index ac245c4..554e778 100644 --- a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift +++ b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift @@ -266,8 +266,8 @@ public class BubbleShowCase: UIView { private var arrowLayers: [CAShapeLayer]? /************** ORIENTATION *******************/ - private var currentOrientation: UIDeviceOrientation = .unknown - private var previousOrientation: UIDeviceOrientation = .unknown + private var currentOrientation: UIInterfaceOrientation = .unknown + private var previousOrientation: UIInterfaceOrientation = .unknown /***************** CONCAT *********************/ fileprivate var nextShowCase: BubbleShowCase? @@ -513,7 +513,7 @@ public class BubbleShowCase: UIView { // Common set up for the showcase private func setUp() { - currentOrientation = UIDevice.current.orientation + currentOrientation = UIApplication.shared.statusBarOrientation previousOrientation = currentOrientation NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil) } @@ -591,7 +591,10 @@ public class BubbleShowCase: UIView { // Redraws those elements that are affected by some change in the screen bounds private func redraw() { takeScreenshot() - arrow.removeFromSuperview() + if (arrow != nil){ + arrow.removeFromSuperview() + } + drawArrow() bubble.isHidden = false @@ -897,13 +900,16 @@ public class BubbleShowCase: UIView { // Device was rotated @objc private func deviceDidRotate() { guard isInitialized else { return } - guard UIDevice.current.orientation != currentOrientation else { return } + + + + guard UIApplication.shared.statusBarOrientation != currentOrientation else { return } bubble.isHidden = true screenshotContainer?.isHidden = true previousOrientation = currentOrientation - currentOrientation = UIDevice.current.orientation + currentOrientation = UIApplication.shared.statusBarOrientation DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in // Gives time to the screen to update self?.redraw() From 2c86618b8d9c5eb73c76b8a88a0be8b7246b7a7d Mon Sep 17 00:00:00 2001 From: Michael Platov Date: Fri, 1 May 2020 18:24:57 +0300 Subject: [PATCH 3/4] confirmation alert for skip button --- .../BubbleShowCase/BubbleShowCase.swift | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift index 554e778..be392d5 100644 --- a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift +++ b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift @@ -14,7 +14,7 @@ The delegate of BubbleShowCase must adopt the BubbleShowCaseDelegate protocol. O @objc public protocol BubbleShowCaseDelegate: class { /// Tells the delegate that the user wants to skip the whole thing - @objc optional func bubbleShowCaseWillSkipTutorial() + @objc optional func bubbleShowCaseDidSkipTutorial() /// Tells the delegate the show case is going to appear into the screen @objc optional func bubbleShowCaseWillTransitionIntoScreen(_ bubbleShowCase: BubbleShowCase) @@ -136,7 +136,7 @@ public class BubbleShowCase: UIView { } - /// Text displayed as the show case title. + /// Text displayed as the show case Skip button. public var skipButtonText: String? { didSet { setNeedsLayout() @@ -144,6 +144,15 @@ public class BubbleShowCase: UIView { } } + /// Message for Skip confirmation dialog + public var skipConfirmMessage: String?; + + // Title for Yes button in skip confirmation dialog + public var skipConfirmYes: String?; + + // Title for No button in skip confirmation dialog + public var skipConfirmNo: String?; + /// Font for the title label. public var titleFont: UIFont = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.bold) { didSet { @@ -1112,7 +1121,28 @@ public class BubbleShowCase: UIView { @objc private func skipDidTap(gestureRecognizer: UITapGestureRecognizer) { - delegate?.bubbleShowCaseWillSkipTutorial?() + + if skipConfirmMessage != nil && skipConfirmNo != nil && skipConfirmYes != nil { + let alert = UIAlertController(title:"", message: skipConfirmMessage!, preferredStyle: UIAlertController.Style.alert); + + self.isHidden = true + alert.addAction(UIAlertAction(title: skipConfirmYes!, style: UIAlertAction.Style.default, handler: { action in + self.delegate?.bubbleShowCaseDidSkipTutorial?() + self.dismiss(); + })) + alert.addAction(UIAlertAction(title: skipConfirmNo!, style: UIAlertAction.Style.cancel, handler: { action in + self.isHidden = false; + })) + + self.window?.rootViewController?.present(alert, animated: true, completion:nil) + + } + else { // no skip confirmation + self.delegate?.bubbleShowCaseDidSkipTutorial?() + self.dismiss(); + } + + } } // Constraints the bubble to the target for both leftAndSide and upAndDown arrow directions From 9f3fd6b1de33bc7fd6eda7550a18d08fc047dd9e Mon Sep 17 00:00:00 2001 From: Michael Platov Date: Fri, 1 May 2020 18:26:55 +0300 Subject: [PATCH 4/4] options to dismiss showcase by tapping background or bubble --- .../BubbleShowCase/BubbleShowCase.swift | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift index be392d5..6f7b348 100644 --- a/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift +++ b/BubbleShowCase/BubbleShowCase/BubbleShowCase.swift @@ -207,6 +207,12 @@ public class BubbleShowCase: UIView { /// The object that acts as the delegate of a Show Case. The delegate is not retained and is therefore qualified as *weak*. public weak var delegate: BubbleShowCaseDelegate? + /// Indicates wether or not the user can close the show case by tapping background + public var isBackgroundDismissable = true + + /// Indicates wether or not the user can close the show case by tapping the bubble + public var isBubbleDismissable = true + /** Direction of the arrow the show case points to. There are 6 possible values. @@ -540,6 +546,9 @@ public class BubbleShowCase: UIView { if arrowDirection != .leftAndRight && arrowDirection != .upAndDown && arrowDirection != .none { embedScreenshot() } + else { + addGestureScreenshotRecognizers(view: self) // add recognizers to self + } embedBubble() @@ -869,39 +878,39 @@ public class BubbleShowCase: UIView { } // Adds gestureRecognizers to the target screenshot to react to some gestures. - private func addGestureRecognizersToScreenshot() { + private func addGestureScreenshotRecognizers(view: UIView) { let longPressTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(targetDidLongPress)) longPressTapGestureRecognizer.minimumPressDuration = 0.5 - screenshotContainer.addGestureRecognizer(longPressTapGestureRecognizer) + view.addGestureRecognizer(longPressTapGestureRecognizer) let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(targetDidDoubleTap(gestureRecognizer:))) doubleTapGestureRecognizer.numberOfTapsRequired = 2 doubleTapGestureRecognizer.require(toFail: longPressTapGestureRecognizer) - screenshotContainer.addGestureRecognizer(doubleTapGestureRecognizer) + view.addGestureRecognizer(doubleTapGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(targetDidTap(gestureRecognizer:))) tapGestureRecognizer.numberOfTapsRequired = 1 tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) - screenshotContainer.addGestureRecognizer(tapGestureRecognizer) - + view.addGestureRecognizer(tapGestureRecognizer) + let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(targetDidSwipeLeft(gestureRecognizer:))) swipeLeftGestureRecognizer.direction = .left - screenshotContainer.addGestureRecognizer(swipeLeftGestureRecognizer) + view.addGestureRecognizer(swipeLeftGestureRecognizer) let swipeRightGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(targetDidSwipeRight(gestureRecognizer:))) swipeRightGestureRecognizer.direction = .right swipeRightGestureRecognizer.require(toFail: swipeLeftGestureRecognizer) - screenshotContainer.addGestureRecognizer(swipeRightGestureRecognizer) + view.addGestureRecognizer(swipeRightGestureRecognizer) let swipeUpGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(targetDidSwipeUp(gestureRecognizer:))) swipeUpGestureRecognizer.direction = .up swipeUpGestureRecognizer.require(toFail: swipeRightGestureRecognizer) - screenshotContainer.addGestureRecognizer(swipeUpGestureRecognizer) + view.addGestureRecognizer(swipeUpGestureRecognizer) let swipeDownGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(targetDidSwipeDown(gestureRecognizer:))) swipeDownGestureRecognizer.direction = .down swipeDownGestureRecognizer.require(toFail: swipeUpGestureRecognizer) - screenshotContainer.addGestureRecognizer(swipeDownGestureRecognizer) + view.addGestureRecognizer(swipeDownGestureRecognizer) } //MARK: Events @@ -989,7 +998,7 @@ public class BubbleShowCase: UIView { self.screenshotContainer = screenshotContainer screenshotContainer.translatesAutoresizingMaskIntoConstraints = false addSubview(screenshotContainer) - addGestureRecognizersToScreenshot() + addGestureScreenshotRecognizers(view: screenshotContainer) if let shadowColor = self.shadowColor { screenshotContainer.layer.backgroundColor = UIColor.clear.cgColor @@ -1077,6 +1086,8 @@ public class BubbleShowCase: UIView { let height = NSLayoutConstraint(item: bubble, attribute: .height, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .height, multiplier: 1, constant: 50) bubble.addConstraint(height) + addGestureRecognizerToBubble() + switch arrowDirection { case .up, .down: constraintBubbleForTopDirections() @@ -1119,6 +1130,24 @@ public class BubbleShowCase: UIView { skipLabel.addGestureRecognizer(tapGestureRecognizer) } + private func addGestureRecognizerToBubble(){ + var tapGestureRecognizer : UITapGestureRecognizer? + if isBubbleDismissable { + tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(bubbleDidTap(gestureRecognizer:))) + tapGestureRecognizer!.numberOfTapsRequired = 1 + bubble.addGestureRecognizer(tapGestureRecognizer!) + } + + if isBackgroundDismissable{ + let backgroundTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenshotContainerDidTap(gestureRecognizer:))) + backgroundTapGestureRecognizer.numberOfTapsRequired = 1 + if tapGestureRecognizer != nil { + backgroundTapGestureRecognizer.require(toFail: tapGestureRecognizer!) + } + self.addGestureRecognizer(backgroundTapGestureRecognizer) + } + } + @objc private func skipDidTap(gestureRecognizer: UITapGestureRecognizer) { @@ -1143,7 +1172,20 @@ public class BubbleShowCase: UIView { } } + + @objc + private func bubbleDidTap(gestureRecognizer: UITapGestureRecognizer) { + self.delegate?.bubbleShowCase?(self, didTap: bubble, gestureRecognizer: gestureRecognizer) + dismiss(); + } + + @objc + private func screenshotContainerDidTap(gestureRecognizer: UITapGestureRecognizer) { + self.delegate?.bubbleShowCase?(self, didTap: bubble, gestureRecognizer: gestureRecognizer) + dismiss(); } + + // Constraints the bubble to the target for both leftAndSide and upAndDown arrow directions private func constraintBubbleForDoubleDirections() {