はじめに
下記のような指定パーツの説明を表示するアプリチュートリアルを実装する方法について考えてみました。
ライブラリもあるみたいなんでカスタムしないならライブラリ探す方がいいと思います。
bannzai/Gecco
方法として思いついたのは下記2つ。
- 半透明の VC を表示してその上に指定の View のスクショを表示した
UIImageView
を置く方法 - 半透明の VC を表示して指定の View の部位だけくり抜いた layer を表示する方法
Viewのスクショを撮る方式
実装方法は下記。それっぽい感じにはなった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
final class ViewController: UIViewController { @IBOutlet private weak var hogeButton: UIButton! override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = .init(title: "Tutorial", style: .done, target: self, action: #selector(showTutorial)) } @objc private func showTutorial() { let vc = TutorialViewController() vc.showTutorial(from: self.navigationController!, target: hogeButton) } } final class TutorialViewController: UIViewController { private var image: UIImage? private var targetFrame: CGRect = .zero private var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5) imageView = UIImageView(frame: targetFrame) imageView.contentMode = .scaleAspectFit imageView.image = image view.addSubview(imageView) let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) view.addGestureRecognizer(tap) } @objc private func didTap() { dismiss(animated: false) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let viewController = PopViewController() viewController.modalPresentationStyle = .popover viewController.preferredContentSize = .init(width: 200, height: 50) viewController.text = "ほげほげするためのボタンです" let presentationController = viewController.popoverPresentationController presentationController?.delegate = self presentationController?.permittedArrowDirections = .up presentationController?.sourceView = imageView presentationController?.sourceRect = imageView.bounds present(viewController, animated: false) } func showTutorial(from parent: UIViewController, target: UIView) { modalPresentationStyle = .overCurrentContext targetFrame = target.convert(target.bounds, to: nil) image = target.screenCapture() parent.present(self, animated: false) } } // ポップで表示用 extension TutorialViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } } // ポップで説明出す用 final class PopViewController: UIViewController { var text: String? private var label: UILabel? override func viewDidLoad() { super.viewDidLoad() label = UILabel(frame: .zero) label?.textAlignment = .center view.addSubview(label!) label?.text = text label?.font = .systemFont(ofSize: 14) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() label?.frame = view.bounds } } // Viewのスクショ撮る用 extension UIView { func screenCapture() -> UIImage? { UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0) defer { UIGraphicsEndImageContext() } guard let context = UIGraphicsGetCurrentContext() else { return nil } layer.render(in: context) let image = UIGraphicsGetImageFromCurrentImageContext() return image } } |
指定部位だけくり抜く
スクショ撮るのが無駄な感じがしたので別案。
実装方法は下記。下記以外の部分はスクショのやつと同じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
final class TutorialViewController: UIViewController { private lazy var backgroundLayer: CALayer = { let backgroundLayer = CALayer() backgroundLayer.bounds = view.bounds backgroundLayer.position = view.center backgroundLayer.backgroundColor = UIColor.black.cgColor backgroundLayer.opacity = 0.5 let maskLayer = CAShapeLayer() maskLayer.bounds = backgroundLayer.bounds let path = UIBezierPath(roundedRect: targetFrame, cornerRadius: 0) path.append(UIBezierPath(rect: maskLayer.bounds)) maskLayer.fillColor = UIColor.black.cgColor maskLayer.path = path.cgPath maskLayer.position = view.center maskLayer.fillRule = CAShapeLayerFillRule.evenOdd backgroundLayer.mask = maskLayer return backgroundLayer }() private var targetFrame: CGRect = .zero override func viewDidLoad() { super.viewDidLoad() view.layer.addSublayer(backgroundLayer) let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) view.addGestureRecognizer(tap) } @objc private func didTap() { dismiss(animated: false) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let viewController = PopViewController() viewController.modalPresentationStyle = .popover viewController.preferredContentSize = .init(width: 200, height: 50) viewController.text = "ほげほげするためのボタンです" let presentationController = viewController.popoverPresentationController presentationController?.delegate = self presentationController?.permittedArrowDirections = .up presentationController?.sourceView = view presentationController?.sourceRect = targetFrame present(viewController, animated: false) } func showTutorial(from parent: UIViewController, target: UIView) { modalPresentationStyle = .overCurrentContext targetFrame = target.convert(target.bounds, to: nil) parent.present(self, animated: false) } } |
くり抜き方は下記記事を参考にしました。
一部をくり抜いたオーバーレイを表示する方法
おわりに
文字での説明部分はめんどくさかったので Popover にしましたがここもカスタム View にすれば柔軟にカスタマイズしていけるはず!
ゴロゴリにカスタマイズしたチュートリアル表示の要望があったので頭をひねってみました。どっちのパターンも横向きありの場合は崩れそう。。。
コメント