インスタなどでよく見る重なり合うアイコンをUIStackViewで作ってみた


この記事はフラー株式会社 [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/fuller-inc) 13日目の記事です。
12日目は @nnsnodnb さんで「[Sign in with Apple のトークン取り消しを実装してみた](https://nnsnodnb.hatenablog.jp/entry/siwa-revoke-rest-api-consent-revoked)」でした。


私は現在業務でSNSアプリを作っているので、最近作ったSNS等でよく見るViewを紹介します。

# 作るもの
ユーザー数によってアイコンの数が変化する、重なり合うアイコンをUIStackViewを用いて作ってみました。

こんな↓感じでいいねしたユーザーのアイコンを最小1つ、最大3つまで表示するやつです。
![スクリーンショット 2022-12-13 3.30.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/6586982b-4555-f4f2-7edd-890aea377310.png) ![スクリーンショット 2022-12-13 2.21.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/dbc5a6d5-d740-f927-ac59-c398f63e58b6.png) ![スクリーンショット 2022-12-13 3.31.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/2fbdfa1e-b0e5-1a1f-cd29-e9ad13c1077e.png)

## ざっくり仕様
- いいねしたユーザーのアイコンを最小1つ、最大3つまで表示する
- ユーザーがアイコン画像を設定していない場合は、ローカルのplaceholderイメージを表示する

## 利用したライブラリ
画像キャッシュライブラリとして `Kingfisher` を使いました。

# いざ実装！
## UITableViewCell デザイン
ざっくりcellのデザインを作ります！
今回説明したいのが、`Main Icons Stack View`以下のところ
![スクリーンショット 2022-12-13 3.45.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/b0cb105f-3d11-fe9e-bcdd-31d02f9f8bff.png)

**Point**
アイコンの重なりは spacingをマイナス値にすることで実現しました。
また、一番左のアイコンを最上階層にする方法として、semanticを`Force Right-to-Left` にしました。
![スクリーンショット 2022-12-13 4.18.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/6663978e-bd05-31eb-0058-b90fd818e422.png)

立体で見るとこういう感じになります。
![スクリーンショット 2022-12-13 4.57.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999511/145eaae9-0d57-614d-1d9a-93402f89a9fe.png)

## Model
PostとUserの２つのModelを用意します。
```swift
import Foundation

struct Post {
    let likedUsers: [User]
}

struct User {
    let id: String
    let icon: URL?
}
```

## UITableViewCell コード
いいねしたユーザー数によって表示するアイコン数の制御や、画像のバインドなどを行います。
```swift
import Kingfisher
import UIKit

final class PostTableViewCell: UITableViewCell {
    // MARK: - Properties
    @IBOutlet var likedUsersStackView: UIStackView!
    @IBOutlet var rightIconStackView: UIStackView!
    @IBOutlet var rightIconImageView: UIImageView! {
        didSet {
            rightIconImageView.layer.cornerRadius = rightIconImageView.frame.width / 2
            rightIconImageView.layer.borderWidth = 2
            rightIconImageView.layer.borderColor = UIColor.white.cgColor
        }
    }
    @IBOutlet var centerIconStackView: UIStackView!
    @IBOutlet var centerIconImageView: UIImageView! {
        didSet {
            centerIconImageView.layer.cornerRadius = centerIconImageView.frame.width / 2
            centerIconImageView.layer.borderWidth = 2
            centerIconImageView.layer.borderColor = UIColor.white.cgColor
        }
    }
    @IBOutlet var leftIconImageView: UIImageView! {
        didSet {
            leftIconImageView.layer.cornerRadius = leftIconImageView.frame.width / 2
            leftIconImageView.layer.borderWidth = 2
            leftIconImageView.layer.borderColor = UIColor.white.cgColor
        }
    }
    @IBOutlet var likedUserIDLabel: UILabel! // "travel_lover"部分のラベル
    @IBOutlet var andMoreLabel: UILabel! // "、他"部分のラベル
    
    func configure(post: Post) {
        let users = post.likedUsers
        likedUsersStackView.isHidden = users.isEmpty
        guard !users.isEmpty else { return }
        // ラベル
        likedUserIDLabel.text = users.first?.id
        andMoreLabel.isHidden = users.count < 2
        // アイコンをplaceholderイメージにリセット
        leftIconImageView.image = UIImage(named: "ic_avatar") // 任意のイメージ名
        centerIconImageView.image = UIImage(named: "ic_avatar")
        rightIconImageView.image = UIImage(named: "ic_avatar")
        // IconStackView
        centerIconStackView.isHidden = users.count < 2
        rightIconStackView.isHidden = users.count < 3
        
        // アイコンのURLがある時だけ画像バインド。 users配列の要素を左から順に表示していく。
        users.enumerated().forEach { index, user in
            guard let imageURL = user.icon else { return }
            switch index {
            case 0:
                leftIconImageView.kf.setImage(with: imageURL)
            case 1:
                centerIconImageView.kf.setImage(with: imageURL)
            case 2:
                rightIconImageView.kf.setImage(with: imageURL)
            default:
                return
            }
        }
    }
}
```

あとは、TableViewControllerを用意し、UITableViewDataSource の`cellForRowAt`にて上記コードで作成したPostTableViewCellのconfigureを呼びます。
configureの引数のPost型には、Mockデータを渡して動作確認しました。

## Mock
Mockは、いいねしたユーザー数や、ユーザーのアイコン設定状況が異なる計6件のデータを用意しました。

```swift
        // Icon
        let iconURL1 = URL(string: "https://www.jpeg") // 任意の画像URL
        let iconURL2 = URL(string: "https://xxx.jpeg")
        let iconURL3 = URL(string: "https://yyy.jpeg")
        let iconURL4 = URL(string: "https://zzz.jpeg")
        // User
        let user1 = User(id: "piyopiyo_bowwow", icon: iconURL1) // 任意のIDと画像
        let user2 = User(id: "akai_shikaku", icon: iconURL2)
        let user3 = User(id: "fal_con_1213", icon: iconURL3)
        let user4 = User(id: "big_sankaku", icon: iconURL4)
        let user5 = User(id: "unkown", icon: nil)
        // Post
        let post1 = Post(likedUsers: [user1])
        let post2 = Post(likedUsers: [user2, user1])
        let post3 = Post(likedUsers: [user3, user2, user1])
        let post4 = Post(likedUsers: [])
        let post5 = Post(likedUsers: [user5, user4])
        let post6 = Post(likedUsers: [user4, user3, user2, user1])
        posts = [post1, post2, post3, post4, post5, post6]

```

# できたもの
実行した結果はこんな感じです。
MockのlikedUsersの件数によってアイコンの表示数が変化するViewができました🙌
<iframe width="100%" height="1208" src="https://www.youtube.com/embed/g3MMZeM9fZQ" title="[デモ] インスタクローンアプリ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 最後に
普段何気なく見ているSNSですが、細かいところまで凝っていたりして、これどうやって作るんだろう？と思うことばかりです。UI組むのは好きなので色々試して少しづつ解明していきたいですね！

## Credit
- hart icon
<a target="_blank" href="https://icons8.com/icon/eLKNCaDTTdcb/favorite">Favorite</a> icon by <a target="_blank" href="https://icons8.com">Icons8</a>
- icon placeholder image
Image by <a href="https://pixabay.com/users/wingtilldie-3058071/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1577909">WingTillDie</a> from <a href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1577909">Pixabay</a>