『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

Twitter のタイムラインのような複数画像を表示するコンポーネントの実装をしたライブラリを雑に作った

はじめに

f:id:nanashinodonbee:20210812171311j:plain

↑ このような表示に見覚えはあるでしょうか?
今回はこんな表示をできる 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] = []

とかをプロパティで宣言してその値をハンドリングして表示・非表示を切り替えるような実装でも良いかと思います.

f:id:nanashinodonbee:20210812175527p:plain

↑ 上記のような並びにしたいと思います.

// 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
}

これで上手く行けるはずです.

作ったもの

github.com

割と適当に実装してるので PR ください

機能

  • どの UIImageView をタップをしたかを index: Int 付きでハンドリングしてくれる Delegate
  • 直接画像を食わせるだけの機会はほぼないと思うので, URLPhotosKit など様々なニーズに応えられるようにカスタマイズできる 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 も待ってます ⭐️