Folioというアプリをご存知でしょうか?
FOLIO(フォリオ)とは、人工知能やドローンといったテーマを選んで株式投資をすることができる資産運用サービスです。
このアプリ、iOSアプリエンジニアとして有名な方々が開発に携わっています。
そんなFolioで使用されているUIのソースがこちらで公開されているので、その中のWalkthroughを本記事では見ていきます🙂
FolioのイケてるWalkthrough
FolioのWalkthrough画面は初回起動時に表示される⬇️のような画面です🤗
画面の構成をみてみる
ストーリーボードの構成は⬇️のようになっています。
簡単な図に起こしてみるとこんな感じです。
(下記の図では画面間のマージンは無視しています。マージンが0の箇所も、見やすさのためにあえて空けています。)
3つのスクロールビューがあり、それらが肝です。
- Outer Scroll View:Viewを4つ持ちます。中のViewではページごとの背景色が設定されています
- Inner Scroll View :Viewを2つ持ちます。1つはロゴ表示画面で、もう一つはスクロールビューを持つ画面(ベゼルの中の要素が切り替わるのを実現している画面)です。
- Bezel Scroll View :Viewを3つ持ちます。ベゼルの中の各ページを表すViewです。
ソースを見てみよう
基本情報
import UIKit
import RxSwift
import RxCocoa
class WalkthroughViewController: UIViewController {
@IBOutlet weak var outerScrollView: UIScrollView!
@IBOutlet weak var innerScrollView: UIScrollView!
@IBOutlet weak var bezelScrollView: UIScrollView!
@IBOutlet weak var pageControl: UIPageControl!
private let disposeBag = DisposeBag()
// 省略
}
RxSwiftを使用して書かれています。
UIでは、IBOutletとして 3つのスクロールビュー、1つのページコントロールが紐づけられています。
viewWillApper, viewDisapper
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
ここではナビゲーションの設定が行われています。
walkThrough画面ではナビゲーションを隠した状態になっており、画面が消えるタイミングでナビゲーションを表示しています。
viewDidLoad
UIのメイン処理部分が設定されています。
まず、outerScrollViewのスクロールに基づく挙動の記述です。
outerScrollView.rx.contentOffset
.subscribe(onNext: { [weak self] in
guard let innerScrollView = self?.innerScrollView, let bezelScrollView = self?.bezelScrollView else {
return
}
①
innerScrollView.contentOffset.x = min($0.x, innerScrollView.bounds.width)
let factor = bezelScrollView.bounds.width / innerScrollView.bounds.width
②
let offsetX = max(0, min(($0.x - innerScrollView.bounds.width) * factor,
bezelScrollView.contentSize.width - bezelScrollView.bounds.width))
bezelScrollView.contentOffset.x = offsetX
})
.disposed(by: disposeBag)
① インナースクロールビューのオフセット値を算出・設定
インナースクロールビューはロゴページとベゼルスクロールページの2ページをもっています。
アウタースクロールビューで2,3,4ページ目を表示しているときにインナースクロールビューは2ページ目を固定で表示しておく必要があるので、innerScrollView.contentOffset.xがinnerScrollView.bounds.width(innerScrollViewの2ページ目までの横幅)より大きくならないようにしています。
② ベゼルスクロールビューのオフセット値を算出・設定
<minの判定について>
($0.x - innerScrollView.bounds.width) * factor:ベゼルスクロールビューのオフセット値を算出しています。
インナースクロールビューが2ページ目にあるとき、ベゼルスクロールビューでは1ページ目を表示していれば良いので、ロゴページがある分(innerScrollView.bounds.width)だけxから引いています。
factorがあるのは、アウタースクロールビュー、インナースクロールビューの値で計算したサイズをベゼルスクロールビューのサイズ値に変換するためです。
bezelScrollView.contentSize.width - bezelScrollView.bounds.width:固定値であり、オフセットの最大値です。ベゼルスクロールビューは3ページなのでそれ以上はスクロールしないようになります。
<maxの判定について>
インナースクロールビューにおいて、ベゼルスクロールビューのあるページに移るまでは ($0.x - innerScrollView.bounds.width) * factor の結果がマイナスの値になりますが、ベゼルスクロールビューのオフセットとしてマイナスを当てる必要がないので最小値を0としています。
ページコントロールとouterScrollViewの連携
private extension Reactive where Base: UIScrollView {
①
var currentPage: Observable<Int> {
return didEndDecelerating.map({
let pageWidth = self.base.frame.width
let page = floor((self.base.contentOffset.x - pageWidth / 2) / pageWidth) + 1
// これでいいのでは?
// let page = self.base.contentOffset.x / pageWidth
return Int(page)
})
}
}
private extension UIScrollView {
②
func setCurrentPage(_ page: Int, animated: Bool) {
var rect = bounds
rect.origin.x = rect.width * CGFloat(page)
rect.origin.y = 0
// 指定されたページが表示されるようにスクロールさせる
scrollRectToVisible(rect, animated: animated)
}
}
① 現在のページ情報を返すObservableを作成
ここですが、
let page = self.base.contentOffset.x / pageWidth
で良いのではと思ったりしているのですが、どうなんでしょう。
② 特定ページまでスクロールさせるメソッドを作成
引数で指定されたページにスクロールビューをスクロールさせるメソッドです。
viewDidload内ではそれらを以下のように利用しています
①
outerScrollView.rx.currentPage
.subscribe(onNext: { [weak self] in
self?.pageControl.currentPage = $0
})
.disposed(by: disposeBag)
②
pageControl.rx.controlEvent(.valueChanged)
.subscribe(onNext: { [weak self] in
guard let currentPage = self?.pageControl.currentPage else { return }
self?.outerScrollView.setCurrentPage(currentPage, animated: true)
})
.disposed(by: disposeBag)
① アウタースクロールビュー → ページコントロール
アウタースクロールビューのページが変わった時に、ページコントロールのcurrentPageを変更しています。
② ページコントロール → アウタースクロールビュー
(タップして)ページコントロールの値が変わった時に、アウタースクロールビューのページも変わるようにsetCurrentPageを読んでいます。
おわりに
少ないコードで、シンプルに実装されていて非常に勉強になります!
Thank you Folio !!