4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SwiftUI】外観カスタマイズ可能なタグリストの実装

Posted at

概要

フロントエンドやアプリによくあるフレーム幅に合わせて良い感じに改行してくれるタグリストをSwiftUIで実装したかったのですが、既存のライブラリではやれることに限度があったのでより柔軟にUIを実装できるライブラリを作成しました。

IMG_A5504AD6EFF9-1.jpeg
※画像はマダミスアプリ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に集約することで先の添付画像のようなタグの並びを実現しています。

SwiftUICustomTagListView.swift
/// 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

世界中からアクセスされる共通リソースにコミットすると聞くと高い心理的障壁を感じますが、作業自体は思ったより難しくはないので興味のある方はぜひトライしてみてください。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?