はじめに
こんにちは、フロントエンド部WEARiOSブロックの西山です。
iOS 13から登場したCompositional Layoutsを使うことで、App Storeのような複雑なUIが簡単に実現できるようになりました。
登場前は、UICollectionView in UICollectionViewまたは、UIStackView + UIScrollView in UICollectionViewで頑張って実現していたところをUICollectionView1つで実現できます。
一方で、登場前の方法では簡単に出来ていたカスタマイズをCompositional Layoutsで実現しようとすると難しくなるケースが存在しました。その1つに、横スクロールするセル全体にドロップシャドウを付ける方法が挙げられます。
WEARには次のようなUIが存在します。
このキャプチャ画像では少し分かりづらいかもしれませんが、セル全体にドロップシャドウが付いています。このUIをCompositional Layoutsで実現するのが一筋縄ではいかなかったので、WEARでの解決方法を紹介します。
環境
- Xcode 13.4.1
- Swift 5.6.1
一筋縄ではいかなかった理由
1.セルを覆うクラスが公開されていない
Compositional Layouts登場前のWEARでは、UIStackView + UIScrollView in UICollectionViewで実現していました。UIStackViewをラップするような形でシャドウ用のViewを用意する方法を取っていたので比較的簡単に実現できていました。
Compositional Layoutsでも似たような方法を取れれば簡単に実現できますが、残念ながらCompositional Layoutsのセルをラップするクラスは公開されていませんでした。
2. セルにシャドウを付けると繋ぎ目からはみ出る
セルのラップクラスにシャドウを付けることは適わなそうなので、セル1つ1つにシャドウを付ける方法を取りました。しかし、この方法ではセルとセルの繋ぎ目から前後どちらかのシャドウがはみ出してしまいうまくいきません。
解決方法
mask layerを利用する
セルにシャドウを付ける方法を取りつつmask layerを利用し、はみ出す部分を隠します。mask layerの簡単なおさらいですが、Viewの切り抜きや穴を開ける方法で語られることが多いと思います。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
// 丸のlayerをセンターに置く
let maskLayer = CAShapeLayer()
maskLayer.frame = view.bounds
let width: CGFloat = 200
let height: CGFloat = 200
let point = CGPoint(x: view.center.x - width / 2, y: view.center.y - height / 2)
let rect = CGRect(origin: point, size: .init(width: width, height: height))
let path = UIBezierPath(roundedRect: rect, cornerRadius: width / 2)
maskLayer.path = path.cgPath
view.layer.mask = maskLayer
}
}
背景色darkGrayのViewに丸のmask layerをセンターに置いたサンプルコードです。上記コードを実行すると次のようになります。
要するにmask layerと重なる部分が表示されるようになります。
mask layerを使用するためのPosition
やりたいことは、初めのセルの右側、中間のセルの左右、最後のセルの左側を隠すことです。
そのため、どのポジションにいるのか判断できるように型を用意しています。
enum Position {
case first
case middle
case last(isSingle: Bool)
}
extension Position {
init(index: Int, itemCount: Int) {
switch (index, itemCount) {
case let (index, count) where (index + 1) == count:
self = .last(isSingle: index == 0)
case (0, _):
self = .first
default:
self = .middle
}
}
}
ポジションに合わせてmask layerを定義します。
let width = bounds.width
let height = bounds.height
let maskSpace: CGFloat = 50 // 適切な値を指定
var maskLayer: CALayer? = CALayer()
maskLayer?.backgroundColor = UIColor.black.cgColor
switch position {
case .first:
// 上、左、下にシャドウが表示されるようlayerを被せる
maskLayer?.frame = .init(x: -maskSpace,
y: -maskSpace,
width: width + maskSpace,
height: height + maskSpace * 2)
case .middle:
// 上、下にシャドウが表示されるようlayerを被せる
maskLayer?.frame = .init(x: 0,
y: -maskSpace,
width: width,
height: height + maskSpace * 2)
case let .last(isSingle):
if isSingle {
// 隠したくないのでlayerを削除
maskLayer = nil
} else {
// 上、下、右にシャドウが表示されるようlayerを被せる
maskLayer?.frame = .init(x: 0,
y: -maskSpace,
width: width + maskSpace,
height: height + maskSpace * 2)
}
}
WEARでは再利用できるようShadowViewを用意しています。
続きはこちら