MaterialShowcase.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. //
  2. // MaterialShowcase.swift
  3. // MaterialShowcase
  4. //
  5. // Created by Quang Nguyen on 5/4/17.
  6. // Copyright © 2017 Aromajoin. All rights reserved.
  7. //
  8. import UIKit
  9. @objc public protocol MaterialShowcaseDelegate: class {
  10. @objc optional func showCaseWillDismiss(showcase: MaterialShowcase, didTapTarget:Bool)
  11. @objc optional func showCaseDidDismiss(showcase: MaterialShowcase, didTapTarget:Bool)
  12. }
  13. open class MaterialShowcase: UIView {
  14. @objc public enum BackgroundTypeStyle: Int {
  15. case circle //default
  16. case full//full screen
  17. }
  18. // MARK: Material design guideline constant
  19. let BACKGROUND_PROMPT_ALPHA: CGFloat = 0.96
  20. let TARGET_HOLDER_RADIUS: CGFloat = 44
  21. let TEXT_CENTER_OFFSET: CGFloat = 44 + 20
  22. let INSTRUCTIONS_CENTER_OFFSET: CGFloat = 20
  23. let LABEL_MARGIN: CGFloat = 40
  24. let TARGET_PADDING: CGFloat = 20
  25. // Other default properties
  26. let LABEL_DEFAULT_HEIGHT: CGFloat = 50
  27. let BACKGROUND_DEFAULT_COLOR = UIColor.fromHex(hexString: "#2196F3")
  28. let TARGET_HOLDER_COLOR = UIColor.white
  29. // MARK: Animation properties
  30. var ANI_COMEIN_DURATION: TimeInterval = 0.5 // second
  31. var ANI_GOOUT_DURATION: TimeInterval = 0.5 // second
  32. var ANI_TARGET_HOLDER_SCALE: CGFloat = 2.2
  33. let ANI_RIPPLE_COLOR = UIColor.white
  34. let ANI_RIPPLE_ALPHA: CGFloat = 0.5
  35. let ANI_RIPPLE_SCALE: CGFloat = 1.6
  36. var offsetThreshold: CGFloat = 88
  37. // MARK: Private view properties
  38. var closeButton : UIButton!
  39. var containerView: UIView!
  40. var targetView: UIView!
  41. var backgroundView: UIView!
  42. var targetHolderView: UIView!
  43. var hiddenTargetHolderView: UIView!
  44. var targetRippleView: UIView!
  45. var targetCopyView: UIView!
  46. var instructionView: MaterialShowcaseInstructionView!
  47. public var skipButton: (() -> Void)?
  48. var onTapThrough: (() -> Void)?
  49. // MARK: Public Properties
  50. // setSkipImage
  51. public var skipImage = "HintClose"
  52. // Background
  53. @objc public var backgroundAlpha: CGFloat = 1.0
  54. @objc public var backgroundPromptColor: UIColor!
  55. @objc public var backgroundPromptColorAlpha: CGFloat = 0.0
  56. @objc public var backgroundViewType: BackgroundTypeStyle = .circle
  57. @objc public var backgroundRadius: CGFloat = -1.0 // If the value is negative, calculate the radius automatically
  58. // Tap zone settings
  59. // - false: recognize tap from all displayed showcase.
  60. // - true: recognize tap for targetView area only.
  61. @objc public var isTapRecognizerForTargetView: Bool = false
  62. // Target
  63. @objc public var shouldSetTintColor: Bool = true
  64. @objc public var targetTintColor: UIColor!
  65. @objc public var targetHolderRadius: CGFloat = 0.0
  66. @objc public var targetHolderColor: UIColor!
  67. // Text
  68. @objc public var primaryText: String!
  69. @objc public var secondaryText: String!
  70. @objc public var primaryTextColor: UIColor!
  71. @objc public var secondaryTextColor: UIColor!
  72. @objc public var primaryTextSize: CGFloat = 0.0
  73. @objc public var secondaryTextSize: CGFloat = 0.0
  74. @objc public var primaryTextFont: UIFont?
  75. @objc public var secondaryTextFont: UIFont?
  76. @objc public var primaryTextAlignment: NSTextAlignment = .left
  77. @objc public var secondaryTextAlignment: NSTextAlignment = .left
  78. // Animation
  79. @objc public var aniComeInDuration: TimeInterval = 0.0
  80. @objc public var aniGoOutDuration: TimeInterval = 0.0
  81. @objc public var aniRippleScale: CGFloat = 0.0
  82. @objc public var aniRippleColor: UIColor!
  83. @objc public var aniRippleAlpha: CGFloat = 0.0
  84. // Delegate
  85. @objc public weak var delegate: MaterialShowcaseDelegate?
  86. public init() {
  87. // Create frame
  88. let frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
  89. super.init(frame: frame)
  90. configure()
  91. }
  92. // No supported initilization method
  93. required public init?(coder aDecoder: NSCoder) {
  94. fatalError("init(coder:) has not been implemented")
  95. }
  96. }
  97. // MARK: - Public APIs
  98. extension MaterialShowcase {
  99. /// Sets a general UIView as target
  100. @objc public func setTargetView(view: UIView) {
  101. targetView = view
  102. if let label = targetView as? UILabel {
  103. targetTintColor = label.textColor
  104. backgroundPromptColor = label.textColor
  105. } else if let button = targetView as? UIButton {
  106. let tintColor = button.titleColor(for: .normal)
  107. targetTintColor = tintColor
  108. backgroundPromptColor = tintColor
  109. } else {
  110. targetTintColor = targetView.tintColor
  111. backgroundPromptColor = targetView.tintColor
  112. }
  113. }
  114. /// Sets a UIBarButtonItem as target
  115. @objc public func setTargetView(button: UIButton, tapThrough: Bool = false) {
  116. targetView = button
  117. let tintColor = button.titleColor(for: .normal)
  118. targetTintColor = tintColor
  119. backgroundPromptColor = tintColor
  120. if tapThrough {
  121. onTapThrough = { button.sendActions(for: .touchUpInside) }
  122. }
  123. }
  124. /// Sets a UIBarButtonItem as target
  125. @objc public func setTargetView(barButtonItem: UIBarButtonItem, tapThrough: Bool = false) {
  126. if let view = (barButtonItem.value(forKey: "view") as? UIView)?.subviews.first {
  127. targetView = view
  128. if tapThrough {
  129. onTapThrough = { _ = barButtonItem.target?.perform(barButtonItem.action, with: nil) }
  130. }
  131. }
  132. }
  133. /// Sets a UITabBar Item as target
  134. @objc public func setTargetView(tabBar: UITabBar, itemIndex: Int, tapThrough: Bool = false) {
  135. let tabBarItems = orderedTabBarItemViews(of: tabBar)
  136. if itemIndex < tabBarItems.count {
  137. targetView = tabBarItems[itemIndex]
  138. targetTintColor = tabBar.tintColor
  139. backgroundPromptColor = tabBar.tintColor
  140. if tapThrough {
  141. onTapThrough = { tabBar.selectedItem = tabBar.items?[itemIndex] }
  142. }
  143. } else {
  144. print ("The tab bar item index is out of range")
  145. }
  146. }
  147. /// Sets a UITableViewCell as target
  148. @objc public func setTargetView(tableView: UITableView, section: Int, row: Int) {
  149. let indexPath = IndexPath(row: row, section: section)
  150. targetView = tableView.cellForRow(at: indexPath)?.contentView
  151. // for table viewcell, we do not need target holder (circle view)
  152. // therefore, set its radius = 0
  153. targetHolderRadius = 0
  154. }
  155. /// Sets a UICollectionViewCell as target
  156. @objc public func setTargetView(collectionView: UICollectionView, section: Int, item: Int) {
  157. let indexPath = IndexPath(item: item, section: section)
  158. targetView = collectionView.cellForItem(at: indexPath)
  159. // for table viewcell, we do not need target holder (circle view)
  160. // therefore, set its radius = 0
  161. targetHolderRadius = 0
  162. }
  163. @objc func dismissTutorialButtonDidTouch() {
  164. skipButton?()
  165. }
  166. /// Shows it over current screen after completing setup process
  167. @objc public func show(animated: Bool = true,hasShadow: Bool = true, hasSkipButton: Bool = true, completion handler: (()-> Void)?) {
  168. initViews()
  169. alpha = 0.0
  170. containerView.addSubview(self)
  171. layoutIfNeeded()
  172. let scale = TARGET_HOLDER_RADIUS / (backgroundView.frame.width / 2)
  173. let center = backgroundView.center
  174. backgroundView.transform = CGAffineTransform(scaleX: scale, y: scale) // Initial set to support animation
  175. backgroundView.center = targetHolderView.center
  176. if hasSkipButton {
  177. closeButton = UIButton()
  178. closeButton.setImage(UIImage(named: skipImage), for: .normal)
  179. addSubview(closeButton)
  180. closeButton.addTarget(self, action: #selector(dismissTutorialButtonDidTouch), for: .touchUpInside)
  181. let margins = layoutMarginsGuide
  182. closeButton.translatesAutoresizingMaskIntoConstraints = false
  183. closeButton.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0).isActive = true
  184. closeButton.rightAnchor.constraint(equalTo: margins.rightAnchor, constant: -8).isActive = true
  185. closeButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.13).isActive = true
  186. closeButton.heightAnchor.constraint(equalTo: closeButton.widthAnchor, multiplier: 1.0/1.0).isActive = true
  187. }
  188. if hasShadow {
  189. backgroundView.layer.shadowColor = UIColor.black.cgColor
  190. backgroundView.layer.shadowRadius = 5.0
  191. backgroundView.layer.shadowOpacity = 0.5
  192. backgroundView.layer.shadowOffset = .zero
  193. backgroundView.clipsToBounds = false
  194. }
  195. if animated {
  196. UIView.animate(withDuration: aniComeInDuration, animations: {
  197. self.targetHolderView.transform = CGAffineTransform(scaleX: 1, y: 1)
  198. self.backgroundView.transform = CGAffineTransform(scaleX: 1, y: 1)
  199. self.backgroundView.center = center
  200. self.alpha = self.backgroundAlpha
  201. }, completion: { _ in
  202. self.startAnimations()
  203. })
  204. } else {
  205. alpha = backgroundAlpha
  206. }
  207. // Handler user's action after showing.
  208. handler?()
  209. }
  210. }
  211. // MARK: - Utility API
  212. extension MaterialShowcase {
  213. /// Returns the current showcases displayed on screen.
  214. /// It will return null if no showcase exists.
  215. public static func presentedShowcases() -> [MaterialShowcase]? {
  216. guard let window = UIApplication.shared.keyWindow else {
  217. return nil
  218. }
  219. return window.subviews.filter({ (view) -> Bool in
  220. return view is MaterialShowcase
  221. }) as? [MaterialShowcase]
  222. }
  223. }
  224. // MARK: - Setup views internally
  225. extension MaterialShowcase {
  226. /// Initializes default view properties
  227. func configure() {
  228. backgroundColor = UIColor.clear
  229. guard let window = UIApplication.shared.keyWindow else {
  230. return
  231. }
  232. containerView = window
  233. setDefaultProperties()
  234. }
  235. func setDefaultProperties() {
  236. // Background
  237. backgroundPromptColor = BACKGROUND_DEFAULT_COLOR
  238. backgroundPromptColorAlpha = BACKGROUND_PROMPT_ALPHA
  239. // Target view
  240. targetTintColor = BACKGROUND_DEFAULT_COLOR
  241. targetHolderColor = TARGET_HOLDER_COLOR
  242. targetHolderRadius = TARGET_HOLDER_RADIUS
  243. // Text
  244. primaryText = MaterialShowcaseInstructionView.PRIMARY_DEFAULT_TEXT
  245. secondaryText = MaterialShowcaseInstructionView.SECONDARY_DEFAULT_TEXT
  246. primaryTextColor = MaterialShowcaseInstructionView.PRIMARY_TEXT_COLOR
  247. secondaryTextColor = MaterialShowcaseInstructionView.SECONDARY_TEXT_COLOR
  248. primaryTextSize = MaterialShowcaseInstructionView.PRIMARY_TEXT_SIZE
  249. secondaryTextSize = MaterialShowcaseInstructionView.SECONDARY_TEXT_SIZE
  250. // Animation
  251. aniComeInDuration = ANI_COMEIN_DURATION
  252. aniGoOutDuration = ANI_GOOUT_DURATION
  253. aniRippleAlpha = ANI_RIPPLE_ALPHA
  254. aniRippleColor = ANI_RIPPLE_COLOR
  255. aniRippleScale = ANI_RIPPLE_SCALE
  256. }
  257. func startAnimations() {
  258. let options: UIView.KeyframeAnimationOptions = [.curveEaseInOut, .repeat]
  259. UIView.animateKeyframes(withDuration: 1, delay: 0, options: options, animations: {
  260. UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
  261. self.targetRippleView.alpha = self.ANI_RIPPLE_ALPHA
  262. self.targetHolderView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
  263. self.targetRippleView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
  264. })
  265. UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
  266. self.targetHolderView.transform = CGAffineTransform.identity
  267. self.targetRippleView.alpha = 0
  268. self.targetRippleView.transform = CGAffineTransform(scaleX: self.aniRippleScale, y: self.aniRippleScale)
  269. })
  270. }, completion: nil)
  271. }
  272. func initViews() {
  273. let center = calculateCenter(at: targetView, to: containerView)
  274. addTargetRipple(at: center)
  275. addTargetHolder(at: center)
  276. // if color is not UIColor.clear, then add the target snapshot
  277. if targetHolderColor != .clear {
  278. addTarget(at: center)
  279. }
  280. //In iPad version InstructionView was add to backgroundView
  281. if UIDevice.current.userInterfaceIdiom == .pad {
  282. addBackground()
  283. }
  284. addInstructionView(at: center)
  285. instructionView.layoutIfNeeded()
  286. //In iPhone version InstructionView was add to self view
  287. if UIDevice.current.userInterfaceIdiom != .pad {
  288. addBackground()
  289. }
  290. // Disable subview interaction to let users click to general view only
  291. subviews.forEach({$0.isUserInteractionEnabled = false})
  292. if isTapRecognizerForTargetView {
  293. //Add gesture recognizer for targetCopyView
  294. hiddenTargetHolderView.addGestureRecognizer(tapGestureRecoganizer())
  295. hiddenTargetHolderView.isUserInteractionEnabled = true
  296. } else {
  297. // Add gesture recognizer for both container and its subview
  298. addGestureRecognizer(tapGestureRecoganizer())
  299. hiddenTargetHolderView.addGestureRecognizer(tapGestureRecoganizer())
  300. hiddenTargetHolderView.isUserInteractionEnabled = true
  301. }
  302. }
  303. /// Add background which is a big circle
  304. private func addBackground() {
  305. switch self.backgroundViewType {
  306. case .circle:
  307. let radius: CGFloat
  308. if backgroundRadius < 0 {
  309. radius = getDefaultBackgroundRadius()
  310. } else {
  311. radius = backgroundRadius
  312. }
  313. let center = targetRippleView.center
  314. backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: radius * 2,height: radius * 2))
  315. backgroundView.center = center
  316. backgroundView.asCircle()
  317. case .full:
  318. backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width,height: UIScreen.main.bounds.height))
  319. }
  320. backgroundView.backgroundColor = backgroundPromptColor.withAlphaComponent(backgroundPromptColorAlpha)
  321. insertSubview(backgroundView, belowSubview: targetRippleView)
  322. // addBackgroundMask(with: targetHolderRadius, in: backgroundView)
  323. }
  324. private func getDefaultBackgroundRadius() -> CGFloat {
  325. var radius: CGFloat = 0.0
  326. if UIDevice.current.userInterfaceIdiom == .pad {
  327. radius = 300.0
  328. } else {
  329. radius = getOuterCircleRadius(center: center, textBounds: instructionView.frame, targetBounds: targetRippleView.frame)
  330. }
  331. return radius
  332. }
  333. private func addBackgroundMask(with radius: CGFloat, in view: UIView) {
  334. let center = backgroundViewType == .circle ? view.bounds.center : targetRippleView.center
  335. let mutablePath = CGMutablePath()
  336. mutablePath.addRect(view.bounds)
  337. mutablePath.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: 2 * .pi, clockwise: false)
  338. let mask = CAShapeLayer()
  339. mask.path = mutablePath
  340. mask.fillRule = CAShapeLayerFillRule.evenOdd
  341. view.layer.mask = mask
  342. }
  343. /// A background view which add ripple animation when showing target view
  344. private func addTargetRipple(at center: CGPoint) {
  345. targetRippleView = UIView(frame: CGRect(x: 0, y: 0, width: targetHolderRadius * 2,height: targetHolderRadius * 2))
  346. targetRippleView.center = center
  347. targetRippleView.backgroundColor = aniRippleColor
  348. targetRippleView.alpha = 0.0 //set it invisible
  349. targetRippleView.asCircle()
  350. addSubview(targetRippleView)
  351. }
  352. /// A circle-shape background view of target view
  353. private func addTargetHolder(at center: CGPoint) {
  354. hiddenTargetHolderView = UIView()
  355. hiddenTargetHolderView.backgroundColor = .clear
  356. targetHolderView = UIView(frame: CGRect(x: 0, y: 0, width: targetHolderRadius * 2,height: targetHolderRadius * 2))
  357. targetHolderView.center = center
  358. targetHolderView.backgroundColor = targetHolderColor
  359. targetHolderView.asCircle()
  360. hiddenTargetHolderView.frame = targetHolderView.frame
  361. targetHolderView.transform = CGAffineTransform(scaleX: 1/ANI_TARGET_HOLDER_SCALE, y: 1/ANI_TARGET_HOLDER_SCALE) // Initial set to support animation
  362. addSubview(hiddenTargetHolderView)
  363. addSubview(targetHolderView)
  364. }
  365. /// Create a copy view of target view
  366. /// It helps us not to affect the original target view
  367. private func addTarget(at center: CGPoint) {
  368. targetCopyView = targetView.snapshotView(afterScreenUpdates: true)
  369. if shouldSetTintColor {
  370. targetCopyView.setTintColor(targetTintColor, recursive: true)
  371. if targetCopyView is UIButton {
  372. let button = targetView as! UIButton
  373. let buttonCopy = targetCopyView as! UIButton
  374. buttonCopy.setImage(button.image(for: .normal)?.withRenderingMode(.alwaysTemplate), for: .normal)
  375. buttonCopy.setTitleColor(targetTintColor, for: .normal)
  376. buttonCopy.isEnabled = true
  377. } else if targetCopyView is UIImageView {
  378. let imageView = targetView as! UIImageView
  379. let imageViewCopy = targetCopyView as! UIImageView
  380. imageViewCopy.image = imageView.image?.withRenderingMode(.alwaysTemplate)
  381. } else if let imageViewCopy = targetCopyView.subviews.first as? UIImageView,
  382. let labelCopy = targetCopyView.subviews.last as? UILabel {
  383. let imageView = targetView.subviews.first as! UIImageView
  384. imageViewCopy.image = imageView.image?.withRenderingMode(.alwaysTemplate)
  385. labelCopy.textColor = targetTintColor
  386. } else if let label = targetCopyView as? UILabel {
  387. label.textColor = targetTintColor
  388. }
  389. }
  390. let width = targetCopyView.frame.width
  391. let height = targetCopyView.frame.height
  392. targetCopyView.frame = CGRect(x: 0, y: 0, width: width, height: height)
  393. targetCopyView.center = center
  394. targetCopyView.translatesAutoresizingMaskIntoConstraints = true
  395. addSubview(targetCopyView)
  396. }
  397. /// Configures and adds primary label view
  398. private func addInstructionView(at center: CGPoint) {
  399. instructionView = MaterialShowcaseInstructionView()
  400. instructionView.primaryTextAlignment = primaryTextAlignment
  401. instructionView.primaryTextFont = primaryTextFont
  402. instructionView.primaryTextSize = primaryTextSize
  403. instructionView.primaryTextColor = primaryTextColor
  404. instructionView.primaryText = primaryText
  405. instructionView.secondaryTextAlignment = secondaryTextAlignment
  406. instructionView.secondaryTextFont = secondaryTextFont
  407. instructionView.secondaryTextSize = secondaryTextSize
  408. instructionView.secondaryTextColor = secondaryTextColor
  409. instructionView.secondaryText = secondaryText
  410. // Calculate x position
  411. var xPosition = LABEL_MARGIN
  412. // Calculate y position
  413. var yPosition: CGFloat!
  414. // Calculate instructionView width
  415. var width : CGFloat
  416. if UIDevice.current.userInterfaceIdiom == .pad {
  417. width = backgroundView.frame.width - xPosition
  418. if backgroundView.frame.origin.x < 0 {
  419. xPosition = abs(backgroundView.frame.origin.x) + xPosition
  420. } else if (backgroundView.frame.origin.x + backgroundView.frame.size.width >
  421. UIScreen.main.bounds.width) {
  422. width = backgroundView.frame.size.width - (xPosition*2)
  423. }
  424. if xPosition + width > backgroundView.frame.size.width {
  425. width = width - CGFloat(xPosition/2)
  426. }
  427. if getTargetPosition(target: targetView, container: containerView) == .above {
  428. yPosition = (backgroundView.frame.size.height/2) + TEXT_CENTER_OFFSET
  429. } else {
  430. yPosition = TEXT_CENTER_OFFSET + LABEL_DEFAULT_HEIGHT * 2
  431. }
  432. } else {
  433. if getTargetPosition(target: targetView, container: containerView) == .above {
  434. yPosition = center.y + TARGET_PADDING + (targetView.bounds.height / 2 > targetHolderRadius ? targetView.bounds.height / 2 : targetHolderRadius)
  435. } else {
  436. yPosition = center.y - TEXT_CENTER_OFFSET - LABEL_DEFAULT_HEIGHT * 2
  437. }
  438. width = containerView.frame.width - (xPosition + xPosition)
  439. }
  440. instructionView.frame = CGRect(x: xPosition,
  441. y: yPosition,
  442. width: width ,
  443. height: 0)
  444. if UIDevice.current.userInterfaceIdiom == .pad {
  445. backgroundView.addSubview(instructionView)
  446. } else {
  447. addSubview(instructionView)
  448. }
  449. }
  450. /// Handles user's tap
  451. private func tapGestureRecoganizer() -> UIGestureRecognizer {
  452. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(MaterialShowcase.tapGestureSelector))
  453. tapGesture.numberOfTapsRequired = 1
  454. tapGesture.numberOfTouchesRequired = 1
  455. return tapGesture
  456. }
  457. @objc private func tapGestureSelector(tapGesture:UITapGestureRecognizer) {
  458. completeShowcase(didTapTarget: tapGesture.view === hiddenTargetHolderView)
  459. }
  460. /// Default action when dimissing showcase
  461. /// Notifies delegate, removes views, and handles out-going animation
  462. @objc public func completeShowcase(animated: Bool = true, didTapTarget: Bool = false) {
  463. if delegate != nil && delegate?.showCaseDidDismiss != nil {
  464. delegate?.showCaseWillDismiss?(showcase: self, didTapTarget: didTapTarget)
  465. }
  466. if animated {
  467. targetRippleView.removeFromSuperview()
  468. UIView.animateKeyframes(withDuration: aniGoOutDuration, delay: 0, options: [.calculationModeLinear], animations: {
  469. UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 3/5, animations: {
  470. self.targetHolderView.transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
  471. self.backgroundView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
  472. self.backgroundView.alpha = 0
  473. })
  474. UIView.addKeyframe(withRelativeStartTime: 3/5, relativeDuration: 2/5, animations: {
  475. self.alpha = 0
  476. })
  477. }, completion: { (success) in
  478. // Recycle subviews
  479. self.recycleSubviews()
  480. // Remove it from current screen
  481. self.removeFromSuperview()
  482. })
  483. } else {
  484. // Recycle subviews
  485. self.recycleSubviews()
  486. // Remove it from current screen
  487. self.removeFromSuperview()
  488. }
  489. if delegate != nil && delegate?.showCaseDidDismiss != nil {
  490. delegate?.showCaseDidDismiss?(showcase: self, didTapTarget: didTapTarget)
  491. }
  492. if didTapTarget {
  493. onTapThrough?()
  494. }
  495. }
  496. private func recycleSubviews() {
  497. subviews.forEach({$0.removeFromSuperview()})
  498. }
  499. }
  500. // MARK: - Private helper methods
  501. extension MaterialShowcase {
  502. /// Defines the position of target view
  503. /// which helps to place texts at suitable positions
  504. enum TargetPosition {
  505. case above // at upper screen part
  506. case below // at lower screen part
  507. }
  508. /// Detects the position of target view relative to its container
  509. func getTargetPosition(target: UIView, container: UIView) -> TargetPosition {
  510. let center = calculateCenter(at: targetView, to: container)
  511. if center.y < container.frame.height / 2 {
  512. return .above
  513. } else {
  514. return .below
  515. }
  516. }
  517. // Calculates the center point based on targetview
  518. func calculateCenter(at targetView: UIView, to containerView: UIView) -> CGPoint {
  519. let targetRect = targetView.convert(targetView.bounds , to: containerView)
  520. return targetRect.center
  521. }
  522. // Gets all UIView from TabBarItem.
  523. func orderedTabBarItemViews(of tabBar: UITabBar) -> [UIView] {
  524. let interactionViews = tabBar.subviews.filter({$0.isUserInteractionEnabled})
  525. return interactionViews.sorted(by: {$0.frame.minX < $1.frame.minX})
  526. }
  527. }