😎

【SwiftUI】 TagListViewの実装

2023/08/13に公開

はじめに

以下のようなよくあるタグをリスト形式で表示するViewを実装してみました。
ライブラリをそのまま使っても良かったのですが、勉強がてらライブラリを参考にしつつ実装してみたので、その方法をまとめようと思います。

実装方法

今回の実装方法では、ZStackとAlignmentGuideを使ってタグ同士が重ならないようにタグの位置をずらして配置することでタグをリスト形式で表示するViewを実現しています。
また、同じ列に並んでいるタグの横幅の合計値とGeometryReaderで取得したViewの横幅を比較して、タグがViewの横幅を超えていたら、次の行に配置されるようにしています。

TagListView
import SwiftUI

struct TagListView<Data: Collection, Content: View>: View where Data.Element: Hashable {

    /// アイテムのリスト
    let items: Data
    /// アイテム同士のスペース(横)
    let horizontalSpacing: CGFloat
    /// アイテム同士のスペース(縦)
    let verticalSpacing: CGFloat
    /// アイテムのView(ex: タグ)
    let content: (Data.Element) -> Content

    var body: some View {
        GeometryReader { geometory in
            tagListView(in: geometory)
        }
    }

    private func tagListView(in geometory: GeometryProxy) -> some View {
        var width: CGFloat = .zero
        var height: CGFloat = .zero
        var lastHeight: CGFloat = .zero

        return ZStack(alignment: .topLeading) {
            ForEach(Array(items.enumerated()), id: \.offset) { index, item in
                content(item)
                    .padding(.horizontal, horizontalSpacing)
                    .padding(.vertical, verticalSpacing)
                    .alignmentGuide(.leading) { d in
                        if abs(width - d.width) > geometory.size.width {
                            // 同じ行の1つ前までのタグの横幅と追加対象のタグの横幅の合計がViewの横幅を超えている場合は、widthをリセットして改行
                            width = 0
                            height -= lastHeight
                        }

                        lastHeight = d.height
                        let result = width

                        if index == items.count - 1 {
                            // Tagが最後の場合はwidthをリセットする
                            // ForEachが複数回呼ばれるためリセットしないと意図した表示にならない(複数回呼ばれる理由は分かってない。。。)
                            width = 0
                        } else {
                            // タグの幅をwidthに追加する
                            width -= d.width
                        }
                        return result
                    }
                    .alignmentGuide(.top) { _ in
                        let result = height

                        if index == items.count - 1 {
                            // Tagが最後の場合はheightをリセットする
                            // ForEachが複数回呼ばれるためリセットしないと意図した表示にならない(複数回呼ばれる理由は分かってない。。。)
                            height = 0
                        }
                        return result
                    }
            }
        }
    }
}

使い方

ContentView
import SwiftUI

struct ContentView: View {

    let tags: [Tag] = [
        .init(title: "Swift"),
        .init(title: "SwiftUI"),
        .init(title: "UIKit"),
        .init(title: "Xcode"),
        .init(title: "Apple"),
        .init(title: "iOS"),
        .init(title: "iPad"),
    ]

    var body: some View {
        content()
    }

    private func content() -> some View {
        TagListView(items: tags, horizontalSpacing: 4, verticalSpacing: 4) { item in
            tagItemView(for: item)
        }
        .frame(height: 500)
    }

    private func tagItemView(for tag: Tag) -> some View {
        Button {
            // TODO: Button Action
        } label: {
            Text(tag.title)
                .foregroundColor(.primary)
                .padding()
                .lineLimit(1)
                .background(Color.secondary)
                .frame(height: 36)
                .cornerRadius(18)
                .overlay(
                    Capsule()
                        .stroke(Color.secondary, lineWidth: 1)
                )
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

おわりに

実装する際に色々と調べてみると何パターンか実装方法がありましたが、今回は以下のライブラリを参考に実装してみました。
https://github.com/danielsaidi/TagKit/tree/main

iOS16以降ではLayoutというものを使っても実装できるみたいなので、今度はそっちを使って実装してみたいと思います。

参考

https://github.com/danielsaidi/TagKit/tree/main
https://medium.com/geekculture/tags-view-in-swiftui-e47dc6ce52e8
https://qiita.com/SNQ-2001/items/d2e752bfd5f25cedfce7
https://qiita.com/shiz/items/0c41d20a2cb7b0748875#verticalalignment

Discussion