概要
フロントエンドやアプリによくあるフレーム幅に合わせて良い感じに改行してくれるタグリストをSwiftUIで実装したかったのですが、既存のライブラリではやれることに限度があったのでより柔軟にUIを実装できるライブラリを作成しました。
※画像はマダミスアプリUZUより。Flutter製なのであくまでイメージです。
完成物 & 使い方
インストール
Cocoapods
pod 'SwiftUICustomTagListView'
SwiftPackageManager
Xcode
-> File
-> Add Packages...
-> SwiftUICustomTagListView
or https://github.com/chitomo12/SwiftUICustomTagListView
で検索 -> Add Package
実装サンプル
import SwiftUICustomTagListView
struct SampleView: View {
let data: [SampleTagViewData] = [
.init(text: "#Technology", color: Color(hex: "#ff4d4d")),
.init(text: "#News", color: Color(hex: "#b33636")),
.init(text: "#Politics", color: Color(hex: "#ff944d")),
.init(text: "#Breaking", color: Color(hex: "#ff4dd3")),
.init(text: "#Global", color: Color(hex: "#b33693")),
]
var views: [SwiftUICustomTagView<SampleTagView>] {
self.data.map { data in
SwiftUICustomTagView(content: {
SampleTagView(data: data)
})
}
}
var body: some View {
SwiftUICustomTagListView(views, horizontalSpace: 8, verticalSpace: 8)
.frame(width: 240, height: 220)
}
}
// MARK: - Define your own component
struct SampleTagView: View {
let data: SampleTagViewData
var body: some View {
Text(data.text)
.font(.title2)
.onTapGesture {
print("[Pressed] \(data.text)")
}
.foregroundColor(.white)
.padding(.all, 8)
.background(LinearGradient(
gradient: Gradient(colors: [data.color, data.color.opacity(0.6)]),
startPoint: .top,
endPoint: .bottom))
.cornerRadius(7)
}
}
struct SampleTagViewData {
let text: String
let color: Color
}
実装
タグのサイズ、位置を算出し、改行するかなどの判定は.alignmentGuide
モディファイアを使って実現しています。Reona様の「タグを自動的に改行させる」のコードを土台に作成しました。
alignmentGuide
はViewの位置をそのView自体のサイズに合わせて操作することのできるモディファイアです。詳しい説明はここでは割愛します。
// ref: https://reona.dev/posts/20200929
private func generateTags(_ geometry: GeometryProxy,
views: [SwiftUICustomTagView<Content>]) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(views, id: \.self) { view in
view
.padding(.trailing, horizontalSpace)
.alignmentGuide(.leading, computeValue: { dimension in
if abs(width - dimension.width) > geometry.size.width {
width = 0
height -= dimension.height + verticalSpace
}
let result = width
if view == views.last {
width = 0
} else {
width -= dimension.width
}
return result
})
.alignmentGuide(.top, computeValue: { dimension in
let result = height
if view == views.last {
height = 0
}
return result
})
}
}
}
ライブラリのコアにあたるSwiftUICustomTagListView
では、ジェネリックなViewを包括したSwiftUICustomTagView
の配列を受け取り、これを上記generateTags
で一つのZStackに集約することで先の添付画像のようなタグの並びを実現しています。
/// A SwiftUI view that presents a customizable tag list.
public struct SwiftUICustomTagListView<Content: View>: View {
/// An array of Tag View
private let tagViews: [SwiftUICustomTagView<Content>]
/// Horizontal space between each tag
private let horizontalSpace: CGFloat
/// Vertical space between each tag
private let verticalSpace: CGFloat
@State private var listHeight: CGFloat = 0
public init(_ views: [SwiftUICustomTagView<Content>],
horizontalSpace: CGFloat,
verticalSpace: CGFloat) {
self.tagViews = views
self.horizontalSpace = horizontalSpace
self.verticalSpace = verticalSpace
}
public var body: some View {
GeometryReader { geometry in
generateTags(geometry, views: tagViews)
.background(GeometryReader { geo in
Color(.clear)
.onAppear {
self.listHeight = geo.size.height
}
})
}
.frame(height: listHeight)
}
...
おまけ
そこそこ汎用的なライブラリになっている気がしたので、思い切ってCocoapods、Swift-Package-Indexにも登録してみました。これについては巷にHowTo記事が沢山あるので参考した記事の紹介に留めます(どちらもuhooi様の記事です🙏)
Cocoapodsへの公開
Swift Package Index
世界中からアクセスされる共通リソースにコミットすると聞くと高い心理的障壁を感じますが、作業自体は思ったより難しくはないので興味のある方はぜひトライしてみてください。