post Image
Swift4にも対応したUI系のライブラリを活用して面白い動きを入れてみたサンプル実装まとめ

1. はじめに

皆様お疲れ様です。Swift AdventCalendarの9日目を担当させて頂きます、fumiyasac(Fumiya Sakai)と申します。何卒よろしくお願い致します。
以前に作成した記事で、できるだけUI系のライブラリを用いないでアニメーションを盛り込んだサンプルの実装に関する紹介をしたのですが、今回の記事はその番外編として動きやアニメーションが美しいライブラリを活用してUIを構築した際の事についての紹介記事になります。

できるだけ、Swift4にすでに対応したUI系のライブラリを組み合わせて作成してみました。(記事の内容に関してはあとで少し手直しする予定です)

Githubでのサンプルコード:

サンプルの全体的な動きの動画:

※こちらのサンプルはPullRequestやIssue等はお気軽にどうぞ!

2. 今回のサンプル概要について

以前にも書いた記事「Swiftで便利なライブラリやUIパーツに工夫を凝らしてUIを彩るサンプル例(ライブラリ選定とベース作成)」と同様に、iOSのライブラリを紹介しているサイトやGithubのREADMEやライブラリに収録されているサンプルソースを調べて、Swift4にも対応がされておりかつStoryboardでもなるべく扱いやすく実装がシンプルそうなものを組み合わせてみました。全体のレイアウトや動きに関わる部分のライブラリに加えて、UIのワンポイントになりそうな部分のライブラリも使用しています。

※ 今回の紹介するサンプルは特に非同期処理の通信やデータの登録等は行なっていません。

サンプルのキャプチャ画像その1:

library_sample1.jpg

サンプルのキャプチャ画像その2:

library_sample2.jpg

環境やバージョンについて:

  • Xcode9.1
  • Swift4.0
  • MacOS Sierra (Ver10.12.6)

使用ライブラリ:

ライブラリ名 ライブラリの機能概要
FlexibleSteppedProgressBar チュートリアルでよくあるステップインジケーターを表示するライブラリ
VegaScrollFlowLayout 奥行きのあるようなスクロールをするUICollectionViewのFlowLayoutをカスタマイズしたライブラリ
FSPagerView 様々なスクロール表現をするライブラリ
ParallaxHeader UITableViewのヘッダー部分をスクロール量に応じてパララックス表示をするライブラリ
AMScrollingNavbar スクロールの変化量に合わせてNavigationBarを折りたたむ表現をするライブラリ
Cosmos 評価のレーティング表示を扱いやすくするライブラリ
ActiveLabel URLやハッシュタグのリンク表示を扱いやすくするライブラリ
FontAwesome.swift 「Font Awesome」アイコンの利用

Podfile内の設定は下記のようになります。(FontAwesome.swift及びActiveLabelに関しては該当リポジトリのSwift4系の対応を行っているブランチのものを直接取得をしています)

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'MixedUILibrariesSample' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for MixedUILibrariesSample

  # UILaibrary
  pod 'FlexibleSteppedProgressBar'
  pod 'VegaScrollFlowLayout'
  pod 'FSPagerView'
  pod 'ParallaxHeader', '~> 2.0.0'
  pod 'AMScrollingNavbar'
  pod 'Cosmos', '~> 12.0'
  pod 'ActiveLabel', :git => 'git@github.com:optonaut/ActiveLabel.swift.git', :branch => 'master'
  pod 'FontAwesome.swift', :git => 'https://github.com/thii/FontAwesome.swift', :branch => 'swift-4.0'
end

今回紹介したライブラリについては、ライブラリのリポジトリに使用サンプルやリファレンス等が詳細に表示されているものが多いので、この記事で紹介した形のUI以外でも活用できると思います。

3. チュートリアル画面の実装でのステップインジケーターを表示する「FlexibleSteppedProgressBar」の実装解説

チュートリアルの動きに合わせて、今どの位置にいるかというインジケーターを表示する部分については、アニメーションを伴う部分の細かな部分までDIYするのはなかなか大変なので、UIPageViewControllerの動きに合わせてライブラリを組み合わせた実装をしました。

インジケーター部分は「FlexibleSteppedProgressBar」を利用しており、インジケーターの初期設定や動いた際の設定に関しては、下記のような形でFlexibleSteppedProgressBarDelegateを活用することができます。

MainViewController.swift
import UIKit
import FlexibleSteppedProgressBar

