Skip to content

Commit 3cfb859

Browse files
committed
Add "Selected Tags" to TagsView
1 parent f0bdcae commit 3cfb859

File tree

3 files changed

+175
-14
lines changed

3 files changed

+175
-14
lines changed

WordPress/Classes/ViewRelated/Tags/TagsService.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@ import Foundation
22
import WordPressKit
33
import WordPressData
44

5-
@MainActor
65
class TagsService {
7-
private let blog: Blog
86
private let remote: TaxonomyServiceRemote?
97

108
init(blog: Blog) {
11-
self.blog = blog
129
self.remote = Self.createRemote(for: blog)
1310
}
1411

WordPress/Classes/ViewRelated/Tags/TagsView.swift

Lines changed: 141 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ struct TagsView: View {
77
@ObservedObject var viewModel: TagsViewModel
88

99
var body: some View {
10-
Group {
10+
VStack(alignment: .leading, spacing: 0) {
11+
SelectedTagsView(viewModel: viewModel)
12+
1113
if !viewModel.searchText.isEmpty {
1214
TagsSearchView(viewModel: viewModel)
1315
} else {
@@ -27,7 +29,7 @@ private struct TagsListView: View {
2729
List {
2830
if let response = viewModel.response {
2931
DataViewPaginatedForEach(response: response) { tag in
30-
TagRowView(tag: tag)
32+
TagRowView(tag: tag, viewModel: viewModel)
3133
}
3234
}
3335
}
@@ -67,28 +69,153 @@ private struct TagsSearchView: View {
6769
search: viewModel.search
6870
) { response in
6971
DataViewPaginatedForEach(response: response) { tag in
70-
TagRowView(tag: tag)
72+
TagRowView(tag: tag, viewModel: viewModel)
7173
}
7274
}
7375
}
7476
}
7577

7678
private struct TagsPaginatedForEach: View {
7779
@ObservedObject var response: TagsPaginatedResponse
80+
@ObservedObject var viewModel: TagsViewModel
7881

7982
var body: some View {
8083
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
82119
}
83120
}
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+
}
84197
}
85198

86199
private struct TagRowView: View {
87200
let tag: RemotePostTag
201+
@ObservedObject var viewModel: TagsViewModel
88202

89203
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+
}
92219
}
93220
}
94221

@@ -110,13 +237,19 @@ private enum Strings {
110237
value: "Tags help organize your content and make it easier for readers to find related posts.",
111238
comment: "Description for empty state when there are no tags"
112239
)
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+
)
113246
}
114247

115248
class TagsViewController: UIHostingController<TagsView> {
116249
let viewModel: TagsViewModel
117250

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)
120253
super.init(rootView: .init(viewModel: viewModel))
121254
}
122255

WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,23 @@ class TagsViewModel: ObservableObject {
1111
@Published var response: TagsPaginatedResponse?
1212
@Published var isLoading = false
1313
@Published var error: Error?
14+
@Published private(set) var selectedTags: [String] = [] {
15+
didSet {
16+
onSelectedTagsChanged?(selectedTags.joined(separator: ", "))
17+
}
18+
}
19+
private var selectedTagsSet: Set<String> = []
1420

15-
let blog: Blog
1621
private let tagsService: TagsService
22+
var onSelectedTagsChanged: ((String) -> Void)?
1723

18-
init(blog: Blog) {
19-
self.blog = blog
24+
init(blog: Blog, selectedTags: String? = nil, onSelectedTagsChanged: ((String) -> Void)? = nil) {
2025
self.tagsService = TagsService(blog: blog)
26+
self.selectedTags = selectedTags?.split(separator: ",").map {
27+
$0.trimmingCharacters(in: .whitespacesAndNewlines)
28+
} ?? []
29+
self.selectedTagsSet = Set(self.selectedTags)
30+
self.onSelectedTagsChanged = onSelectedTagsChanged
2131
}
2232

2333
func onAppear() {
@@ -78,4 +88,25 @@ class TagsViewModel: ObservableObject {
7888
)
7989
}
8090
}
91+
92+
func toggleSelection(for tag: RemotePostTag) {
93+
guard let tagName = tag.name else { return }
94+
if selectedTagsSet.contains(tagName) {
95+
selectedTagsSet.remove(tagName)
96+
selectedTags.removeAll { $0 == tagName }
97+
} else {
98+
selectedTagsSet.insert(tagName)
99+
selectedTags.append(tagName)
100+
}
101+
}
102+
103+
func isSelected(_ tag: RemotePostTag) -> Bool {
104+
guard let tagName = tag.name else { return false }
105+
return selectedTagsSet.contains(tagName)
106+
}
107+
108+
func removeSelectedTag(_ tagName: String) {
109+
selectedTagsSet.remove(tagName)
110+
selectedTags.removeAll { $0 == tagName }
111+
}
81112
}

0 commit comments

Comments
 (0)