├── LICENSE ├── README.md └── TagView.swift /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ahmadreza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI TagView 2 | Creating a simple selectable tag view in SwiftUI is quite a challenge. here is a simple & elegant example of it. 3 | 4 | ![Output Example](https://i.stack.imgur.com/1ruBt.png) 5 | 6 | **Usage:** 7 | 8 | Just copy the TagView.swift file in your project & write your tags like so: 9 | 10 | > TagView(tags: [TagViewItem(title: "cat", isSelected: false), TagViewItem(title: "dog", isSelected: true)]) 11 | -------------------------------------------------------------------------------- /TagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagView.swift 3 | // EventApp 4 | // 5 | // Created by Ahmadreza on 10/15/21. 6 | // Copyright © 2021 Alexani. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TagViewItem: Hashable { 12 | 13 | var title: String 14 | var isSelected: Bool 15 | 16 | static func == (lhs: TagViewItem, rhs: TagViewItem) -> Bool { 17 | return lhs.isSelected == rhs.isSelected 18 | } 19 | 20 | func hash(into hasher: inout Hasher) { 21 | hasher.combine(title) 22 | hasher.combine(isSelected) 23 | } 24 | } 25 | 26 | struct TagView: View { 27 | @State var tags: [TagViewItem] 28 | @State private var totalHeight = CGFloat.zero // << variant for ScrollView/List // = CGFloat.infinity // << variant for VStack 29 | var body: some View { 30 | VStack { 31 | GeometryReader { geometry in 32 | self.generateContent(in: geometry) 33 | } 34 | } 35 | .frame(height: totalHeight)// << variant for ScrollView/List 36 | //.frame(maxHeight: totalHeight) // << variant for VStack 37 | } 38 | 39 | private func generateContent(in g: GeometryProxy) -> some View { 40 | var width = CGFloat.zero 41 | var height = CGFloat.zero 42 | return ZStack(alignment: .topLeading) { 43 | ForEach(tags.indices) { index in 44 | item(for: tags[index].title, isSelected: tags[index].isSelected) 45 | .padding([.horizontal, .vertical], 4) 46 | .alignmentGuide(.leading, computeValue: { d in 47 | if (abs(width - d.width) > g.size.width) { 48 | width = 0 49 | height -= d.height 50 | } 51 | let result = width 52 | if tags[index].title == self.tags.last!.title { 53 | width = 0 //last item 54 | } else { 55 | width -= d.width 56 | } 57 | return result 58 | }) 59 | .alignmentGuide(.top, computeValue: {d in 60 | let result = height 61 | if tags[index].title == self.tags.last!.title { 62 | height = 0 // last item 63 | } 64 | return result 65 | }).onTapGesture { 66 | tags[index].isSelected.toggle() 67 | } 68 | } 69 | }.background(viewHeightReader($totalHeight)) 70 | } 71 | 72 | private func item(for text: String, isSelected: Bool) -> some View { 73 | Text(text) 74 | .foregroundColor(isSelected ? Colors.primaryBarBackground : Colors.textColor) 75 | .padding() 76 | .lineLimit(1) 77 | .background(isSelected ? Colors.primaryBlue : Colors.primaryBarBackground) 78 | .frame(height: 36) 79 | .cornerRadius(18) 80 | .overlay(Capsule().stroke(Colors.primaryBlue, lineWidth: 1)) 81 | } 82 | 83 | private func viewHeightReader(_ binding: Binding) -> some View { 84 | return GeometryReader { geometry -> Color in 85 | let rect = geometry.frame(in: .local) 86 | DispatchQueue.main.async { 87 | binding.wrappedValue = rect.size.height 88 | } 89 | return .clear 90 | } 91 | } 92 | } 93 | --------------------------------------------------------------------------------