class MainViewController: UIViewController {

    //ステップインジケータ表示用部品のOutlet
    @IBOutlet weak var stepIndicator: FlexibleSteppedProgressBar!

    ・・・(省略)・・・

    //ContainerViewにEmbedしたUIPageViewControllerのインスタンスを保持する
    fileprivate var pageViewController: UIPageViewController?

    //ページングして表示させるViewControllerを保持する配列
    fileprivate var tutorialControllerLists = [TutorialBaseViewController]()

    //チュートリアル画面に表示する要素
    private let tutorialContents: [Tutorial] = Tutorial.getSampleData()

    override func viewDidLoad() {
        super.viewDidLoad()

        ・・・(省略)・・・

        setupStepIndicator()

        ・・・(省略)・・・
    }

    ・・・(省略)・・・

    //ステップインジケータ表示の初期表示に関するセッティングをするメソッド
    private func setupStepIndicator() {
        stepIndicator.delegate = self

        //ステップインジケータの表示数を設定する
        stepIndicator.numberOfPoints = 3

        //ステップインジケータの線幅を設定する
        stepIndicator.lineHeight = 4

        //ステップインジケータの配色及び外枠を設定する
        stepIndicator.selectedOuterCircleLineWidth = 4.0
        stepIndicator.selectedOuterCircleStrokeColor = UIColor.orange
        stepIndicator.currentSelectedCenterColor = UIColor.white

        //ステップインジケータのアニメーション秒数を設定する
        stepIndicator.stepAnimationDuration = 0.26

        //ステップインジケータの現在位置を設定する
        stepIndicator.currentIndex = 0
    }

    ・・・(省略)・・・
}

・・・(省略)・・・

// MARK - FlexibleSteppedProgressBarDelegate

extension MainViewController: FlexibleSteppedProgressBarDelegate {

    //ステップインジケータを選択した際に実行されるメソッド
    func progressBar(_ progressBar: FlexibleSteppedProgressBar,
                     didSelectItemAtIndex index: Int) {
        stepIndicator.currentIndex = index
    }

    //ステップインジケータを選択の選択可否を設定するメソッド
    func progressBar(_ progressBar: FlexibleSteppedProgressBar,
                     canSelectItemAtIndex index: Int) -> Bool {
        return false
    }

    //ステップインジケータに表示される文言を表示するメソッド
    func progressBar(_ progressBar: FlexibleSteppedProgressBar,
                     textAtIndex index: Int, position: FlexibleSteppedProgressBarTextLocation) -> String {
        //特に必要ない場合にはカラッポの文字列にする
        return ""
    }
}

ちなみにFlexibleSteppedProgressBarは下記のようにInterfaceBuilderで配色やインジケーターの大きさ、一緒に表示する文言なども設定できるので、上手に活用することでデザイン面でも綺麗にこだわったインジケーターができるかと思います。

その一例として、今回の実装ではインジケーター部分の番号表示は削除をして対応をしています。

FlexibleSteppedProgressBarのInterfaceBuilder上での見え方:

tutorial_capture.jpg

初回起動画面をはじめとするような部分については、綺麗なアニメーションやUIがあるとかなり目を引くポイントになるので色々と動きに彩りのあるライブラリ等を活用することで、良い動きを工夫してみても面白いかもしれませんね。

4. フォトギャラリーの様な動きを表現する「FSPagerView」及び奥行きのあるレイアウトのUICollectionViewを実現する「VegaScrollFlowLayout」の実装解説

フォトギャラリーで良く見かける様な動きやUICollectionViewやUITableViewを拡張して心地の良い変化やアニメーションの実装はUIを構築する上では、こだわりたい部分の1つだと思います。今回はCoverFlowの様な表現のフォトギャラリー部分とコンテンツの表示に一工夫を加えた動きを演出するために「FSPagerView」「VegaScrollFlowLayout」を活用した実装をしてみました。
また、コンテンツ一覧の表示部分のViewControllerとフォトギャラリー部分のViewControllerについては、今回のサンプルではContainerViewを用いて分割しています。

FSPagerView及びVegaScrollFlowLayout導入時のInterfaceBuilderでの見え方:

contents_storyboard_1.jpg

1.FSPagerViewを活用したフォトギャラリー部分の実装:

基本的にはUICollectionViewを取り扱う時と同様な実装で実現ができます。
フォトギャラリーの動きをライブラリを変更したい場合は、coverFlowSliderView.transformer = FSPagerViewTransformer(type: .coverFlow)FSPagerViewTransformer(type: .coverFlow)に設定するenum値を変更することで実現ができます。

