1.はじめに
自作の買い物リストアプリにスワイプ式のオンボーディング機能を実装したので、この記事にまとめます。
この記事では、以下のようなUIをUIKit(Swift)で実現する手順を紹介します。
- 横スワイプでページ切り替え
- 最終ページに「はじめる」ボタン
- 「スキップ」ボタン付き
- 初回起動時のみ表示
UIKitでオンボーディングを実装したい方の参考になれば嬉しいです。
2.完成イメージ
完成イメージは以下です。スキップボタンを押すか、最後の画面ではじめるボタンを押すとアプリのメイン画面が表示されます。
3.オンボーディングとは
実装の前に、簡単にオンボーディングについて調べました。
オンボーディング(Onboarding)とは、アプリやサービスを初めて使うユーザーに対して、使い方や魅力をわかりやすく案内するための導入フローのこと
オンボーディングの目的
- アプリの使い方を説明する
- 初期設定を手助けする
- ユーザーの離脱を防ぐ
- アプリの価値を伝える
よくあるオンボーディングの形式
種類 | 内容 |
---|---|
スワイプ形式(UIPageViewControllerなど) | ページを横にスワイプして読む |
モーダル形式 | 最初に表示される説明画面(スキップ可能) |
ツアー形式 | 実際の画面に説明バルーンが出る(コーチマーク) |
スライダー+「はじめる」ボタン | 最後のページで本編に遷移できる |
4.実装ステップ
4.1 OnboardingContentViewControllerの作成
3ページ分のコンテンツを表示するUIViewControllerです。
OnboardingContentViewController.swift
import UIKit
class OnboardingContentViewController: UIViewController {
// 各ページで表示する情報
private let imageName: String
private let titleText: String
private let subtitleText: String
private let isLastPage: Bool // 最後のページかどうかを判定
// 初期化(画像名・タイトル・サブタイトル・最終ページ判定を渡す)
init(imageName: String, title: String, subtitle: String, isLastPage: Bool = false) {
self.imageName = imageName
self.titleText = title
self.subtitleText = subtitle
self.isLastPage = isLastPage
super.init(nibName: nil, bundle: nil)
}
// ストーリーボード未使用なので未対応
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupLayout()
}
// UIパーツをレイアウト
private func setupLayout() {
// 画像表示
let imageView = UIImageView(image: UIImage(named: imageName))
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
// タイトルラベル
let titleLabel = UILabel()
titleLabel.text = titleText
titleLabel.font = .boldSystemFont(ofSize: 24)
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 0
// サブタイトルラベル
let subtitleLabel = UILabel()
subtitleLabel.text = subtitleText
subtitleLabel.font = .systemFont(ofSize: 16)
subtitleLabel.textAlignment = .center
subtitleLabel.textColor = .gray
subtitleLabel.numberOfLines = 0
// スタックで縦に並べる
let stack = UIStackView(arrangedSubviews: [imageView, titleLabel, subtitleLabel])
stack.axis = .vertical
stack.spacing = 20
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
// 最後のページのみ「はじめる」ボタンを追加
if isLastPage {
let startButton = UIButton(type: .system)
startButton.setTitle("はじめる", for: .normal)
startButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .medium)
startButton.addTarget(self, action: #selector(dismissOnboarding), for: .touchUpInside)
stack.addArrangedSubview(startButton)
}
// レイアウト制約
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
imageView.heightAnchor.constraint(equalToConstant: 200)
])
}
// 「はじめる」ボタン or スキップで閉じる
@objc private func dismissOnboarding() {
dismiss(animated: true)
}
}
4.2 OnboardingViewController(UIPageViewController)の作成
ページを切り替えるメインのクラスです。
OnboardingViewController.swift
import UIKit
// UIPageViewController を使ってオンボーディングをスワイプで表示
class OnboardingViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
// 表示する3ページ分のコンテンツ
private lazy var pages: [UIViewController] = [
OnboardingContentViewController(
imageName: "onboarding1",
title: "🛒 アイテムを追加しよう",
subtitle: "買い物リストにアイテムを追加できます。"
),
OnboardingContentViewController(
imageName: "onboarding2",
title: "✅ チェックで管理",
subtitle: "チェックすると購入済みに移動します。"
),
OnboardingContentViewController(
imageName: "onboarding3",
title: "📅 日付・カテゴリで並び替え",
subtitle: "予定に合わせて整理整頓しましょう。",
isLastPage: true
)
]
// ページインジケーターとスキップボタン
private let pageControl = UIPageControl()
private let skipButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
// スワイプ操作のための設定
dataSource = self
delegate = self
view.backgroundColor = .systemBackground
// 最初のページを表示
if let first = pages.first {
setViewControllers([first], direction: .forward, animated: true)
}
setupPageControl()
setupSkipButton()
}
// ページインジケータの設定
private func setupPageControl() {
pageControl.numberOfPages = pages.count
pageControl.currentPage = 0
pageControl.currentPageIndicatorTintColor = .systemBlue
pageControl.pageIndicatorTintColor = .lightGray
pageControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
// スキップボタンの設定
private func setupSkipButton() {
skipButton.setTitle("スキップ", for: .normal)
skipButton.addTarget(self, action: #selector(skipTapped), for: .touchUpInside)
skipButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(skipButton)
NSLayoutConstraint.activate([
skipButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
skipButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
// スキップボタン押下時に閉じる
@objc private func skipTapped() {
dismiss(animated: true)
}
// 前のページへ戻る処理
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController), index > 0 else { return nil }
return pages[index - 1]
}
// 次のページへ進む処理
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController), index < pages.count - 1 else { return nil }
return pages[index + 1]
}
// ページが切り替わったら pageControl を更新
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
guard let visibleVC = viewControllers?.first,
let index = pages.firstIndex(of: visibleVC) else { return }
pageControl.currentPage = index
}
}
4.3 TableViewControllerに追加した起動判定(初回起動時のみ表示)
UserDefaultsに起動済みフラグを追加
TableViewController.swift
// UserDefaults に初回起動フラグを追加する拡張
extension UserDefaults {
// hasLaunchedBefore が true なら2回目以降の起動
var hasLaunchedBefore: Bool {
get { bool(forKey: "hasLaunchedBefore") }
set { set(newValue, forKey: "hasLaunchedBefore") }
}
}
TableViewControllerで起動判定して表示
TableViewController.swift
// 画面が表示されたときに初回起動かどうかを判定
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 初回起動ならオンボーディングを表示
if !UserDefaults.standard.hasLaunchedBefore {
UserDefaults.standard.hasLaunchedBefore = true // 起動済みに設定
// オンボーディング画面をモーダル表示
let onboardingVC = OnboardingViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal
)
onboardingVC.modalPresentationStyle = .fullScreen
present(onboardingVC, animated: true)
}
}
おわりに
UIPageViewControllerを使ったシンプルなオンボーディングを実装することで、簡単に新規ユーザーに機能説明をすることができます。
現在は最終ページで「はじめる」ボタンを追加していますが、これに加えて「ログイン」や「プランを選択」などの操作を続けられるUIに拡張していくのも良いかもしれません。
この記事が、UIKitでオンボーディングを実装しようとしている方の参考になれば幸いです。