@@ -7,7 +7,9 @@ struct TagsView: View {
7
7
@ObservedObject var viewModel : TagsViewModel
8
8
9
9
var body : some View {
10
- Group {
10
+ VStack ( alignment: . leading, spacing: 0 ) {
11
+ SelectedTagsView ( viewModel: viewModel)
12
+
11
13
if !viewModel. searchText. isEmpty {
12
14
TagsSearchView ( viewModel: viewModel)
13
15
} else {
@@ -27,7 +29,7 @@ private struct TagsListView: View {
27
29
List {
28
30
if let response = viewModel. response {
29
31
DataViewPaginatedForEach ( response: response) { tag in
30
- TagRowView ( tag: tag)
32
+ TagRowView ( tag: tag, viewModel : viewModel )
31
33
}
32
34
}
33
35
}
@@ -67,28 +69,153 @@ private struct TagsSearchView: View {
67
69
search: viewModel. search
68
70
) { response in
69
71
DataViewPaginatedForEach ( response: response) { tag in
70
- TagRowView ( tag: tag)
72
+ TagRowView ( tag: tag, viewModel : viewModel )
71
73
}
72
74
}
73
75
}
74
76
}
75
77
76
78
private struct TagsPaginatedForEach : View {
77
79
@ObservedObject var response : TagsPaginatedResponse
80
+ @ObservedObject var viewModel : TagsViewModel
78
81
79
82
var body : some View {
80
83
DataViewPaginatedForEach ( response: response) { tag in
81
- TagRowView ( tag: tag)
84
+ TagRowView ( tag: tag, viewModel: viewModel)
85
+ }
86
+ }
87
+ }
88
+
89
+ private struct FlowLayout : Layout {
90
+ let spacing : CGFloat
91
+
92
+ init ( spacing: CGFloat ) {
93
+ self . spacing = spacing
94
+ }
95
+
96
+ func sizeThatFits( proposal: ProposedViewSize , subviews: Subviews , cache: inout ( ) ) -> CGSize {
97
+ let rows = arrangeRows ( proposal: proposal, subviews: subviews)
98
+ let width = proposal. width ?? 0
99
+ let height = rows. reduce ( 0 ) { result, row in
100
+ let rowHeight = row. map { $0. dimensions ( in: . unspecified) . height } . max ( ) ?? 0
101
+ return result + rowHeight + ( result > 0 ? spacing : 0 )
102
+ }
103
+ return CGSize ( width: width, height: height)
104
+ }
105
+
106
+ func placeSubviews( in bounds: CGRect , proposal: ProposedViewSize , subviews: Subviews , cache: inout ( ) ) {
107
+ let rows = arrangeRows ( proposal: proposal, subviews: subviews)
108
+ var y = bounds. minY
109
+
110
+ for row in rows {
111
+ var x = bounds. minX
112
+ let rowHeight = row. map { $0. dimensions ( in: . unspecified) . height } . max ( ) ?? 0
113
+
114
+ for subview in row {
115
+ subview. place ( at: CGPoint ( x: x, y: y) , proposal: ProposedViewSize ( subview. sizeThatFits ( . unspecified) ) )
116
+ x += subview. dimensions ( in: . unspecified) . width + spacing
117
+ }
118
+ y += rowHeight + spacing
82
119
}
83
120
}
121
+
122
+ private func arrangeRows( proposal: ProposedViewSize , subviews: Subviews ) -> [ [ LayoutSubviews . Element ] ] {
123
+ let availableWidth = proposal. width ?? . infinity
124
+ var rows : [ [ LayoutSubviews . Element ] ] = [ ]
125
+ var currentRow : [ LayoutSubviews . Element ] = [ ]
126
+ var currentWidth : CGFloat = 0
127
+
128
+ for subview in subviews {
129
+ let subviewWidth = subview. dimensions ( in: . unspecified) . width
130
+
131
+ if currentWidth + subviewWidth <= availableWidth || currentRow. isEmpty {
132
+ currentRow. append ( subview)
133
+ currentWidth += subviewWidth + ( currentRow. count > 1 ? spacing : 0 )
134
+ } else {
135
+ rows. append ( currentRow)
136
+ currentRow = [ subview]
137
+ currentWidth = subviewWidth
138
+ }
139
+ }
140
+
141
+ if !currentRow. isEmpty {
142
+ rows. append ( currentRow)
143
+ }
144
+
145
+ return rows
146
+ }
147
+ }
148
+
149
+ private struct SelectedTagsView : View {
150
+ @ObservedObject var viewModel : TagsViewModel
151
+
152
+ var body : some View {
153
+ VStack ( alignment: . leading, spacing: 4 ) {
154
+ if !viewModel. selectedTags. isEmpty {
155
+ FlowLayout ( spacing: 8 ) {
156
+ ForEach ( viewModel. selectedTags, id: \. self) { tagName in
157
+ SelectedTag ( tagName: tagName) {
158
+ viewModel. removeSelectedTag ( tagName)
159
+ }
160
+ }
161
+ }
162
+ . padding ( . horizontal)
163
+ } else {
164
+ Text ( Strings . noTagsSelected)
165
+ . font ( . body)
166
+ . foregroundColor ( . secondary)
167
+ . padding ( . horizontal)
168
+ }
169
+ }
170
+ . padding ( . top, 8 )
171
+ . padding ( . bottom, 4 )
172
+ }
173
+ }
174
+
175
+ private struct SelectedTag : View {
176
+ let tagName : String
177
+ let onRemove : ( ) -> Void
178
+
179
+ var body : some View {
180
+ HStack ( spacing: 4 ) {
181
+ Text ( tagName)
182
+ . font ( . caption)
183
+ . foregroundColor ( . primary)
184
+ . lineLimit ( 1 )
185
+
186
+ Button ( action: onRemove) {
187
+ Image ( systemName: " xmark.circle.fill " )
188
+ . foregroundColor ( . secondary)
189
+ . font ( . caption)
190
+ }
191
+ }
192
+ . padding ( . horizontal, 8 )
193
+ . padding ( . vertical, 4 )
194
+ . background ( Color ( UIColor . systemGray5) )
195
+ . clipShape ( Capsule ( ) )
196
+ }
84
197
}
85
198
86
199
private struct TagRowView : View {
87
200
let tag : RemotePostTag
201
+ @ObservedObject var viewModel : TagsViewModel
88
202
89
203
var body : some View {
90
- Text ( tag. name ?? " " )
91
- . font ( . body)
204
+ HStack {
205
+ Text ( tag. name ?? " " )
206
+ . font ( . body)
207
+
208
+ Spacer ( )
209
+
210
+ if viewModel. isSelected ( tag) {
211
+ Image ( systemName: " checkmark " )
212
+ . foregroundColor ( . accentColor)
213
+ }
214
+ }
215
+ . contentShape ( Rectangle ( ) )
216
+ . onTapGesture {
217
+ viewModel. toggleSelection ( for: tag)
218
+ }
92
219
}
93
220
}
94
221
@@ -110,13 +237,19 @@ private enum Strings {
110
237
value: " Tags help organize your content and make it easier for readers to find related posts. " ,
111
238
comment: " Description for empty state when there are no tags "
112
239
)
240
+
241
+ static let noTagsSelected = NSLocalizedString (
242
+ " tags.selected.empty " ,
243
+ value: " No tags are selected " ,
244
+ comment: " Message shown when no tags are selected "
245
+ )
113
246
}
114
247
115
248
class TagsViewController : UIHostingController < TagsView > {
116
249
let viewModel : TagsViewModel
117
250
118
- init ( blog: Blog ) {
119
- viewModel = TagsViewModel ( blog: blog)
251
+ init ( blog: Blog , selectedTags : String ? = nil , onSelectedTagsChanged : ( ( String ) -> Void ) ? = nil ) {
252
+ viewModel = TagsViewModel ( blog: blog, selectedTags : selectedTags , onSelectedTagsChanged : onSelectedTagsChanged )
120
253
super. init ( rootView: . init( viewModel: viewModel) )
121
254
}
122
255
0 commit comments