補足)FSPagerViewとUICollectionViewの類似点についての大まかなまとめ:

diff_collectionView.jpg

CoverFlowSliderViewController.swift
import UIKit
import FSPagerView

class CoverFlowSliderViewController: UIViewController {

    //カバーフロー形式のギャラリーを表示するためのFSPagerView
    @IBOutlet weak fileprivate var coverFlowSliderView: FSPagerView! {
        didSet {
            self.coverFlowSliderView.register(FSPagerViewCell.self, forCellWithReuseIdentifier: "cell")
        }
    }

    fileprivate var coverflowContents: [Coverflow] = [] {
        didSet {
            self.coverFlowSliderView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCoverFlowSliderView()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    //MARK: - Private Functions

    private func setupCoverFlowSliderView() {
        //UICollectionViewとほとんど同じ感じで設定ができる

        coverFlowSliderView.delegate = self
        coverFlowSliderView.dataSource = self
        coverFlowSliderView.isInfinite = true
        coverFlowSliderView.itemSize = CGSize(width: 180, height: 120)
        coverFlowSliderView.interitemSpacing = 16
        coverFlowSliderView.transformer = FSPagerViewTransformer(type: .coverFlow)

        coverflowContents = Coverflow.getSampleData()
    }
}

//MARK: - FSPagerViewDataSource, FSPagerViewDelegate

extension CoverFlowSliderViewController: FSPagerViewDataSource, FSPagerViewDelegate {

    func numberOfItems(in pagerView: FSPagerView) -> Int {
        return coverflowContents.count
    }

    func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
        let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
        let coverflow = coverflowContents[index]

        cell.contentView.layer.shadowOpacity = 0.4
        cell.contentView.layer.opacity = 0.86

        cell.imageView?.image = coverflow.thumbnail
        cell.imageView?.contentMode = .scaleAspectFill
        cell.imageView?.clipsToBounds = true
        return cell
    }

    func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) {
        pagerView.deselectItem(at: index, animated: true)
        pagerView.scrollToItem(at: index, animated: true)
    }
}

2.VegaScrollFlowLayoutを活用した奥行きのある動きをするUICollectionViewの実装:

こちらも前述した「FSPagerView」と同様に、基本的にはUICollectionViewを取り扱う時と同様な実装で実現ができます。
注意点としては、InterfaceBuilder上のUICollectionViewFlowLayoutの部分にはVegaScrollFlowLayoutを設定しておくことを忘れないでください。
「FSPagerView」と比べて独自に作成したセルクラスを適用することもできます。

ListeViewController.swift
import UIKit
import VegaScrollFlowLayout

class ListViewController: UIViewController {

    //UI部品の配置
    @IBOutlet weak var listCollectionView: UICollectionView!

    fileprivate var listContents: [List] = [] {
        didSet {
            self.listCollectionView.reloadData()
        }
    }

    //CollectionView表示の隙間やサイズに関する設定
    private let itemHeight: CGFloat  = 180
    private let lineSpacing: CGFloat = 15
    private let spaceInset: CGFloat  = 15
    private let topInset: CGFloat    = 10

    override func viewDidLoad() {
        super.viewDidLoad()

        setupNavigationBar()
        setupListCollectionViewCell()
        setupListCollectionViewLayout()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    //MARK: - Private Functiom

    private func setupNavigationBar() {
        removeBackButtonText()
        self.navigationController?.navigationBar.tintColor = UIColor.white
        self.navigationItem.title = "クリスマスに食べたいディナー"
    }

    private func setupListCollectionViewCell() {
        listCollectionView.delegate = self
        listCollectionView.dataSource = self
        listCollectionView.registerCustomCell(ListCollectionViewCell.self)

        //リスト用のUICollectionViewの下部をセルの高さ分マージンを開ける
        listCollectionView.contentInset.bottom = itemHeight

        listContents = List.getSampleData()
    }

    private func setupListCollectionViewLayout() {

        //表示するセルのサイズや隙間に関する値の設定をする
        guard let layout = listCollectionView.collectionViewLayout as? VegaScrollFlowLayout else { return }
        layout.minimumLineSpacing = lineSpacing
        layout.sectionInset = UIEdgeInsets(top: topInset, left: 0, bottom: 0, right: 0)

        let itemWidth = UIScreen.main.bounds.width - 2 * spaceInset
        layout.itemSize = CGSize(width: itemWidth, height: itemHeight)

        listCollectionView.collectionViewLayout.invalidateLayout()
    }
}

//MARK: - UICollectionViewDataSource, UICollectionViewDelegate

extension ListViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCustomCell(with: ListCollectionViewCell.self, indexPath: indexPath)
        let list = listContents[indexPath.row]
        cell.setCell(list)
        return cell
    }

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

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

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        performSegue(withIdentifier: "ToArticleViewController", sender: self)
    }
}

