はじめに
コーチマークとUIViewのアニメーションを組み合わせて、スワイプで削除する操作を表現してみました。
コーチマークについて
コーチマークとは・・・
実際の画面の上にオーバーレイや吹き出しを出して
画面上のボタンなどの使い方を案内してくれるやつです。
アプリで初めて開く画面とかでときどき見かけますね('・ω・`)
スワイプ削除
iOSのTableViewでは、セルをスワイプで削除する機能が簡単に実装できますし、
アプリユーザにとっても慣れている操作の一つではないかと思います(^ω^)
ただ、削除ボタンみたいに目に見えてわかる機能ではないので、
ユーザ全員が当たり前のようにスワイプで削除できると認識してくれるとは限りません。。。
そこで、初めて画面を開いた時にコーチマークを利用して、
スワイプで削除できるよっていうことを案内してみたいと思います!
実装
今回は、Instructionsを使用してコーチマークを実装しました!
Instructionsの実装方法は、こちらの記事を参考にさせていただきましたm(_ _)m
→ swift 簡単でおしゃれなチュートリアルライブラリ,Instructions
今回は、Carthageでライブラリを導入しました。
github "ephread/Instructions" ~> 2.0
まずは、コーチマークを表示するところ
実装は、Instructionsのリポジトリにもサンプルがあったので、そちらを参考にしながら進めました。
CoachMarksControllerのdataSourceを実装して、
coachMarksController.start(in: )
を呼んであげるだけでコーチマークを表示できました(⌒ω⌒)
今回は、コーチマークのターゲットがTableViewCellなので、事前にIndexPathを指定して、
spotlightTargetViewにcellを保持するように実装してみました。
// コーチマークの初期設定
private func setupCoachMarksController() {
self.coachMarksController.dataSource = self
// オーバーレイをタップでもコーチマークを閉じれるようにする
self.coachMarksController.overlay.isUserInteractionEnabled = true
// コーチマークの背景色を設定
self.coachMarksController.overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}
/// コーチマークを表示する
private func startCoachMarks() {
let indexPath = IndexPath(row: 0, section: 0)
// [section, row] = [0, 0]のCellがあれば、コーチマークを表示する
if tableView.numberOfRows(inSection: indexPath.section) > indexPath.row, let cell = tableView.cellForRow(at: indexPath) {
self.spotlightTargetView = cell
self.coachMarksController.start(in: .currentWindow(of: self))
}
}
CoachMarksControllerのdataSourceの実装はこんな感じでシンプル
extension ViewController: CoachMarksControllerDataSource {
func numberOfCoachMarks(for coachMarksController: CoachMarksController) -> Int {
return 1
}
func coachMarksController(_ coachMarksController: CoachMarksController, coachMarkAt index: Int) -> CoachMark {
// 吹き出しを表示する対象のビューを指定する
return coachMarksController.helper.makeCoachMark(for: spotlightTargetView)
}
func coachMarksController(_ coachMarksController: CoachMarksController,
coachMarkViewsAt index: Int,
madeFrom coachMark: CoachMark) -> (bodyView: (UIView & CoachMarkBodyView), arrowView: (UIView & CoachMarkArrowView)?) {
let coachViews = coachMarksController.helper.makeDefaultCoachViews(withArrow: true,
withNextText: false,
arrowOrientation: coachMark.arrowOrientation)
coachViews.bodyView.hintLabel.text = "スワイプでお気に入りから削除できます"
return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)
}
}
この状態だと、まだ分かりづらいかなーと思うので、アニメーションを加えていきます。
スワイプで削除風のアニメーションを実装
スワイプで削除するようなアニメーションを実装します。
アニメーションは、Stack Overflowの投稿をパクらせて参考にさせていただきましたm(_ _)m
UITableView invoke swipe actions programmatically
参考情報そのままだと少し使いづらいところがあったので、
アニメーション完了後の処理をクロージャで実装できるようにしています。
動作
アニメーションを加えたら、いい感じになりました(^-^)v
(Gifの問題で、「削除」ラベルの色が実際と少し違っています。)
完成したコード
以下、アニメーション完了後からオーバーレイのタップを許可するなどの微調整をした最終的なソースコードです。
(コーチマーク表示済みかどうかの判定は、UserDefaultsなどで実現できると思うので、省いています。)
import UIKit
import Instructions
final class ViewController: UIViewController {
private let coachMarksController = CoachMarksController()
// コーチマークでスポットライトが当たるView
private var spotlightTargetView: UIView!
private var spotlightTargetIndexPath: IndexPath?
private var items = ["apple", "banana", "cherry"]
@IBOutlet private weak var tableView: UITableView! {
didSet {
tableView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.setupCoachMarksController()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = "お気に入り一覧"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.startCoachMarks()
}
private func setupCoachMarksController() {
self.coachMarksController.dataSource = self
self.coachMarksController.delegate = self
// コーチマークの背景色を設定
self.coachMarksController.overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}
/// コーチマークを表示する
private func startCoachMarks() {
let indexPath = IndexPath(row: 0, section: 0)
// [section, row] = [0, 0]のCellがあれば、コーチマークを表示する
if tableView.numberOfRows(inSection: indexPath.section) > indexPath.row, let cell = tableView.cellForRow(at: indexPath) {
self.spotlightTargetView = cell
self.spotlightTargetIndexPath = indexPath
self.coachMarksController.start(in: .currentWindow(of: self))
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else {
fatalError("Cell is nil.")
}
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
extension ViewController: CoachMarksControllerDataSource {
func numberOfCoachMarks(for coachMarksController: CoachMarksController) -> Int {
return 1
}
func coachMarksController(_ coachMarksController: CoachMarksController, coachMarkAt index: Int) -> CoachMark {
// 吹き出しを表示する対象のビューを指定する
return coachMarksController.helper.makeCoachMark(for: spotlightTargetView)
}
func coachMarksController(_ coachMarksController: CoachMarksController,
coachMarkViewsAt index: Int,
madeFrom coachMark: CoachMark) -> (bodyView: (UIView & CoachMarkBodyView), arrowView: (UIView & CoachMarkArrowView)?) {
let coachViews = coachMarksController.helper.makeDefaultCoachViews(withArrow: true,
withNextText: false,
arrowOrientation: coachMark.arrowOrientation)
coachViews.bodyView.hintLabel.text = "スワイプでお気に入りから削除できます"
return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)
}
}
extension ViewController: CoachMarksControllerDelegate {
func coachMarksController(_ coachMarksController: CoachMarksController,
didShow coachMark: CoachMark,
afterChanging change: ConfigurationChange,
at index: Int) {
guard let targetIndexPath = self.spotlightTargetIndexPath else { return }
UIView.animateRevealHideActionForRow(tableView: self.tableView, indexPath: targetIndexPath) { [weak self] in
// アニメーション完了後、オーバーレイをタップでもコーチマークを閉じれるようにする
self?.coachMarksController.overlay.isUserInteractionEnabled = true
}
}
}
import UIKit
extension UIView {
/// TableViewのスワイプアクションの擬似的なアニメーション
class func animateRevealHideActionForRow(tableView: UITableView, indexPath: IndexPath, completion: (() -> Void)? = nil) {
guard let cell = tableView.cellForRow(at: indexPath) else { return }
let swipeLabelWidth = UIScreen.main.bounds.width / 2
let swipeLabelFrame = CGRect(x: cell.bounds.size.width, y: 0, width: swipeLabelWidth, height: cell.bounds.size.height)
var swipeLabel: UILabel? = .init(frame: swipeLabelFrame)
swipeLabel?.text = " 削除"
swipeLabel?.backgroundColor = .red
swipeLabel?.textColor = .white
cell.addSubview(swipeLabel!)
UIView.animate(withDuration: 0.5, animations: {
cell.frame = .init(x: cell.frame.origin.x - swipeLabelWidth / 2,
y: cell.frame.origin.y,
width: cell.bounds.size.width + swipeLabelWidth / 2,
height: cell.bounds.size.height)
}) { finished in
UIView.animate(withDuration: 0.5, animations: {
cell.frame = .init(x: cell.frame.origin.x + swipeLabelWidth / 2,
y: cell.frame.origin.y,
width: cell.bounds.size.width - swipeLabelWidth / 2,
height: cell.bounds.size.height)
}, completion: { finished in
swipeLabel?.removeFromSuperview()
swipeLabel = nil
completion?()
})
}
}
}
さいごに
コーチマークでテキストを添えるだけでも親切だけど、動きをつけるとより親切かなと思いました!
(やり過ぎると、しつこいですけどね・・・)