Skip to content

Commit 8a6a947

Browse files
authored
New tags list using Data View (#24668)
1 parent 45f2f13 commit 8a6a947

File tree

4 files changed

+281
-0
lines changed

4 files changed

+281
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
import WordPressKit
3+
4+
extension RemotePostTag: @retroactive Identifiable {
5+
public var id: Int {
6+
return tagID?.intValue ?? 0
7+
}
8+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Foundation
2+
import WordPressKit
3+
import WordPressData
4+
5+
@MainActor
6+
class TagsService {
7+
private let blog: Blog
8+
private let remote: TaxonomyServiceRemote?
9+
10+
init(blog: Blog) {
11+
self.blog = blog
12+
self.remote = Self.createRemote(for: blog)
13+
}
14+
15+
private static func createRemote(for blog: Blog) -> TaxonomyServiceRemote? {
16+
if let siteID = blog.dotComID, let api = blog.wordPressComRestApi {
17+
return TaxonomyServiceRemoteREST(wordPressComRestApi: api, siteID: siteID)
18+
}
19+
20+
if let username = blog.username, let password = blog.password, let xmlrpcApi = blog.xmlrpcApi {
21+
return TaxonomyServiceRemoteXMLRPC(api: xmlrpcApi, username: username, password: password)
22+
}
23+
24+
return nil
25+
}
26+
27+
func getTags(number: Int = 100, offset: Int = 0) async throws -> [RemotePostTag] {
28+
guard let remote else {
29+
throw TagsServiceError.noRemoteService
30+
}
31+
32+
let paging = RemoteTaxonomyPaging()
33+
paging.number = NSNumber(value: number)
34+
paging.offset = NSNumber(value: offset)
35+
36+
return try await withCheckedThrowingContinuation { continuation in
37+
remote.getTagsWith(paging, success: { remoteTags in
38+
continuation.resume(returning: remoteTags)
39+
}, failure: { error in
40+
continuation.resume(throwing: error)
41+
})
42+
}
43+
}
44+
45+
func searchTags(with query: String) async throws -> [RemotePostTag] {
46+
guard let remote else {
47+
throw TagsServiceError.noRemoteService
48+
}
49+
50+
guard !query.isEmpty else {
51+
return []
52+
}
53+
54+
return try await withCheckedThrowingContinuation { continuation in
55+
remote.searchTags(withName: query, success: { remoteTags in
56+
continuation.resume(returning: remoteTags)
57+
}, failure: { error in
58+
continuation.resume(throwing: error)
59+
})
60+
}
61+
}
62+
}
63+
64+
enum TagsServiceError: Error {
65+
case noRemoteService
66+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import SwiftUI
2+
import WordPressUI
3+
import WordPressKit
4+
import WordPressData
5+
6+
struct TagsView: View {
7+
@ObservedObject var viewModel: TagsViewModel
8+
9+
var body: some View {
10+
Group {
11+
if !viewModel.searchText.isEmpty {
12+
TagsSearchView(viewModel: viewModel)
13+
} else {
14+
TagsListView(viewModel: viewModel)
15+
}
16+
}
17+
.navigationTitle(Strings.title)
18+
.searchable(text: $viewModel.searchText)
19+
.textInputAutocapitalization(.never)
20+
}
21+
}
22+
23+
private struct TagsListView: View {
24+
@ObservedObject var viewModel: TagsViewModel
25+
26+
var body: some View {
27+
List {
28+
if let response = viewModel.response {
29+
DataViewPaginatedForEach(response: response) { tag in
30+
TagRowView(tag: tag)
31+
}
32+
}
33+
}
34+
.listStyle(.plain)
35+
.overlay {
36+
if let response = viewModel.response {
37+
if response.isEmpty {
38+
EmptyStateView(
39+
Strings.empty,
40+
systemImage: "tag",
41+
description: Strings.emptyDescription
42+
)
43+
}
44+
} else if viewModel.isLoading {
45+
ProgressView()
46+
} else if let error = viewModel.error {
47+
EmptyStateView.failure(error: error) {
48+
Task { await viewModel.refresh() }
49+
}
50+
}
51+
}
52+
.onAppear {
53+
viewModel.onAppear()
54+
}
55+
.refreshable {
56+
await viewModel.refresh()
57+
}
58+
}
59+
}
60+
61+
private struct TagsSearchView: View {
62+
@ObservedObject var viewModel: TagsViewModel
63+
64+
var body: some View {
65+
DataViewSearchView(
66+
searchText: viewModel.searchText,
67+
search: viewModel.search
68+
) { response in
69+
DataViewPaginatedForEach(response: response) { tag in
70+
TagRowView(tag: tag)
71+
}
72+
}
73+
}
74+
}
75+
76+
private struct TagsPaginatedForEach: View {
77+
@ObservedObject var response: TagsPaginatedResponse
78+
79+
var body: some View {
80+
DataViewPaginatedForEach(response: response) { tag in
81+
TagRowView(tag: tag)
82+
}
83+
}
84+
}
85+
86+
private struct TagRowView: View {
87+
let tag: RemotePostTag
88+
89+
var body: some View {
90+
Text(tag.name ?? "")
91+
.font(.body)
92+
}
93+
}
94+
95+
private enum Strings {
96+
static let title = NSLocalizedString(
97+
"tags.title",
98+
value: "Tags",
99+
comment: "Title for the tags screen"
100+
)
101+
102+
static let empty = NSLocalizedString(
103+
"tags.empty.title",
104+
value: "No Tags",
105+
comment: "Title for empty state when there are no tags"
106+
)
107+
108+
static let emptyDescription = NSLocalizedString(
109+
"tags.empty.description",
110+
value: "Tags help organize your content and make it easier for readers to find related posts.",
111+
comment: "Description for empty state when there are no tags"
112+
)
113+
}
114+
115+
class TagsViewController: UIHostingController<TagsView> {
116+
let viewModel: TagsViewModel
117+
118+
init(blog: Blog) {
119+
viewModel = TagsViewModel(blog: blog)
120+
super.init(rootView: .init(viewModel: viewModel))
121+
}
122+
123+
@MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) {
124+
fatalError("init(coder:) has not been implemented")
125+
}
126+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import Foundation
2+
import WordPressKit
3+
import WordPressData
4+
import WordPressUI
5+
6+
typealias TagsPaginatedResponse = DataViewPaginatedResponse<RemotePostTag, Int>
7+
8+
@MainActor
9+
class TagsViewModel: ObservableObject {
10+
@Published var searchText = ""
11+
@Published var response: TagsPaginatedResponse?
12+
@Published var isLoading = false
13+
@Published var error: Error?
14+
15+
let blog: Blog
16+
private let tagsService: TagsService
17+
18+
init(blog: Blog) {
19+
self.blog = blog
20+
self.tagsService = TagsService(blog: blog)
21+
}
22+
23+
func onAppear() {
24+
guard response == nil else { return }
25+
Task {
26+
await loadInitialTags()
27+
}
28+
}
29+
30+
@MainActor
31+
func refresh() async {
32+
response = nil
33+
error = nil
34+
await loadInitialTags()
35+
}
36+
37+
private func loadInitialTags() async {
38+
isLoading = true
39+
defer { isLoading = false}
40+
41+
error = nil
42+
43+
do {
44+
let paginatedResponse = try await TagsPaginatedResponse { [weak self] pageIndex in
45+
guard let self else {
46+
throw TagsServiceError.noRemoteService
47+
}
48+
49+
let offset = pageIndex ?? 0
50+
let remoteTags = try await self.tagsService.getTags(number: 100, offset: offset)
51+
52+
let hasMore = remoteTags.count == 100
53+
let nextPage = hasMore ? offset + 100 : nil
54+
55+
return TagsPaginatedResponse.Page(
56+
items: remoteTags,
57+
total: nil,
58+
hasMore: hasMore,
59+
nextPage: nextPage
60+
)
61+
}
62+
63+
self.response = paginatedResponse
64+
} catch {
65+
self.error = error
66+
}
67+
}
68+
69+
func search() async throws -> DataViewPaginatedResponse<RemotePostTag, Int> {
70+
let remoteTags = try await tagsService.searchTags(with: searchText)
71+
72+
return try await DataViewPaginatedResponse { _ in
73+
return DataViewPaginatedResponse.Page(
74+
items: remoteTags,
75+
total: remoteTags.count,
76+
hasMore: false,
77+
nextPage: nil
78+
)
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)