diff --git a/Demo/Marker.xcodeproj/project.pbxproj b/Demo/Marker.xcodeproj/project.pbxproj index 11eacee..50b4e17 100644 --- a/Demo/Marker.xcodeproj/project.pbxproj +++ b/Demo/Marker.xcodeproj/project.pbxproj @@ -7,7 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 08A877ED29363F140047F6AC /* Marker+Info+Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A877E629363F140047F6AC /* Marker+Info+Text.swift */; }; + 0856B5EE2937A1B6001B6369 /* Marker+Draw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856B5ED2937A1B6001B6369 /* Marker+Draw.swift */; }; + 0856B5F22937B160001B6369 /* Marker+Info+Alignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856B5F12937B160001B6369 /* Marker+Info+Alignment.swift */; }; + 08762CBA2938489500BF9409 /* Marker+Calculate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08762CB92938489500BF9409 /* Marker+Calculate.swift */; }; 08A877EE29363F140047F6AC /* Marker+Info+Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A877E729363F140047F6AC /* Marker+Info+Options.swift */; }; 08A877EF29363F140047F6AC /* Marker+Info+ArrowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A877E829363F140047F6AC /* Marker+Info+ArrowPosition.swift */; }; 08A877F029363F140047F6AC /* Marker+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A877E929363F140047F6AC /* Marker+Info.swift */; }; @@ -25,8 +27,10 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0856B5ED2937A1B6001B6369 /* Marker+Draw.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Marker+Draw.swift"; sourceTree = ""; }; + 0856B5F12937B160001B6369 /* Marker+Info+Alignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Marker+Info+Alignment.swift"; sourceTree = ""; }; + 08762CB92938489500BF9409 /* Marker+Calculate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Marker+Calculate.swift"; sourceTree = ""; }; 08990CF726467BAF004202BA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; - 08A877E629363F140047F6AC /* Marker+Info+Text.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Marker+Info+Text.swift"; sourceTree = ""; }; 08A877E729363F140047F6AC /* Marker+Info+Options.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Marker+Info+Options.swift"; sourceTree = ""; }; 08A877E829363F140047F6AC /* Marker+Info+ArrowPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Marker+Info+ArrowPosition.swift"; sourceTree = ""; }; 08A877E929363F140047F6AC /* Marker+Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Marker+Info.swift"; sourceTree = ""; }; @@ -78,12 +82,14 @@ children = ( 08A877EB29363F140047F6AC /* Marker.swift */, 08A877EC29363F140047F6AC /* Marker+Appearence.swift */, + 08762CB92938489500BF9409 /* Marker+Calculate.swift */, + 0856B5ED2937A1B6001B6369 /* Marker+Draw.swift */, 08A877E929363F140047F6AC /* Marker+Info.swift */, + 0856B5F12937B160001B6369 /* Marker+Info+Alignment.swift */, 08A877E829363F140047F6AC /* Marker+Info+ArrowPosition.swift */, 08A877EA29363F140047F6AC /* Marker+Info+Color.swift */, - 08A877E729363F140047F6AC /* Marker+Info+Options.swift */, - 08A877E629363F140047F6AC /* Marker+Info+Text.swift */, 08A877F4293648B20047F6AC /* Marker+Info+CornerStyle.swift */, + 08A877E729363F140047F6AC /* Marker+Info+Options.swift */, 08A877F6293648E40047F6AC /* Marker+Info+Style.swift */, ); path = Marker; @@ -195,15 +201,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0856B5F22937B160001B6369 /* Marker+Info+Alignment.swift in Sources */, 08A877EF29363F140047F6AC /* Marker+Info+ArrowPosition.swift in Sources */, 08F7A69325B074C000250D41 /* ViewController.swift in Sources */, + 0856B5EE2937A1B6001B6369 /* Marker+Draw.swift in Sources */, 08F7A68F25B074C000250D41 /* AppDelegate.swift in Sources */, 08A877F7293648E40047F6AC /* Marker+Info+Style.swift in Sources */, - 08A877ED29363F140047F6AC /* Marker+Info+Text.swift in Sources */, 08F7A69125B074C000250D41 /* SceneDelegate.swift in Sources */, 08A877F129363F140047F6AC /* Marker+Info+Color.swift in Sources */, 08A877EE29363F140047F6AC /* Marker+Info+Options.swift in Sources */, 08A877F029363F140047F6AC /* Marker+Info.swift in Sources */, + 08762CBA2938489500BF9409 /* Marker+Calculate.swift in Sources */, 08A877F229363F140047F6AC /* Marker.swift in Sources */, 08A877F329363F140047F6AC /* Marker+Appearence.swift in Sources */, 08A877F5293648B20047F6AC /* Marker+Info+CornerStyle.swift in Sources */, diff --git a/Demo/Marker/Base.lproj/Main.storyboard b/Demo/Marker/Base.lproj/Main.storyboard index 31cf753..435f642 100644 --- a/Demo/Marker/Base.lproj/Main.storyboard +++ b/Demo/Marker/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -17,56 +17,56 @@ - - + + - - + + - - + + - + @@ -79,12 +79,12 @@ - + @@ -94,56 +94,56 @@ - - + + - - + + - + - - + + @@ -257,8 +257,8 @@ - + diff --git a/Demo/Marker/ViewController.swift b/Demo/Marker/ViewController.swift index 62d5f08..60a3e63 100644 --- a/Demo/Marker/ViewController.swift +++ b/Demo/Marker/ViewController.swift @@ -53,19 +53,19 @@ class ViewController: UIViewController { } @objc func bottomAction(_ sender: UIButton) { - let marker = Marker(.init(marker: bottomButton, intro: "Marker 引导,显示在控件上方"), identifier: "bottom") - marker.next(.init(marker: bottomButtons[0], intro: "Marker: 支持三角箭头位置调整, 支持调整左/中/右, 且可调整偏移量, 本次显示为自动处理三角箭头位置")) - marker.next(.init(marker: bottomButtons[1], intro: "本次三角箭头在左侧,且向右偏移(移动) 10px", styles: [.arrowPosition(.left(offset: 10))])) - marker.next(.init(marker: bottomButtons[2], intro: "本次三角箭头在右侧,且向左偏移(移动) 10px", styles: [.arrowPosition(.right(offset: -10))])) - marker.next(.init(marker: bottomButtons[3], intro: "本次三角箭头在中间,无偏移", styles: [.arrowPosition(.center(offset: 0))])) - marker.next(.init(marker: bottomButtons[4], intro: "无")) - marker.next(.init(marker: bottomButtons[5], intro: "无")) + let marker = Marker(.init(marker: bottomButton, intro: "Marker guide, all configurate is default. auto handle and display.\n所有配置都是默认的。"), identifier: "bottom") + marker.next(.init(marker: bottomButtons[0], intro: "Support control triangle arrow position (left, center, right) and offset.\n支持三角箭头位置调整(左/中/右), 且可调整偏移量, 本次显示为自动处理三角箭头位置.")) + marker.next(.init(marker: bottomButtons[1], intro: "Triangle arrow on left, and offset set 10.\n本次三角箭头在左侧,且向右偏移 10px.", styles: [.arrowPosition(.left(offset: 10))])) + marker.next(.init(marker: bottomButtons[2], intro: "Triangle arrow on right, and offset set -10.\n本次三角箭头在右侧,且向左偏移 10px.", styles: [.arrowPosition(.right(offset: -10))])) + marker.next(.init(marker: bottomButtons[3], intro: "Triangle arrow on center (default value).\n居中,默认处理方式。", styles: [.arrowPosition(.center(offset: 0))])) + marker.next(.init(marker: bottomButtons[4], intro: "Align left. triangle position auto handle.\n左对齐,三角箭头自动处理。", styles: [.hAlignment(.left)])) + marker.next(.init(marker: bottomButtons[5], intro: "Align right. triangle position auto handle.\n右对齐,三角箭头自动处理。", styles: [.hAlignment(.right)])) marker.show(on: self.view, completion: nil) } @objc func respondAction(_ sender: UIButton) { - let alert = UIAlertController(title: "透传事件", message: "你点击了透传按钮, 且按下`知道了`的时候会触发下一步引导", preferredStyle: .alert) - alert.addAction(.init(title: "知道了", style: .cancel, handler: { _ in + let alert = UIAlertController(title: "Pass event 事件穿透", message: "You click on the highlighted range and the click event is passed to the button.\n你点击了高亮范围,并且事件被传递到了按钮上.", preferredStyle: .alert) + alert.addAction(.init(title: "I know 我晓得了", style: .cancel, handler: { _ in Marker.instance(from: "normal")?.showNext(triggerByUser: true) })) self.present(alert, animated: true, completion: nil) @@ -96,13 +96,13 @@ class ViewController: UIViewController { Marker.default.timeoutAfterAnimateDidCompletion = true Marker.default.timeout = 0 - let startInfo = Marker.Info(marker: startButton, intro: "起始按钮, 默认配置, 最大宽度 320, 点击任意处进入下一个", styles: [.arrowPosition(.left(offset: 0))]) - let number2Info = Marker.Info(marker: number2Button, intro: "第二个按钮, 默认配置", styles: [.arrowPosition(.right(offset: 0))]) - let actionInfo = Marker.Info(marker: respondActionButton, intro: "第三个按钮, 可透传事件:仅点击高亮范围有效,且点击事,事件可以传递到按钮上(执行按钮的点击事件)并触发下一步事件", options: [.strongGuidance, .eventPenetration]) - let noMaskInfo = Marker.Info(marker: noMaskButton, intro: "第四个按钮, 没有遮罩", styles: [.arrowPosition(.center(offset: 0)), .dimFrame(.zero)]) - let roundStyleInfo = Marker.Info(marker: roundButton, intro: "第五个按钮, 圆角遮罩, 且高亮范围有 10px 的扩张", styles: [.cornerStyle(.round), .highlightRangeExpande(10)]) - let squareStyleInfo = Marker.Info(marker: squareButton, intro: "第六个按钮, 方形遮罩", styles: [.cornerStyle(.square)]) - let followStyleInfo = Marker.Info(marker: followStyleButton, intro: "第七个按钮, 跟随视图的风格, 视图是圆角就是圆角,方形就是方形, 高亮范围有 4px 的扩张", styles: [.cornerStyle(.marker), .highlightRangeExpande(4)]) + let startInfo = Marker.Info(marker: startButton, intro: "Description.\n样式描述。") + let number2Info = Marker.Info(marker: number2Button, intro: "styles: [.hideArrow], no triangle arrow.\n没有三角指示箭头。", styles: [.hideArrow]) + let actionInfo = Marker.Info(marker: respondActionButton, intro: "options: [.strongGuidance, .eventPenetration], now you can tap the button.\n强引导和事件穿透,你可以点击触发按钮的响应事件了。", options: [.strongGuidance, .eventPenetration]) + let noMaskInfo = Marker.Info(marker: noMaskButton, intro: "styles: [.dimFrame(.zero)], no gray mask.\n没有遮罩。", styles: [.dimFrame(.zero)]) + let roundStyleInfo = Marker.Info(marker: roundButton, intro: "styles: [.cornerStyle(.round), .highlightRangeExpande(10)], highlight range is round and it has a 10px expande.\n圆角遮罩, 且高亮范围有 10px 的扩张。", styles: [.cornerStyle(.round), .highlightRangeExpande(10)]) + let squareStyleInfo = Marker.Info(marker: squareButton, intro: "styles: [.timeout(2)], timeout 2 seconds.\n两秒后自动消失。", styles: [.timeout(2)]) + let followStyleInfo = Marker.Info(marker: followStyleButton, intro: "More details see: `Marker+Info+Style` and `Marker+Info+Option`.\n更多信息请参考`Marker+Info+Style` 和 `Marker+Info+Option`。", styles: [.cornerStyle(.marker), .highlightRangeExpande(4)]) Marker(startInfo, identifier: "normal") .nexts([number2Info, actionInfo, noMaskInfo, roundStyleInfo, squareStyleInfo, followStyleInfo]) diff --git a/Demo/preview-new.gif b/Demo/preview-new.gif new file mode 100644 index 0000000..44a7424 Binary files /dev/null and b/Demo/preview-new.gif differ diff --git a/README.md b/README.md index a7a6532..861d9c7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Light, convenient, and qualified guidance prompts. # Preview -![Demo](Demo/preview.gif) +![Demo](Demo/preview-new.gif) # Features @@ -131,6 +131,40 @@ Marker(info) ) ``` +### HAlignment / VAlignment + +#### \#HAlignment + +Describe `intro` horizontal alignment. +```swift +public enum HAlignment { + /// `Default` if available. + case center + + case left + case right +} + +Marker.Info(... styles: [.hAlignment(Marker.Info.HAlignment)]) +``` + +#### \#VAlignment + +Describe `intro` vertical alignment. +```swift +public enum VAlignment { + /// `Default`. + case auto + + /// Above the highlighted view. + case top + /// Below the highlighted view. + case bottom +} + +Marker.Info(... styles: [.vAlignment(Marker.Info.VAlignment)]) +``` + ### Strong guidance #### \#1 diff --git a/Sources/Marker/Marker+Appearence.swift b/Sources/Marker/Marker+Appearence.swift index dc77068..5c7c104 100644 --- a/Sources/Marker/Marker+Appearence.swift +++ b/Sources/Marker/Marker+Appearence.swift @@ -21,7 +21,7 @@ extension Marker { public var textColor: UIColor = .white /// Spacing between triangle arrow and highlight view. - public var spacing: CGFloat = 10 + public var spacing: CGFloat = 6 /// Padding of intro(text/description). public var padding: UIEdgeInsets = .init(top: 5, left: 10, bottom: 5, right: 10) diff --git a/Sources/Marker/Marker+Calculate.swift b/Sources/Marker/Marker+Calculate.swift new file mode 100644 index 0000000..1a37229 --- /dev/null +++ b/Sources/Marker/Marker+Calculate.swift @@ -0,0 +1,79 @@ +// +// Created by i on 2022/11/30. +// + +import UIKit + +// Internal +extension Marker { + + struct Calculate { + typealias MarkerInfo = Marker.Info + + let info: MarkerInfo + + weak var marked: UIView! + + let spacing: CGFloat + + /// describe `marked` frame in self. + let innerFrame: CGRect + + let highlightRangeRect: CGRect + + let isRound: Bool + let markerFrame: CGRect + let withoutDimFrame: CGRect + + let dimmingViewMaskPath: UIBezierPath + + let safetyRangeSize: CGSize + + init?(info: MarkerInfo, onView: Marker) { + self.info = info + + guard let marked = info.marker, + let markedSuperview = marked.superview + else { + assert(false, "`marker` release or not added on view") + return nil + } + + self.spacing = Marker.default.spacing + self.innerFrame = markedSuperview.convert(marked.frame, to: onView) + self.isRound = marked.layer.cornerRadius == innerFrame.height / 2 + self.markerFrame = info.dimFrame == .zero ? .zero : innerFrame.insetBy(dx: -info.enlarge, dy: -info.enlarge) + self.withoutDimFrame = self.innerFrame.insetBy(dx: -info.enlarge, dy: -info.enlarge) + self.highlightRangeRect = self.markerFrame == .zero ? self.innerFrame : self.markerFrame + + // corner path reversing + let cornerRadius: CGFloat + switch info.style { + case .marker: + cornerRadius = self.isRound ? self.markerFrame.height / 2 : marked.layer.cornerRadius + case .square: + cornerRadius = 0 + case .round: + cornerRadius = self.markerFrame.height / 2 + case .radius(let radius): + cornerRadius = radius + } + let cornerReversingPath = UIBezierPath(roundedRect: self.markerFrame, cornerRadius: cornerRadius).reversing() + let dimmingViewMaskPath = UIBezierPath(rect: info.dimFrame) + dimmingViewMaskPath.append(cornerReversingPath) + self.dimmingViewMaskPath = dimmingViewMaskPath + + self.safetyRangeSize = CGSize(width: onView.superview!.frame.width - 20, height: markedSuperview.frame.height) + } + + func calculateContentSize(label: UILabel) -> CGSize { + let padding = Marker.default.padding + let maxWidth = max(10, min(UIScreen.main.bounds.width - 20, info.maxWidth) - padding.left - padding.right) + var contentSize = label.sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) + contentSize.width = ceil(contentSize.width) + contentSize.height = ceil(contentSize.height) + return contentSize + } + } + +} diff --git a/Sources/Marker/Marker+Draw.swift b/Sources/Marker/Marker+Draw.swift new file mode 100644 index 0000000..42f40da --- /dev/null +++ b/Sources/Marker/Marker+Draw.swift @@ -0,0 +1,214 @@ +// +// Created by i on 2022/11/30. +// + +import UIKit + +// Internal used + +// MARK: setup +extension Marker { + + internal func setupTimeoutIfNeeded() { + guard current.timeout > 0 else { + // skip when timeout is 0, bcz 0 means forever + return + } + + let identifier = current.identifier + let timeout = Marker.default.timeoutAfterAnimateDidCompletion ? (current.timeout + animateDuration) : current.timeout + DispatchQueue.main.asyncAfter(wallDeadline: .now() + timeout) { [weak self] in + guard self?.animateMaps[identifier] == false else { + // whether it has been manually skipped + return + } + self?.showNext(triggerByUser: false) + } + } + + internal func setupIntro(calculate: Calculate) { + if let introString = current.intro as? String { + contentLabel.text = introString + } else if let attributedString = current.intro as? NSAttributedString { + contentLabel.attributedText = attributedString + } else { + contentLabel.text = "Can not support this type: \(type(of: current.intro))" + } + contentLabel.frame.size = calculate.calculateContentSize(label: contentLabel) + } + + internal func setupDimmingView(calculate: Calculate) { + dimmingView.frame = bounds + let dimmingPath = calculate.dimmingViewMaskPath + + if maskLayer.superlayer == nil || dimmingViewShouldTransition { + maskLayer.path = dimmingPath.cgPath + maskLayer.backgroundColor = UIColor.black.cgColor + dimmingView.layer.mask = maskLayer + UIView.animate(withDuration: animateDuration) { + self.dimmingView.alpha = 1 + } + } else { + if current.dimFrame == .zero { + UIView.animate(withDuration: animateDuration) { + self.dimmingView.alpha = 0 + } + } else if dimmingView.alpha != 1 { + maskLayer.path = dimmingPath.cgPath + UIView.animate(withDuration: animateDuration) { + self.dimmingView.alpha = 1 + } + } else { + pathAnimate(from: maskLayer, to: dimmingPath) + } + } + } +} + +// First call the above method(setup) and then Call the following method. + +extension Marker { + + internal func calculateGradientRange(calculate: Calculate) -> (gradientRect: CGRect, vAlignment: Info.VAlignment) { + let padding = Marker.default.padding + let contentSize = contentLabel.frame.size + let bumpHeight: CGFloat = calculate.info.isArrowHidden ? 0 : 6 + let highlightRangeRect: CGRect = calculate.highlightRangeRect + + var gradientRect: CGRect = .zero + // MARK: size + gradientRect.size = CGSize( + width: contentSize.width + padding.left + padding.right, + height: contentSize.height + padding.top + padding.bottom + bumpHeight + ) + + // MARK: origin.x + switch calculate.info.hAlignment { + case .center: + // origin.midX = highlightRect.midX + gradientRect.origin.x = highlightRangeRect.midX - (gradientRect.width / 2) + + case .left: + // origin.x = highlightRect.minX + gradientRect.origin.x = highlightRangeRect.minX + + case .right: + // origin.maxX = highlightRect.maxX + gradientRect.origin.x = highlightRangeRect.maxX - gradientRect.width + } + + // MARK: check and fix origin.x + if gradientRect.minX < 10 { // horizontally safe area + gradientRect.origin.x = 10 + } + if gradientRect.maxX > calculate.safetyRangeSize.width { + gradientRect.origin.x = calculate.safetyRangeSize.width - gradientRect.width + } + + // MARK: origin.y + let topOriginY: CGFloat = highlightRangeRect.minY - Marker.default.spacing - gradientRect.height - bumpHeight + let bottomOriginY: CGFloat = highlightRangeRect.maxY + Marker.default.spacing + bumpHeight + + var resultVAlignment: Info.VAlignment = .auto + switch calculate.info.vAlignment { + case .auto: + gradientRect.origin.y = topOriginY + resultVAlignment = .top + + // MARK: check and fix origin.y + let gradientRectAtWindow = self.convert(gradientRect, to: nil) + if gradientRectAtWindow.minY < 120 { + // display on dynamic island or notch + // switch to `.bottom` + gradientRect.origin.y = bottomOriginY + resultVAlignment = .bottom + } + if gradientRectAtWindow.maxY > UIScreen.main.bounds.height - 34 { + // display outside the safe area + // switch to `.top` + gradientRect.origin.y = topOriginY + resultVAlignment = .top + } + + case .top: + // keep at top and not check available + gradientRect.origin.y = topOriginY + resultVAlignment = .top + + case .bottom: + // keep at bottom and not check available + gradientRect.origin.y = bottomOriginY + resultVAlignment = .bottom + } + + return (gradientRect, resultVAlignment) + } + + internal func triangleArrowBezierPath( + calculate: Calculate, + gradientFrame: CGRect, + vAlignment: Info.VAlignment + ) -> (arrowBezierPath: UIBezierPath, labelOrigin: CGPoint) { + let padding = Marker.default.padding + let bumpHeight: CGFloat = calculate.info.isArrowHidden ? 0 : 6 + + var rectY: CGFloat = bumpHeight + var labelOrigin: CGPoint = CGPoint(x: padding.left, y: bumpHeight + padding.top) + + let bezierPath = UIBezierPath() + if !calculate.info.isArrowHidden { + let rect: CGRect = calculate.highlightRangeRect.width > gradientFrame.width ? gradientFrame : calculate.highlightRangeRect + + let width: CGFloat + + switch calculate.info.trianglePosition { + case .auto: + switch calculate.info.hAlignment { + case .center: + width = self.convert(rect, to: contentView).midX + case .left: + width = self.convert(rect, to: contentView).minX + 10 + (rect.width * 0.05) + case .right: + width = self.convert(rect, to: contentView).maxX - 10 - (rect.width * 0.05) + } + + case .left(let offset): + width = self.convert(rect, to: contentView).minX + 10 + offset + + case .center(let offset): + width = self.convert(rect, to: contentView).midX + offset + + case .right(let offset): + width = self.convert(rect, to: contentView).maxX - 10 + offset + } + + if vAlignment == .top { + // triangle on bottom-center + rectY = 0 + labelOrigin.y = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height + + let startPoint = CGPoint(x: width + 6, y: gradientFrame.height - bumpHeight) + bezierPath.move(to: startPoint) + bezierPath.addLine(to: .init(x: startPoint.x - 6, y: gradientFrame.height)) + bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) + } else { + // triangle on top-center + let startPoint = CGPoint(x: width + 6, y: bumpHeight) + bezierPath.move(to: startPoint) + bezierPath.addLine(to: .init(x: startPoint.x - 6, y: 0)) + bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) + } + } + bezierPath.append( + UIBezierPath( + roundedRect: CGRect( + x: 0, y: rectY, + width: gradientLayer.frame.width, + height: gradientLayer.frame.height - bumpHeight + ), + cornerRadius: 6 + ) + ) + return (bezierPath, labelOrigin) + } +} diff --git a/Sources/Marker/Marker+Info+Alignment.swift b/Sources/Marker/Marker+Info+Alignment.swift new file mode 100644 index 0000000..7724785 --- /dev/null +++ b/Sources/Marker/Marker+Info+Alignment.swift @@ -0,0 +1,28 @@ +// +// Created by i on 2022/11/30. +// + +import UIKit + +extension Marker.Info { + + /// Describe `Marker` horizotnally alignment. + public enum HAlignment { + /// `Default` if available. + case center + + case left + case right + } + + /// Describe `Marker` vertically alignment. + public enum VAlignment { + /// `Default`. + case auto + + /// Above the highlighted view. + case top + /// Below the highlighted view. + case bottom + } +} diff --git a/Sources/Marker/Marker+Info+ArrowPosition.swift b/Sources/Marker/Marker+Info+ArrowPosition.swift index f071a6b..5b3af58 100644 --- a/Sources/Marker/Marker+Info+ArrowPosition.swift +++ b/Sources/Marker/Marker+Info+ArrowPosition.swift @@ -1,5 +1,5 @@ // -// Created by bro on 2022/11/29. +// Created by i on 2022/11/29. // import UIKit diff --git a/Sources/Marker/Marker+Info+Color.swift b/Sources/Marker/Marker+Info+Color.swift index f7e09de..8f9bf13 100644 --- a/Sources/Marker/Marker+Info+Color.swift +++ b/Sources/Marker/Marker+Info+Color.swift @@ -1,5 +1,5 @@ // -// Created by bro on 2022/11/29. +// Created by i on 2022/11/29. // import UIKit diff --git a/Sources/Marker/Marker+Info+Options.swift b/Sources/Marker/Marker+Info+Options.swift index 7cad90a..9dcdc5e 100644 --- a/Sources/Marker/Marker+Info+Options.swift +++ b/Sources/Marker/Marker+Info+Options.swift @@ -1,5 +1,5 @@ // -// Created by bro on 2022/11/29. +// Created by i on 2022/11/29. // import UIKit @@ -12,12 +12,13 @@ extension Marker.Info { /// Default is weak guidance: tap anywhere to continue(next). /// /// `强引导`,只有点击高亮范围才会响应下一步操作。 + /// 这里不会出现`事件穿透`现象,需要与`eventPenetration`搭配才有。 case strongGuidance /// Will not repond any tap events. Pass the event to the next hitTestView. /// ⚠️ Need to be used with `strongGuidance`. /// - /// `事件穿透`,需要与 `strongGuidance` 搭配使用,将触摸事件向高亮范围传递下去。 + /// `事件穿透`,需要与 `strongGuidance` 强引导搭配使用,将触摸事件向高亮范围传递下去。 /// 意思就是:如果高亮范围是个 Button,那么就会触发 Button 的点击事件。 case eventPenetration diff --git a/Sources/Marker/Marker+Info+Style.swift b/Sources/Marker/Marker+Info+Style.swift index 1175be4..888b54a 100644 --- a/Sources/Marker/Marker+Info+Style.swift +++ b/Sources/Marker/Marker+Info+Style.swift @@ -21,6 +21,9 @@ extension Marker.Info { case timeout(TimeInterval) case maxWidth(CGFloat) case cornerStyle(CornerStyle) + + case hAlignment(HAlignment) + case vAlignment(VAlignment) } } @@ -57,6 +60,11 @@ extension Marker.Info.Style: Equatable { case (.cornerStyle(_), .cornerStyle(_)): return true + case (.hAlignment(_), .hAlignment(_)): + return true + case (.vAlignment(_), .vAlignment(_)): + return true + default: return false } diff --git a/Sources/Marker/Marker+Info+Text.swift b/Sources/Marker/Marker+Info+Text.swift deleted file mode 100644 index 22b5369..0000000 --- a/Sources/Marker/Marker+Info+Text.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Created by bro on 2022/11/29. -// - -import UIKit - -extension Marker.Info { - - public struct Intro { - - /// String or NSAttributedString - public var text: Any? - - let font: UIFont - let color: UIColor - - public init( - _ text: Any?, - font: UIFont = Marker.default.textFont, - color: UIColor = Marker.default.textColor - ) { - self.text = text - self.font = font - self.color = color - } - } - -} diff --git a/Sources/Marker/Marker+Info.swift b/Sources/Marker/Marker+Info.swift index b55f87e..207d346 100644 --- a/Sources/Marker/Marker+Info.swift +++ b/Sources/Marker/Marker+Info.swift @@ -23,15 +23,18 @@ extension Marker { let timeout: TimeInterval let enlarge: CGFloat + let hAlignment: HAlignment + let vAlignment: VAlignment + let isArrowHidden: Bool let isStrongGuidance: Bool let isEventPenetration: Bool let isDecoration: Bool - let completion: CompletionBlock? + var completion: CompletionBlock? var identifier: String { - "\(marker?.description ?? "")-\(String(describing: intro))-\(dimFrame)-\(timeout)-\(style)" + "\(marker?.frame ?? .zero)-\(String(describing: intro))-\(dimFrame)-\(timeout)" } public init(marker: UIView?, @@ -120,6 +123,22 @@ extension Marker { self.style = Marker.default.style } + if let hAlignmentStyle = styles.first(where: { $0 == .hAlignment(.center) }), + case .hAlignment(let alignment) = hAlignmentStyle + { + self.hAlignment = alignment + } else { + self.hAlignment = .center + } + + if let vAlignmentStyle = styles.first(where: { $0 == .vAlignment(.auto) }), + case .vAlignment(let alignment) = vAlignmentStyle + { + self.vAlignment = alignment + } else { + self.vAlignment = .auto + } + self.isDecoration = options.contains(.decoration) self.isStrongGuidance = options.contains(.strongGuidance) self.isEventPenetration = options.contains(.eventPenetration) diff --git a/Sources/Marker/Marker.swift b/Sources/Marker/Marker.swift index d41ed08..82cf5ab 100644 --- a/Sources/Marker/Marker.swift +++ b/Sources/Marker/Marker.swift @@ -1,7 +1,4 @@ // -// Marker.swift -// vietnam -// // Created by iWw on 2021/1/15. // @@ -30,6 +27,7 @@ public class Marker: UIView { var nexts: [Info] = [] var animateMaps: [String: Bool] = [:] var completion: CompletionBlock? + var lastVAlignment: Info.VAlignment = .auto // Views let dimmingView = UIView() @@ -114,129 +112,24 @@ public class Marker: UIView { // MARK: Layout func layout(triggerByUser: Bool) { - guard let markView = current.marker, let markSuperView = current.marker?.superview else { + guard let calculate = Calculate(info: current, onView: self) else { showNext(triggerByUser: triggerByUser) return } - if current.timeout > 0 { // enable timeout if set timeout - let identifier = current.identifier - let timeout = Marker.default.timeoutAfterAnimateDidCompletion ? (Double(current.timeout) + animateDuration) : Double(current.timeout) - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in - // whether it has been manually skipped - guard self?.animateMaps[identifier] == false else { return } - self?.showNext(triggerByUser: false) - } - } - - dimmingView.frame = bounds - - let spacing = Self.default.spacing - let innerFrame = markSuperView.convert(markView.frame, to: self) - let isRound = markView.layer.cornerRadius == innerFrame.height / 2 - let markerFrame = current.dimFrame == .zero ? .zero : innerFrame.insetBy(dx: -current.enlarge, dy: -current.enlarge) - let withoutDimframe = innerFrame.insetBy(dx: -current.enlarge, dy: -current.enlarge) - - // set cornerRadiu - let cornerRadius: CGFloat - switch current.style { - case .marker: - cornerRadius = isRound ? markerFrame.height / 2 : markView.layer.cornerRadius - case .square: - cornerRadius = 0 - case .round: - cornerRadius = markerFrame.height / 2 - case .radius(let radius): - cornerRadius = radius - } - - let markerPath = UIBezierPath(roundedRect: markerFrame, cornerRadius: cornerRadius).reversing() - - // set dimming path - let dimmingPath = UIBezierPath(roundedRect: current.dimFrame, cornerRadius: 0) - dimmingPath.append(markerPath) - - if maskLayer.superlayer == nil || dimmingViewShouldTransition { - maskLayer.path = dimmingPath.cgPath - maskLayer.backgroundColor = UIColor.black.cgColor - dimmingView.layer.mask = maskLayer - UIView.animate(withDuration: animateDuration) { - self.dimmingView.alpha = 1 - } - } else { - if current.dimFrame == .zero { - UIView.animate(withDuration: animateDuration) { - self.dimmingView.alpha = 0 - } - } else if dimmingView.alpha != 1 { - maskLayer.path = dimmingPath.cgPath - UIView.animate(withDuration: animateDuration) { - self.dimmingView.alpha = 1 - } - } else { - pathAnimate(from: maskLayer, to: dimmingPath) - } - } - - if let introString = current.intro as? String { - contentLabel.text = introString - } else if let attributedString = current.intro as? NSAttributedString { - contentLabel.attributedText = attributedString - } else { - contentLabel.text = "Can not support this type: \(type(of: current.intro))" - } - - // reload contentLabel size - let padding = Self.default.padding - let maxWidth = min(UIScreen.main.bounds.width - 20, - current.maxWidth) - let contentSize = contentLabel.sizeThatFits(.init(width: maxWidth - padding.left - padding.right, height: .greatestFiniteMagnitude)) - contentLabel.frame.size = contentSize - - // calculator gradient frame - let bumpHeight: CGFloat = current.isArrowHidden ? 0 : 6 - // 如果视图在中心线右边,则三角形也在右边, 否则在左边 - let isRight = innerFrame.minX >= (frame.width / 2) - - var gradientFrame: CGRect = .zero - gradientFrame.size = .init(width: contentSize.width + padding.left + padding.right, - height: contentSize.height + padding.top + padding.bottom + bumpHeight) - - // 三角形起点偏移量 - var bumpOffsetX: CGFloat = 0 + self.setupTimeoutIfNeeded() + self.setupDimmingView(calculate: calculate) - if isRight { - // right - gradientFrame.origin.x = innerFrame.maxX - gradientFrame.width - if gradientFrame.origin.x < 10 { - bumpOffsetX = -gradientFrame.origin.x + 10 - gradientFrame.origin.x = 10 - } - } else { - // left - gradientFrame.origin.x = innerFrame.minX - if gradientFrame.maxX > UIScreen.main.bounds.width - 10 { // 右边超出右边 - bumpOffsetX = gradientFrame.minX - (UIScreen.main.bounds.width - gradientFrame.width - 10) - gradientFrame.origin.x = UIScreen.main.bounds.width - gradientFrame.width - 10 - } else if gradientFrame.minX < 10 { - gradientFrame.origin.x = 10 - } - } - - let isBottom = innerFrame.maxY >= frame.height / 2 - if isBottom { - // bottom - gradientFrame.origin.y = innerFrame.minY - gradientFrame.height - spacing - current.enlarge - } else { - // top - gradientFrame.origin.y = innerFrame.maxY + spacing + current.enlarge - } - gradientLayer.bounds = .init(x: 0, y: 0, width: gradientFrame.width, height: gradientFrame.height) + self.setupIntro(calculate: calculate) + let (gradientFrame, vAlignment) = self.calculateGradientRange(calculate: calculate) + self.gradientLayer.bounds = CGRect(origin: .zero, size: gradientFrame.size) if contentView.frame == .zero { contentView.alpha = 0 contentView.frame = gradientFrame - contentView.transform = .init(translationX: 0, y: isBottom ? -30 : 30) + contentView.transform = CGAffineTransform(translationX: 0, y: vAlignment == .top ? 20 : -20) + .concatenating(CGAffineTransform(scaleX: 0.95, y: 0.95)) + UIView.animate(withDuration: animateDuration) { self.contentView.alpha = 1 self.contentView.transform = .identity @@ -245,115 +138,20 @@ public class Marker: UIView { contentView.frame = gradientFrame } - // make mask - var rectY: CGFloat = bumpHeight - - var labelOriginY: CGFloat = bumpHeight + padding.top - let bezierPath = UIBezierPath() - if !current.isArrowHidden { - // draw triangle - switch current.trianglePosition { - case .auto: - if isRight, !isBottom { // top, right - //labelOriginY - let startPoint = CGPoint(x: gradientFrame.width - 15 - bumpOffsetX, y: bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: 0)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } else if isRight, isBottom { // bottom, right - rectY = 0 - labelOriginY = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height - - let startPoint = CGPoint(x: gradientFrame.width - 15 - bumpOffsetX, y: gradientFrame.height - bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: gradientFrame.height)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } else if !isRight, isBottom { // bottom, left - rectY = 0 - labelOriginY = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height - - let startPoint = CGPoint(x: 15 + bumpOffsetX, y: gradientFrame.height - bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x + 6, y: gradientFrame.height)) - bezierPath.addLine(to: .init(x: startPoint.x + 12, y: startPoint.y)) - - } else if !isRight, !isBottom { // top, left - let startPoint = CGPoint(x: 15 + bumpOffsetX, y: bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x + 6, y: 0)) - bezierPath.addLine(to: .init(x: startPoint.x + 12, y: startPoint.y)) - } - - case .left(let offset): - let minX = min(gradientFrame.minX, withoutDimframe.minX) - if isBottom { - // triangle on bottom-left - rectY = 0 - labelOriginY = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height - - let startPoint = CGPoint(x: minX + 10 + offset, y: gradientFrame.height - bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x + 6, y: gradientFrame.height)) - bezierPath.addLine(to: .init(x: startPoint.x + 12, y: startPoint.y)) - } else { - // triangle on top-left - let startPoint = CGPoint(x: minX + 10 + offset, y: bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x + 6, y: 0)) - bezierPath.addLine(to: .init(x: startPoint.x + 12, y: startPoint.y)) - } - break - case .center(let offset): - //let width = min(gradientFrame.width, withoutDimframe.width) - let width = self.convert(withoutDimframe, to: contentView).midX - if isBottom { - // triangle on bottom-center - rectY = 0 - labelOriginY = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height - - let startPoint = CGPoint(x: width + 6 + offset, y: gradientFrame.height - bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: gradientFrame.height)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } else { - // triangle on top-center - let startPoint = CGPoint(x: width + 6 + offset, y: bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: 0)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } - - case .right(let offset): - let width = min(gradientFrame.width, withoutDimframe.width) - if isBottom { - // triangle on bottom-right - rectY = 0 - labelOriginY = gradientFrame.height - bumpHeight - padding.bottom - contentLabel.frame.height - - let startPoint = CGPoint(x: width - 10 + offset, y: gradientFrame.height - bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: gradientFrame.height)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } else { - // triangle on top-right - let startPoint = CGPoint(x: width - 10 + offset, y: bumpHeight) - bezierPath.move(to: startPoint) - bezierPath.addLine(to: .init(x: startPoint.x - 6, y: 0)) - bezierPath.addLine(to: .init(x: startPoint.x - 12, y: startPoint.y)) - } - } - } - bezierPath.append(.init(roundedRect: .init(x: 0, y: rectY, width: gradientLayer.frame.width, height: gradientLayer.frame.height - bumpHeight), cornerRadius: 6)) - - contentLabel.frame.origin.x = padding.left - contentLabel.frame.origin.y = labelOriginY + let (arrowBezierPath, labelOrigin) = self.triangleArrowBezierPath( + calculate: calculate, + gradientFrame: gradientFrame, + vAlignment: vAlignment + ) + self.lastVAlignment = vAlignment + contentLabel.frame.origin = labelOrigin if gradientLayer.mask == nil { - bumpLayer.path = bezierPath.cgPath + bumpLayer.path = arrowBezierPath.cgPath bumpLayer.backgroundColor = UIColor.black.cgColor gradientLayer.mask = bumpLayer } else { - pathAnimate(from: bumpLayer, to: bezierPath) + pathAnimate(from: bumpLayer, to: arrowBezierPath) } } @@ -382,6 +180,7 @@ public class Marker: UIView { } current.completion?(self, triggerByUser) + current.completion = nil // release guard let next = nexts.first else { // dimiss dismiss(triggerByUser: triggerByUser) @@ -439,10 +238,11 @@ public extension Marker { self.dimmingView.alpha = 0 self.contentView.alpha = 0 - self.contentView.transform = CGAffineTransform(translationX: 0, y: 50).concatenating(CGAffineTransform(scaleX: 1.1, y: 1.1)) - } completion: { [weak self] (_) in - guard let self = self else { return } + self.contentView.transform = CGAffineTransform(translationX: 0, y: self.lastVAlignment == .bottom ? -20 : 20) + .concatenating(CGAffineTransform(scaleX: 0.95, y: 0.95)) + } completion: { (_) in self.completion?(self, triggerByUser) + self.completion = nil self.removeFromSuperview() } }