今回紹介したライブラリの他にもスクロール時の動きやフォトギャラリーの様な表現をするライブラリは数多くありますので色々と調べたり、READMEを追いかけていくと素敵かつ作ろうとしているアプリのUI表現にぴったりの物に出会えるかもしれませんね。

5. スクロールに合わせて画像のパララックス表現をする「ParallaxHeader」及びNavigationBarの折りたたみを表現する「AMScrollingNavbar」を組み合わせた実装解説

NavigationBarの折りたたみ開閉部分と併せてのUITableViewHeaderのパララックス表現に関しては、自前で実装する際にはNavigationBarや特にSafeAreaの考慮を意識しないと思わぬUI上の不具合を作ってしまいやすい部分です。また、Viewが込み入った実装になりやすい部分なので、できるだけパララックス表現の実装とNavigationBarの実装を理解しやすい状態にするのもなかなか大変です。

できるだけUI系のライブラリを用いないアニメーションを盛り込んだサンプル実装まとめ(前編) でも紹介した動きと似た様な表現ができるライブラリの1つに「ParallaxHeader」があるのでこちらを活用して行きます。また手軽にNavigationBarの折りたたみ表現をするライブラリには「AMScrollingNavbar」を使用しました。

「ParallaxHeader」に関しては、ライブラリを該当のViewControllerにインポートするとUITableViewに.parallaxHeaderというプロパティができるので下記のような形でサイズやパララックスの状態・配色などを設定していきます。スクロール中に発火させたい処理等がある場合にはparallaxHeaderDidScrollHandlerのクロージャー内に処理を記述する形になります。

ArticleViewController.swift
private func setupParallaxTableViewHeader() {
    let imageView = UIImageView()
    imageView.image = UIImage(named: "article_main")
    imageView.contentMode = .scaleAspectFill

    articleTableView.parallaxHeader.view = imageView
    articleTableView.parallaxHeader.view.backgroundColor = UIColor.black
    articleTableView.parallaxHeader.height = 240
    articleTableView.parallaxHeader.minimumHeight = 0
    articleTableView.parallaxHeader.mode = .centerFill
    articleTableView.parallaxHeader.parallaxHeaderDidScrollHandler = { parallaxHeader in
        parallaxHeader.view.alpha = parallaxHeader.progress
    }
}

「AMScrollingNavbar」に関しては、まずは下記のような形でStoryboardのrootViewControllerになっているNavigationControllerをScrollingNavigationControllerクラスに差し替えておき、ScrollingNavigationControllerにつなげてあるNavigationBarの折りたたみ表現を行いたいViewControllerに対して下記のような処理をつけてあげればOKです。

(スクロール状態で異なる処理を行いたい場合についてはScrollingNavigationControllerDelegateを活用するような形になります。)

AMScrollingNavbar導入時のInterfaceBuilderでの見え方:

contents_storyboard_2.jpg

ArticleViewController.swift
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    if let navigationController = navigationController as? ScrollingNavigationController {
        navigationController.followScrollView(articleTableView, delay: 44.0)

        //MEMO: ScrollingNavigationControllerDelegateを利用する際に必要な宣言
        //navigationController.scrollingNavbarDelegate = self
    }
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if let navigationController = self.navigationController as? ScrollingNavigationController {
        navigationController.stopFollowingScrollView()
    }
}

この動きも様々なアプリのUIでよくお目にかかる動きではありますが、このような形でライブラリを活用していくことで「実装の見通しを良くできる」または「少ない労力で手早く実装ができる」というメリットもあるので、仕様や要件に合わせてUI表現をフルスクラッチ or ライブラリの活用の判断をすることでより選択肢が広がっていくのではないかと思います。

6. レーティング表示部分「Cosomos」及びハッシュタグ・リンク等の表示をしやすくする「ActiveLabel」の実装解説

