今回はアプリの初回チュートリアルなどでスクロールをカッコよく見せる演出として使われるパララックス(視差効果)を表現する実装をご紹介します。
今回のものはサンプルなので見た目はイマイチですが、参考程度にどうぞ。
完成イメージ
白い正方形のViewには視差効果を与えておらず、1ページ目の青いLabel、2ページ目の黒いLabelに視差効果を与えています。
青のLabelと白いViewのスクロール量が少しずれていることがわかると思います。

実装概要
- UIScrollViewでページングをする横スクロールを作成します
- UIScrollView内にはUIStackViewを配置し、任意のページ分のコンテンツを追加してスクロールできるようにします。
- 各ページのUIViewのtransform, alphaにアクセスして、スクロール量に応じて値を変化させます。
View階層

実装
今回はUI実装の簡略化のためにSnapKitというライブラリを使用させていただきます。
import UIKit
import SnapKit
final class ParallaxViewController: UIViewController {
let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.isPagingEnabled = true
return scroll
}()
let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .fill
stack.backgroundColor = .purple
return stack
}()
let containers: [LabelContainer] = [
LabelContainer(labelColor: .blue),
LabelContainer(labelColor: .black)
]
private var contentOffsetObservation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
configureScrollView()
contentOffsetObservation = scrollView.observe(\.contentOffset) { [weak self] scrollView, _ in
guard let me = self else { return }
me.containers.enumerated().forEach { index, container in
let distanceContainerToLabel = container.frame.origin.x - scrollView.contentOffset.x
// スクロール量に応じてLabelの表示位置をずらす
container.label.transform = .init(
translationX: distanceContainerToLabel * -0.3,
y: .zero
)
let limit = me.scrollView.bounds.width
// スクロール量に応じてLabelの透明度を変化させる
container.alpha = {
switch distanceContainerToLabel {
case -limit..<0, 0..<limit:
return 1 - abs(distanceContainerToLabel) / limit
default:
return .zero
}
}()
}
}
}
private func configureScrollView() {
view.addSubview(scrollView)
scrollView.snp.makeConstraints {
$0.top.left.right.equalToSuperview()
$0.height.equalTo(300)
}
scrollView.addSubview(stackView)
stackView.snp.makeConstraints {
$0.edges.height.equalToSuperview()
}
containers.forEach { container in
stackView.addArrangedSubview(container)
container.snp.makeConstraints {
$0.width.equalTo(view.snp.width)
}
}
let box = UIView()
box.backgroundColor = .white
scrollView.addSubview(box)
box.snp.makeConstraints {
$0.width.height.equalTo(200)
$0.center.equalToSuperview()
}
}
}
final class LabelContainer: UIView {
let label: UILabel
init(labelColor: UIColor) {
label = makeLabel(color: labelColor)
super.init(frame: .zero)
addSubview(label)
label.snp.makeConstraints { $0.edges.equalToSuperview() }
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
fileprivate func makeLabel(color: UIColor) -> UILabel {
let label = UILabel()
label.text = Array(repeating: "LABEL", count: 100).joined()
label.textAlignment = .center
label.numberOfLines = 0
label.textColor = .white
label.backgroundColor = color
return label
}