LoginSignup
3

More than 5 years have passed since last update.

【Swift】UICollectionViewを使ってチュートリアル画面っぽいものを作成する(コードで作成する場合)

Last updated at Posted at 2018-02-11

Interface Builderを使った場合

経緯

アプリ初回起動時によく見る操作説明画面など
画像とちょっとした説明文を使った複数の連続した画面を作成する際、
CollectionViewを使うと楽に実装できるのかと思い
サンプルを作成してみました。

Cellのサイズを画面サイズに合わせることで
ページ遷移をしているように見せます。

1 2 3 4
Simulator Screen Shot - iPhone X - 2018-02-12 at 08.04.43.png Simulator Screen Shot - iPhone X - 2018-02-12 at 08.04.45.png Simulator Screen Shot - iPhone X - 2018-02-12 at 08.04.48.png Simulator Screen Shot - iPhone X - 2018-02-12 at 08.04.50.png

作成したサンプル
https://github.com/stzn/CollectionViewTutorial

実装

Tutorial.swift
struct Tutorial {
    let imageName: String
    let summary: String
    let detail: String
}
CollectionViewController.swift

private let reuseIdentifier = "Cell"

final class CollectionViewController: UICollectionViewController {

    let tutorials = [
        Tutorial(imageName: "1", summary: "レッスン1", detail: "レッスン1の説明です。\n読み終わったら次へ進んでください。"),
        Tutorial(imageName: "2", summary: "レッスン2", detail: "レッスン2の説明です。\n読み終わったら次へ進んでください。"),
        Tutorial(imageName: "3", summary: "レッスン3", detail: "レッスン3の説明です。\n読み終わったら次へ進んでください。"),
        Tutorial(imageName: "4", summary: "レッスン4", detail: "レッスン4の説明です。\n以上でレッスンは終了です。")
    ]

    private let previousButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("PREV", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
        button.setTitleColor(.gray, for: .normal)
        button.addTarget(self, action: #selector(handlePrev), for: .touchUpInside)
        return button
    }()

    @objc private func handlePrev() {
        let prev = max(0, pageControl.currentPage - 1)
        let index = IndexPath(row: prev, section: 0)
        pageControl.currentPage = prev
        collectionView?.scrollToItem(at: index, at: .centeredHorizontally, animated: true)
    }

    private let nextButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("NEXT", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
        button.setTitleColor(.gray, for: .normal)
        button.addTarget(self, action: #selector(handleNext), for: .touchUpInside)
        return button
    }()

    @objc private func handleNext() {
        let next = min(pageControl.currentPage + 1, tutorials.count - 1)
        let index = IndexPath(row: next, section: 0)
        pageControl.currentPage = next
        collectionView?.scrollToItem(at: index, at: .centeredHorizontally, animated: true)
    }

    lazy var pageControl: UIPageControl = {
        let pc = UIPageControl()
        pc.currentPage = 0
        pc.numberOfPages = tutorials.count
        pc.currentPageIndicatorTintColor = .red
        pc.pageIndicatorTintColor = .gray
        return pc
    }()


    private func setupBottomControls() {

        let stackView = UIStackView(arrangedSubviews: [previousButton, pageControl, nextButton])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.distribution = .fillEqually

        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            stackView.heightAnchor.constraint(equalToConstant: 50)
            ])
    }

    // スクロールの位置とpageControlのページを合わせる
    override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let x = targetContentOffset.pointee.x
        pageControl.currentPage = Int(x / view.frame.width)
    }


    override func viewDidLoad() {
        super.viewDidLoad()

        setupBottomControls()

        collectionView?.backgroundColor = .white
        self.collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)

        collectionView?.isPagingEnabled = true
    }
}

extension CollectionViewController {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return tutorials.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CollectionViewCell

        let item = tutorials[indexPath.item]
        cell.tutorial = item
        return cell
    }

}


extension CollectionViewController: UICollectionViewDelegateFlowLayout {

    // セルを画面のサイズに合わせる
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width, height: view.frame.height)
    }
}

extension CollectionViewController {

    // 画面の回転に合わせてcollectionViewとpageControlの位置を合わせる
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        coordinator.animate(alongsideTransition: { (_) in

            self.collectionViewLayout.invalidateLayout()
            if self.pageControl.currentPage == 0 {
                self.collectionView?.contentOffset = .zero
            } else {
                let indexPath = IndexPath(item: self.pageControl.currentPage, section: 0)
                self.collectionView?.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            }
        })
    }
}
CollectionViewCell.swift

final class CollectionViewCell: UICollectionViewCell {

    var tutorial: Tutorial? = nil {

        didSet {

            guard let tutorial = tutorial else {
                return
            }
            pageImageView.image = UIImage(named: tutorial.imageName)

            let attributedText = NSMutableAttributedString(
                                    string: tutorial.summary,
                                    attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18)])

            attributedText
                .append(NSAttributedString(
                                    string: "\n\n\n\(tutorial.detail)",
                                    attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 13),
                                                 NSAttributedStringKey.foregroundColor: UIColor.gray]))
            descriptionTextView.attributedText = attributedText
            descriptionTextView.textAlignment = .center
        }
    }

    let pageImageView: UIImageView = {
        let imageView = UIImageView(image: #imageLiteral(resourceName: "1"))  // 適当です
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()

    let descriptionTextView: UITextView = {
        let textView = UITextView()

        textView.text = "aaaaa" // 適当です
        textView.font = UIFont.systemFont(ofSize: 20)
        textView.textAlignment = .center
        textView.isEditable = false
        textView.isScrollEnabled = false
        textView.translatesAutoresizingMaskIntoConstraints = false
        return textView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayout()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupLayout() {


        let containerView = UIView()
        addSubview(containerView)
        containerView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: topAnchor),
            containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
            containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5)
            ])

        containerView.addSubview(pageImageView)

        NSLayoutConstraint.activate([
            pageImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            pageImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            pageImageView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.5)
            ])


        addSubview(descriptionTextView)
        NSLayoutConstraint.activate([
            descriptionTextView.topAnchor.constraint(equalTo: pageImageView.bottomAnchor),
            descriptionTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
            descriptionTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
            descriptionTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0)
            ])       
    }
}

まとめ

IBを使用した場合に比べコード量は増えますが、
何をしているのかが全てコードに書かれているので
将来的な拡張や修正などやりやすいのかなと感じました。
※IBで作成したときにiPhoneXで問題が出てきたので
それがやっかいだなと思ったこともあるかもしれませんが

レイアウトが複雑になったとき、
コード量が増えるのと、IBで複雑な制約を設定していくことのどちらを取るのか、
意見が分かれるところかもしれませんね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3