評価を表示する★型のレーティング表示はボタンを並べて表示するなどの方法がありますが、3.5等の小数点表示をする時に★の部分を半分だけ表示したり等の細かな点まで表現したい時に自前だと結構面倒な部分です。その際に役立ちそうなライブラリの1つに「Cosmos」があります。

まずは下準備として、@IBOutlet weak private var ratingStarView: CosmosView!でStoryboardとの接続をしておき、ratingStarViewに対してのセッティングを行うようにします。

ListCollectionViewCell.swift
private func setupListCollectionViewCell() {
    ・・・(省略)・・・

    //タップすると星型レーティングの値を更新するか否かの判定
    ratingStarView.settings.updateOnTouch = false

    //星型レーティングのサイズ
    ratingStarView.settings.starSize = 20

    //星型レーティングの間隔サイズ
    ratingStarView.settings.starMargin = 5

    //星型レーティングの塗りつぶしのモード(今回は小数点の場合は比率表示にする)
    ratingStarView.settings.fillMode = .precise
}

このライブラリについては、デフォルトのレーティングの形は星型ですが星型以外の形を設定することはもちろん、配色などの細部に関してもデザインの調整がしやすことやInterfaceBuilder上で上記の様なサイズ等に関する設定することも可能なので、意外と使い所はありそうなライブラリかなと感じています。

CosmosViewのInterfaceBuilder上での見え方:

cosmos_view.jpg

またWebページのテキストの中にリンクがあるような形と同様に、UILabelに表示するテキストの中にURLやハッシュタグを混ぜた状態にする場合も「ActiveLabel」を利用すると、このような場合でも便利に扱うことができます。

こちらも同様に下準備として、@IBOutlet weak private var articleHashtagLabel: ActiveLabel!でStoryboardとの接続をしておき、articleHashtagLabelに対してのセッティングを行うようにします。(ここではハッシュタグの例になりますが、URLや@に関しても同様に扱うことができるのでなかなか便利です)

ListCollectionViewCell.swift
func setCell(_ article: Article) {
    let hashtagString = article.hashTags.joined(separator: " ")

    //ハッシュタグの行の高さを調節する
    let hashtagParagraphStyle = NSMutableParagraphStyle.init()
    hashtagParagraphStyle.minimumLineHeight = 20
    let hashtagAttributedText = NSMutableAttributedString.init(string: hashtagString)
    hashtagAttributedText.addAttribute(NSAttributedStringKey.paragraphStyle, value: hashtagParagraphStyle, range: NSMakeRange(0, hashtagAttributedText.length))

    //リンク表示にするラベルの種類を設定する
    articleHashtagLabel.enabledTypes = [.hashtag]

    //ハッシュタグを表示する
    articleHashtagLabel.attributedText = hashtagAttributedText

    //それぞれのハッシュタグをクリックした時に発火するアクションを設定する
    articleHashtagLabel.handleHashtagTap { hashtag in
        print("押されたハッシュタグ:\(hashtag)")
    }
}

上記2つのライブラリはUIのレイアウトやアニメーションに関する部分とは異なるライブラリの活用例になりますが、このようなDIYだとなかなか対応がしにくい部分にピンポイントで活用していくと実装もシンプルにできる時があるのではないかと思います。

7. あとがき

今回は前に前編・後編の2本立てでお送りした「できるだけUI系のライブラリを用いないアニメーションを盛り込んだサンプル実装まとめ」の番外編として、この記事で紹介しているサンプル実装に類似した動きをSwift4系に対応したUIライブラリを活用して実装をした事例の紹介になります。

導入の手順や実装に関してもさほど大きく既存のUIKitの実装に乖離しないものを選んで実装したつもりなので、皆様の参考になれば嬉しく思います。

そして記事の公開がギリギリになってしまって大変申し訳ございませんでした。引き続き様々な綺麗かつコンテンツにびったしと合う様なUIの研究に今後も精進していきたいと思いますので、何卒よろしくお願い致します。

UIまわりの実装は迷うことや組み合わせで迷う部分が多々ある部分でもありますし、またUIへの細部のこだわりによって結果的に冗長なコードになってしまうデメリットもあるかもしれません。しかしながら、ライブラリの特性や使い方を理解した上で活用することで彩りのある気持ちの良いUI作成の一助にできるケースも数多くあるので、その点を踏まえてのライブラリの精査や検証を行う事でより楽しいUI開発ができると考えております。


『 Swift 』Article List