はじめに
↑ このような表示に見覚えはあるでしょうか?
今回はこんな表示をできる UIView
のサブクラスを実装しました.
実装方法
iOS 9.0+であれば皆さん大好き UIStackView
が使えるのでこいつを使うようにしました.
UIStackView
は3つ使っています.
UIView
に設置する前提で書いてみます.
ベース
まずはベースとなる大枠に入る UIStackView です.
2枚目以上表示したい場合にどういう表示にしたいかという前提を考えるとベースは水平方向へのスタックがいいと考えました.
lazy var baseStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = true stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .fillEqually stackView.clipsToBounds = true return stackView }() addSubView(baseStackView) // AutoLayout を設定しておく NSLayoutConstraint(item: baseStackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0).isActive = true NSLayoutConstraint(item: baseStackView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0).isActive = true NSLayoutConstraint(item: baseStackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0).isActive = true NSLayoutConstraint(item: baseStackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0).isActive = true
左右に並ぶ UIStackView
こちらは,ベースとは違って3〜4枚の画像があるときに垂直方向へのスタックがいいと考えています.
lazy var leftStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .fillEqually stackView.clipsToBounds = true return stackView }() lazy var rightStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .fillEqually stackView.clipsToBounds = true return stackView }()
leftStackView
rightStackView
をプロパティとして宣言しておけば,
baseStackView
の生成時に
// baseStackView let stackView = UIStackView(arrangedSubviews: [leftStackView, rightStackView])
と変更することができますね.
それぞれの画像を表示する UIImageView
ここで扱うのは, leftStackView
rightStackView
に2つずつ入れる UIImageView
です.
let imageView = UIImageView(image: placeholderImage) imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true
↑の内容で4つ同じ UIImageView
を作りました.
また,こちらも leftStackView
rightStackView
と同様に
プロパティとして topLeftImageView
topRightImageView
bottomLeftImageView
bottomRightImageView
と 宣言しておくとそれぞれの UIStackView
で
// leftStackView let stackView = UIStackView(arrangedSubviews: [topLeftImageView, bottomLeftImageView]) // rightStackView let stackView = UIStackView(arrangedSubviews: [topRightImageView, bottomRightImageView])
と変更できます.
ここまでできたら,大本となっている UIStackView (baseStackView) もしくは baseStackView
を載せている UIView
にある程度の AutoLayout の制約をつけると虚無ではありますがものは表示できるかと思います.
spacing の調整
それぞれ3つの UIStackView
に対して spacing を設定しておくことで自動でマージンを取ってくれますね.
let spacing: CGFloat = 3 baseStackView.spacing = spacing leftStackView.spacing = spacing rightStackView.spacing = spacing
UIImageView の表示をコントロールする
var images: [UIImage] = []
とかをプロパティで宣言してその値をハンドリングして表示・非表示を切り替えるような実装でも良いかと思います.
↑ 上記のような並びにしたいと思います.
// UIStackView leftStackView.isHidden = images.isEmpty rightStackView.isHidden = images.count < 2 // UIImageView topLeftImageView.isHidden = images.isEmpty topRightImageView.isHidden = images.count < 2 bottomLeftImageView.isHidden = images.count != 4 bottomRightImageView.isHidden = images.count < 3
これで上のスクショで示した形で与えた画像の枚数 (4枚以内) で表示が可能になりました.
画像のバインド
画像の枚数と与えた画像配列の順序によって画像を正しい場所に表示しないといけません.
func getImageView(from index: Int) -> UIImageView? { switch (images.count, index) { case (1, 0), (2, 0), (3, 0), (4, 0): return topLeftImageView case (2, 1), (3, 1), (4, 1): return topRightImageView case (4, 2): return bottomLeftImageView case (3, 2), (4, 3): return bottomRightImageView default: return nil } }
というメソッドを実装してみました.
images.enumerated().forEach { index, image in guard let imageView = getImageView(from: index) else { return } imageView.image = image }
これで上手く行けるはずです.
作ったもの
割と適当に実装してるので PR ください
機能
- どの
UIImageView
をタップをしたかをindex: Int
付きでハンドリングしてくれる Delegate - 直接画像を食わせるだけの機会はほぼないと思うので,
URL
やPhotosKit
など様々なニーズに応えられるようにカスタマイズできるSource
の対応
使い方
インストール方法は README.md に書いてあるので読んでくだしあ
ソースコードで対応する場合
import MultipleImageView class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let imageView = MultipleImageView(frame: .init(x: 0, y: 0, width: 200, height: 100) view.addSubview(imageView) imageView.sources = [ .uiimage(UIImage(named: "ic_hoge")!), .url(URL(string: "https://example.com/foo.jpg")!) .custom { imageView in DispatchQueue.global().async { guard let data = UserDefaults.standard.data(forKey: "key_bar_image_data"), let image = UIImage(data: data) else { return } DispatchQueue.main.async { imageView.image = image } } } ] imageView.reloadData() } } // MARK: - MultipleImageViewDelegate extension ViewController: MultipleImageViewDelegate { func multipleImageViewShouldGetImage(_ imageView: UIImageView, sourceForURL url: URL, index: Int) { // Nuke や PINRemoteImage などプロジェクトやご自身の宗教に合わせてキャッシュライブラリを使えます DispatchQueue.global().async { let task = URLSession.shared.dataTask(with: url) { data, _, _ in guard let data = data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { imageView.image = image } } task.resume() } } func multipleImageViewDidSelect(_ imageView: UIImageView, index: Int) { print("Tapped index: \(index)") } }
というような感じで扱えます.
最後に
PR も Star も待ってます ⭐️