ephread/Instructions

Flickering When Closing Coach Marks on Tap

alionaalias opened this issue · 1 comments

I've been using the Instructions library for adding coach marks to my application and it's been great. However, I've stumbled upon an issue where a flicker is observed when closing the coach marks by tapping on them.

Here's a brief description of the issue:

  1. The flicker happens only when closing the coach marks by tapping on them, and not when closing by tapping elsewhere on the screen.
  2. I've tried using the stop(immediately: true) and stop(immediately: false) methods to close the coach marks, but the flicker persists.

I have looked through the documentation but couldn't find a solution to this problem. I've tried tweaking the animation properties to attempt to smoothen the transition but to no avail.

I'm attaching a video to demonstrate the flicker. The flicker happens when transitioning from the coach mark to the normal state of the app.

[Video or GIF demonstrating the issue]

Here's the relevant code snippet from my project:

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

    let didShowOnboardingTooltip = UserDefaults.standard.bool(forKey: self.privateFullScreanDidShowOnboardingTooltipKey)
    if !didShowOnboardingTooltip {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0) { [weak self] in
            guard let strongSelf = self else { return }
            strongSelf.coachMarksController.start(in: .viewController(strongSelf))
            UserDefaults.standard.setValue(true, forKey: strongSelf.privateFullScreanDidShowOnboardingTooltipKey)
        }
    }
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    coachMarksController.stop(immediately: true)
}

func numberOfCoachMarks(for coachMarksController: Instructions.CoachMarksController) -> Int {
2
}

func coachMarksController(_ coachMarksController: CoachMarksController, coachMarkAt index: Int) -> CoachMark {
    switch index {
    case 0:
        return coachMarksController.helper.makeCoachMark(for: sendToPublicBtn)
    case 1:
        return coachMarksController.helper.makeCoachMark(for: sendToRecommendsBtn)
    default:
        return coachMarksController.helper.makeCoachMark()
    }
}

func coachMarksController(_ coachMarksController: Instructions.CoachMarksController, coachMarkViewsAt index: Int, madeFrom coachMark: Instructions.CoachMark) -> (bodyView: (UIView & Instructions.CoachMarkBodyView), arrowView: (UIView & Instructions.CoachMarkArrowView)?) {
    
    let coachViews = coachMarksController.helper.makeDefaultCoachViews(withArrow: true, arrowOrientation: coachMark.arrowOrientation)
    
    var text: String
    var boldSubstring: String?
    
    switch index {
    case 0:
        text = "Tap here to move this wish to your Public wishlist. Others can see and know what to gift you."
        boldSubstring = "Public wishlist"
    case 1:
        text = "Tap here to show this item in Recommendations from you."
        boldSubstring = "Recommendations"
    default:
        text = ""
    }
    
    let attributedText = NSMutableAttributedString(string: text)
    let regularFont = UIFont.systemFont(ofSize: 14)
    let boldFont = UIFont.boldSystemFont(ofSize: 14)
    let regularAttributes: [NSAttributedString.Key: Any] = [.font: regularFont]
    
    attributedText.addAttributes(regularAttributes, range: NSRange(location: 0, length: text.count))
    
    if let boldSubstring = boldSubstring, let range = text.range(of: boldSubstring) {
        let nsRange = NSRange(range, in: text)
        attributedText.addAttributes([.font: boldFont], range: nsRange)
    }
    
    coachViews.bodyView.hintLabel.attributedText = attributedText
    coachViews.bodyView.nextLabel.text = "Ok!"
    coachViews.bodyView.background.innerColor = UIColor.rgbColorFor(hex: "696DBF")
    coachViews.arrowView?.background.innerColor = UIColor.rgbColorFor(hex: "696DBF")
    coachViews.bodyView.background.borderColor = UIColor.rgbColorFor(hex: "696DBF")
    coachViews.arrowView?.background.borderColor = UIColor.rgbColorFor(hex: "696DBF")
    coachViews.bodyView.hintLabel.textColor = .white
    coachViews.bodyView.nextLabel.textColor = .white
    coachViews.bodyView.separator.translatesAutoresizingMaskIntoConstraints = false
    coachViews.bodyView.separator.bottomAnchor.constraint(equalTo: coachViews.bodyView.bottomAnchor, constant: -10).isActive = true
    coachViews.bodyView.separator.topAnchor.constraint(equalTo: coachViews.bodyView.topAnchor, constant: 10).isActive = true
    coachViews.bodyView.layer.cornerRadius = 10
    coachViews.bodyView.clipsToBounds = true
    
    return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)
}
func fetchFadeAnimationOfCoachMark(_ coachMark: CoachMark, forIndex index: Int) -> CAAnimationGroup? {
    let animationGroup = CAAnimationGroup()
    
    let fadeAnimation = CABasicAnimation(keyPath: "opacity")
    fadeAnimation.fromValue = 0.0
    fadeAnimation.toValue = 1.0
    fadeAnimation.duration = 0.3
    
    animationGroup.animations = [fadeAnimation]
    animationGroup.duration = 0.3
    
    return animationGroup
}

func fetchTransitionAnimationOfCoachMark(_ coachMark: CoachMark, forIndex index: Int) -> CAAnimationGroup? {
    let animationGroup = CAAnimationGroup()
    
    let fadeAnimation = CABasicAnimation(keyPath: "opacity")
    fadeAnimation.fromValue = 0.0
    fadeAnimation.toValue = 1.0
    fadeAnimation.duration = 0.3
    
    animationGroup.animations = [fadeAnimation]
    animationGroup.duration = 0.3
    
    return animationGroup
}

func didTap(in coachMarksController: Instructions.CoachMarksController, at index: Int?) {
    coachMarksController.stop(immediately: true)
}
func shouldHandleOverlayTap(in coachMarksController: Instructions.CoachMarksController, at index: Int) -> Bool {
    return true
}
func shouldAnimateOverlay(in coachMarksController: Instructions.CoachMarksController, for index: Int) -> Bool {
    return false
}
FILE.2023-09-29.16.27.31.mp4

Solved: Flickering Issue on Coach Mark Disappearance

It turned out that the functions fetchFadeAnimationOfCoachMark() and fetchTransitionAnimationOfCoachMark() I implemented above were not being called. A careful review of the documentation helped to correctly implement and call the necessary functions, solving the problem.
I managed to solve the problem by setting the animation duration to 0 in the fetchDisappearanceTransitionOfCoachMark and fetchAppearanceTransitionOfCoachMark methods of the CoachMarksControllerAnimationDelegate protocol. Here's the code snippet:
func coachMarksController(
_ coachMarksController: CoachMarksController,
fetchAppearanceTransitionOfCoachMark coachMarkView: UIView,
at index: Int,
using manager: CoachMarkTransitionManager
) {
manager.parameters.duration = 0
manager.parameters.delay = 0
manager.animate(.regular, animations: { context in
coachMarkView.alpha = 1
})
}

func coachMarksController(
_ coachMarksController: CoachMarksController,
fetchDisappearanceTransitionOfCoachMark coachMarkView: UIView,
at index: Int,
using manager: CoachMarkTransitionManager
) {
manager.parameters.duration = 0
manager.parameters.delay = 0
manager.animate(.regular, animations: { context in
coachMarkView.alpha = 0
})
}

This change eliminated the flickering effect, providing a smoother experience.