😎
【SwiftUI】 TagListViewの実装
はじめに
以下のようなよくあるタグをリスト形式で表示する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()
}
}
おわりに
実装する際に色々と調べてみると何パターンか実装方法がありましたが、今回は以下のライブラリを参考に実装してみました。
iOS16以降ではLayoutというものを使っても実装できるみたいなので、今度はそっちを使って実装してみたいと思います。
参考
Discussion