From e1503288854dbff1b20386d1a16e906c1e28ee10 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 19:29:23 +1200 Subject: [PATCH 1/7] Route tags and categories to the REST API on self-hosted sites Self-hosted taxonomy went through XML-RPC term methods, which some hosts block at the WAF, breaking tags and categories on new posts (#25758). Prefer the core REST API (wp/v2) whenever the site has application-password access, keeping XML-RPC only as a fallback for legacy sites without it. --- .../Classes/Services/PostCategoryService.m | 12 +++++++-- .../PostSettings/PostSettingsViewModel.swift | 2 +- .../ViewRelated/Tags/TagsViewModel.swift | 25 ++++++++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Services/PostCategoryService.m b/WordPress/Classes/Services/PostCategoryService.m index 55b26097d8e6..8fa51d3f946a 100644 --- a/WordPress/Classes/Services/PostCategoryService.m +++ b/WordPress/Classes/Services/PostCategoryService.m @@ -184,8 +184,16 @@ - (void)mergeCategories:(NSArray *)remoteCategories forBl if (blog.wordPressComRestApi) { return [[TaxonomyServiceRemoteREST alloc] initWithWordPressComRestApi:blog.wordPressComRestApi siteID:blog.dotComID]; } - } else if (blog.xmlrpcApi) { - return [[TaxonomyServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password]; + } else { + // Prefer the core REST API (wp/v2) for self-hosted sites. Some hosts + // block the XML-RPC term methods, which breaks categories (#25758). + TaxonomyServiceRemoteCoreREST *coreREST = [[TaxonomyServiceRemoteCoreREST alloc] initWithBlog:blog]; + if (coreREST) { + return coreREST; + } + if (blog.xmlrpcApi) { + return [[TaxonomyServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password]; + } } return nil; } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 2f66e9874b40..9a9e20df70bc 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -302,7 +302,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM Task { [weak self] in guard let self else { return } - let service = TagsService(blog: blog) + let service = TagsViewModel.makeTagsService(for: blog) let resolved = await service.resolveTerms(named: pendingNames) for (name, existing) in resolved { if let index = settings.tags.firstIndex(where: { $0.name == name }) { diff --git a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift index 4e2366e2c160..1d350dad6d1f 100644 --- a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift @@ -58,7 +58,12 @@ class TagsViewModel: ObservableObject { } convenience init(blog: Blog, selectedTags: [SelectedTerm] = [], mode: TagsViewMode) { - self.init(taxonomy: nil, service: TagsService(blog: blog), selectedTerms: selectedTags, mode: mode) + self.init( + taxonomy: nil, + service: TagsViewModel.makeTagsService(for: blog), + selectedTerms: selectedTags, + mode: mode + ) } convenience init( @@ -90,6 +95,24 @@ class TagsViewModel: ObservableObject { self.selectedTagsSet = Set(selectedTerms.map { $0.name.lowercased() }) } + /// Chooses the taxonomy service backing a blog's tags. + /// + /// Sites with core REST API access (WordPress.com, or self-hosted sites with + /// an application password) use `AnyTermService` so tags go through the wp/v2 + /// REST API. Some hosts block the XML-RPC term methods that `TagsService` + /// uses for self-hosted sites, which breaks tags entirely (issue #25758). + /// Legacy self-hosted sites without REST access keep using `TagsService`. + static func makeTagsService( + for blog: Blog, + keychain: KeychainAccessible = AppKeychain() + ) -> TaxonomyServiceProtocol { + if let site = try? WordPressSite(blog: blog, keychain: keychain) { + let client = WordPressClientFactory.shared.instance(for: site) + return AnyTermService(client: client, endpoint: .tags) + } + return TagsService(blog: blog) + } + func onAppear() { guard response == nil else { return } Task { From dae39893d7f6e683a4b1b2d3410755c99c2d34cb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 19:30:11 +1200 Subject: [PATCH 2/7] Replace TagsViewModel convenience inits with named factory methods The two convenience initializers were ambiguous about when to use each. Introduce tags(for:) and taxonomy(_:client:) so call sites read by intent, and drop the unused blog parameter that was threaded through the custom taxonomy path. --- .../SiteSettingsViewController+Swift.swift | 4 +-- .../Post/PostSettings/PostSettingsView.swift | 1 - .../PostSettings/Views/PostTagsView.swift | 6 ++-- .../Tags/SiteCustomTaxonomiesView.swift | 6 ++-- .../ViewRelated/Tags/SiteTagsView.swift | 2 +- .../ViewRelated/Tags/TagsViewModel.swift | 29 +++++++++++++------ 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift index a96aafabeff8..906d650d6265 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift @@ -62,7 +62,7 @@ extension SiteSettingsViewController { @objc public func showCustomTaxonomies() { let viewController: UIViewController if let client = try? WordPressClientFactory.shared.instance(for: .init(blog: blog)) { - let rootView = SiteCustomTaxonomiesView(blog: self.blog, client: client) + let rootView = SiteCustomTaxonomiesView(client: client) viewController = UIHostingController(rootView: rootView) } else { let feature = NSLocalizedString( @@ -76,7 +76,7 @@ extension SiteSettingsViewController { source: "taxonomies", presentingViewController: self ) { client in - SiteCustomTaxonomiesView(blog: self.blog, client: client) + SiteCustomTaxonomiesView(client: client) } viewController = UIHostingController(rootView: rootView) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 58f10c8cdbb0..6b86070bf53c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -243,7 +243,6 @@ struct PostSettingsFormContentView: Vi ForEach(viewModel.customTaxonomies, id: \.slug) { taxonomy in NavigationLink { PostTagsView( - blog: viewModel.blog, client: client, taxonomy: taxonomy, selectedTerms: viewModel.settings.getTerms(forTaxonomySlug: taxonomy.slug) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostTagsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostTagsView.swift index 201086620873..8138f87de456 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostTagsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostTagsView.swift @@ -12,14 +12,14 @@ struct PostTagsView: View { @State private var isKeyboardPresented = false init(blog: Blog, selectedTags: [TagsViewModel.SelectedTerm], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) { - let viewModel = TagsViewModel(blog: blog, selectedTags: selectedTags, mode: .selection(onSelectedTagsChanged: { tags in + let viewModel = TagsViewModel.tags(for: blog, selectedTerms: selectedTags, mode: .selection(onSelectedTagsChanged: { tags in onSelectionChanged(tags) })) self._viewModel = StateObject(wrappedValue: viewModel) } - init(blog: Blog, client: WordPressClient, taxonomy: SiteTaxonomy, selectedTerms: [TagsViewModel.SelectedTerm] = [], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) { - let viewModel = TagsViewModel(blog: blog, client: client, taxonomy: taxonomy, selectedTerms: selectedTerms, mode: .selection(onSelectedTagsChanged: { tags in + init(client: WordPressClient, taxonomy: SiteTaxonomy, selectedTerms: [TagsViewModel.SelectedTerm] = [], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) { + let viewModel = TagsViewModel.taxonomy(taxonomy, client: client, selectedTerms: selectedTerms, mode: .selection(onSelectedTagsChanged: { tags in onSelectionChanged(tags) })) self._viewModel = StateObject(wrappedValue: viewModel) diff --git a/WordPress/Classes/ViewRelated/Tags/SiteCustomTaxonomiesView.swift b/WordPress/Classes/ViewRelated/Tags/SiteCustomTaxonomiesView.swift index f1c2815ac3c9..3baa87a9fb46 100644 --- a/WordPress/Classes/ViewRelated/Tags/SiteCustomTaxonomiesView.swift +++ b/WordPress/Classes/ViewRelated/Tags/SiteCustomTaxonomiesView.swift @@ -7,15 +7,13 @@ import WordPressShared import WordPressUI struct SiteCustomTaxonomiesView: View { - let blog: Blog let client: WordPressClient @State private var isLoading: Bool = false @State private var taxonomies: [SiteTaxonomy]? = nil @State private var error: Error? - init(blog: Blog, client: WordPressClient) { - self.blog = blog + init(client: WordPressClient) { self.client = client } @@ -23,7 +21,7 @@ struct SiteCustomTaxonomiesView: View { List { ForEach(taxonomies ?? [], id: \.slug) { taxonomy in NavigationLink { - SiteTagsView(viewModel: .init(blog: blog, client: client, taxonomy: taxonomy, mode: .browse)) + SiteTagsView(viewModel: .taxonomy(taxonomy, client: client, mode: .browse)) } label: { Text(taxonomy.localizedName) } diff --git a/WordPress/Classes/ViewRelated/Tags/SiteTagsView.swift b/WordPress/Classes/ViewRelated/Tags/SiteTagsView.swift index ce82ff1f3389..6ce0d49332f6 100644 --- a/WordPress/Classes/ViewRelated/Tags/SiteTagsView.swift +++ b/WordPress/Classes/ViewRelated/Tags/SiteTagsView.swift @@ -193,7 +193,7 @@ class SiteTagsViewController: UIHostingController { let viewModel: TagsViewModel init(blog: Blog) { - viewModel = TagsViewModel(blog: blog, mode: .browse) + viewModel = TagsViewModel.tags(for: blog, mode: .browse) super.init(rootView: .init(viewModel: viewModel)) } diff --git a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift index 1d350dad6d1f..4b1e55b3ecef 100644 --- a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift @@ -57,23 +57,32 @@ class TagsViewModel: ObservableObject { return false } - convenience init(blog: Blog, selectedTags: [SelectedTerm] = [], mode: TagsViewMode) { - self.init( + /// Builds a view model for a site's built-in tags. `makeTagsService(for:)` + /// picks the backing service so self-hosted sites use REST where they can + /// (issue #25758). + static func tags( + for blog: Blog, + selectedTerms: [SelectedTerm] = [], + mode: TagsViewMode + ) -> TagsViewModel { + TagsViewModel( taxonomy: nil, - service: TagsViewModel.makeTagsService(for: blog), - selectedTerms: selectedTags, + service: makeTagsService(for: blog), + selectedTerms: selectedTerms, mode: mode ) } - convenience init( - blog: Blog, + /// Builds a view model for a specific site `taxonomy` (e.g. a custom + /// taxonomy). Always uses the core REST API, which a custom taxonomy's + /// `client` already guarantees is available. + static func taxonomy( + _ taxonomy: SiteTaxonomy, client: WordPressClient, - taxonomy: SiteTaxonomy, selectedTerms: [SelectedTerm] = [], mode: TagsViewMode - ) { - self.init( + ) -> TagsViewModel { + TagsViewModel( taxonomy: taxonomy, service: AnyTermService(client: client, endpoint: taxonomy.endpoint), selectedTerms: selectedTerms, @@ -81,6 +90,8 @@ class TagsViewModel: ObservableObject { ) } + /// Designated initializer. Inject a `service` directly; production code + /// should prefer the `tags(for:)` / `taxonomy(_:client:)` factories. init( taxonomy: SiteTaxonomy?, service: TaxonomyServiceProtocol, From bfdb97ae00f547088974d566b34786b749872532 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 19:38:47 +1200 Subject: [PATCH 3/7] Add release note --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 423368e006f3..711fc84f4ed5 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ * [*] [internal] In-app updates: guard the flexible update notice against double-posting when two update checks race [#25666] * [*] [build tooling] Add a String Catalog localization pipeline (plurals + build-free catalog generation) with a CI coverage gate [#25688] * [*] [internal] Stats widgets: migrate the site picker from SiriKit to App Intents [#25753] +* [*] Fix categories and tags not working on self-hosted sites with XML-RPC disabled [#25767] 27.0 From fc247e223227262c6c5a57c83ca5e4fb4f749242 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 14 Jul 2026 11:40:37 +1200 Subject: [PATCH 4/7] Format taxonomy Core REST service --- .../TaxonomyServiceRemoteCoreREST.swift | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift index 868796e36aa6..9fddc2f3b171 100644 --- a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift +++ b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift @@ -17,7 +17,11 @@ import WordPressAPI self.client = client } - public func createCategory(_ category: RemotePostCategory, success: ((RemotePostCategory) -> Void)?, failure: ((any Error) -> Void)? = nil) { + public func createCategory( + _ category: RemotePostCategory, + success: ((RemotePostCategory) -> Void)?, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let params = TermCreateParams( @@ -33,7 +37,10 @@ import WordPressAPI } } - public func getCategoriesWithSuccess(_ success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func getCategoriesWithSuccess( + _ success: @escaping ([RemotePostCategory]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let sequence = await client.api.terms.sequenceWithEditContext( @@ -51,7 +58,11 @@ import WordPressAPI } } - public func getCategoriesWith(_ paging: RemoteTaxonomyPaging, success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func getCategoriesWith( + _ paging: RemoteTaxonomyPaging, + success: @escaping ([RemotePostCategory]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let params = TermListParams( @@ -73,7 +84,11 @@ import WordPressAPI } } - public func searchCategories(withName nameQuery: String, success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func searchCategories( + withName nameQuery: String, + success: @escaping ([RemotePostCategory]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let params = TermListParams(search: nameQuery) @@ -89,7 +104,11 @@ import WordPressAPI } } - public func createTag(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) { + public func createTag( + _ tag: RemotePostTag, + success: ((RemotePostTag) -> Void)?, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let params = TermCreateParams( @@ -109,7 +128,8 @@ import WordPressAPI } } - public func update(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) { + public func update(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) + { guard let tagID = tag.tagID else { failure?(URLError(.unknown)) return @@ -151,7 +171,10 @@ import WordPressAPI } } - public func getTagsWithSuccess(_ success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func getTagsWithSuccess( + _ success: @escaping ([RemotePostTag]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let response = try await client.api.terms.listWithEditContext( @@ -166,7 +189,11 @@ import WordPressAPI } } - public func getTagsWith(_ paging: RemoteTaxonomyPaging, success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func getTagsWith( + _ paging: RemoteTaxonomyPaging, + success: @escaping ([RemotePostTag]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let params = TermListParams( @@ -188,7 +215,11 @@ import WordPressAPI } } - public func searchTags(withName nameQuery: String, success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) { + public func searchTags( + withName nameQuery: String, + success: @escaping ([RemotePostTag]) -> Void, + failure: ((any Error) -> Void)? = nil + ) { Task { @MainActor in do { let response = try await client.api.terms.listWithEditContext( From ee54cebf7da012160fb451a5a16c7103b81b110e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 14 Jul 2026 12:31:57 +1200 Subject: [PATCH 5/7] Use view context for taxonomy reads --- .../TaxonomyServiceRemoteCoreREST.swift | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift index 9fddc2f3b171..243e4fdd1afe 100644 --- a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift +++ b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift @@ -43,7 +43,7 @@ import WordPressAPI ) { Task { @MainActor in do { - let sequence = await client.api.terms.sequenceWithEditContext( + let sequence = await client.api.terms.sequenceWithViewContext( type: .categories, params: TermListParams(perPage: 100) ) @@ -72,7 +72,7 @@ import WordPressAPI order: WpApiParamOrder(paging.order), orderby: WpApiParamTermsOrderBy(paging.orderBy) ) - let response = try await client.api.terms.listWithEditContext( + let response = try await client.api.terms.listWithViewContext( termEndpointType: .categories, params: params ) @@ -92,7 +92,7 @@ import WordPressAPI Task { @MainActor in do { let params = TermListParams(search: nameQuery) - let response = try await client.api.terms.listWithEditContext( + let response = try await client.api.terms.listWithViewContext( termEndpointType: .categories, params: params ) @@ -177,7 +177,7 @@ import WordPressAPI ) { Task { @MainActor in do { - let response = try await client.api.terms.listWithEditContext( + let response = try await client.api.terms.listWithViewContext( termEndpointType: .tags, params: TermListParams() ) @@ -203,7 +203,7 @@ import WordPressAPI order: WpApiParamOrder(paging.order), orderby: WpApiParamTermsOrderBy(paging.orderBy) ) - let response = try await client.api.terms.listWithEditContext( + let response = try await client.api.terms.listWithViewContext( termEndpointType: .tags, params: params ) @@ -222,7 +222,7 @@ import WordPressAPI ) { Task { @MainActor in do { - let response = try await client.api.terms.listWithEditContext( + let response = try await client.api.terms.listWithViewContext( termEndpointType: .tags, params: TermListParams(search: nameQuery) ) @@ -236,6 +236,13 @@ import WordPressAPI } private extension RemotePostCategory { + convenience init(category: AnyTermWithViewContext) { + self.init() + self.categoryID = NSNumber(value: category.id) + self.name = category.name + self.parentID = NSNumber(value: category.parent ?? 0) + } + convenience init(category: AnyTermWithEditContext) { self.init() self.categoryID = NSNumber(value: category.id) @@ -245,6 +252,15 @@ private extension RemotePostCategory { } private extension RemotePostTag { + convenience init(tag: AnyTermWithViewContext) { + self.init() + self.tagID = NSNumber(value: tag.id) + self.name = tag.name + self.slug = tag.slug + self.tagDescription = tag.description + self.postCount = NSNumber(value: tag.count) + } + convenience init(tag: AnyTermWithEditContext) { self.init() self.tagID = NSNumber(value: tag.id) From f3b73d00b9531b54b1d19bcdd4a1fd4ced0609e1 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 14 Jul 2026 13:02:35 +1200 Subject: [PATCH 6/7] Fix a swiftlint issue --- .../Classes/Services/TaxonomyServiceRemoteCoreREST.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift index 243e4fdd1afe..71b9b18457fb 100644 --- a/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift +++ b/WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift @@ -128,8 +128,11 @@ import WordPressAPI } } - public func update(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) - { + public func update( + _ tag: RemotePostTag, + success: ((RemotePostTag) -> Void)?, + failure: ((any Error) -> Void)? = nil + ) { guard let tagID = tag.tagID else { failure?(URLError(.unknown)) return From f25653d0317c59f3a0bc67e974adeb602803e554 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:07:30 -0600 Subject: [PATCH 7/7] Use the REST pagination signal for tag lists TagsViewModel inferred `hasMore` from `count == 100`, so a tag list whose size is an exact multiple of the page size requested a page past the last one. On the wp/v2 path that returns 400 (rest_term_invalid_page_number), surfacing a stuck pagination error where the offset-based path stopped cleanly. Return `hasMore` from `getTags` instead: AnyTermService derives it from the response's `nextPageParams`; TagsService keeps the count check that is safe for its offset-based remotes. --- .../Classes/ViewRelated/Tags/TagsService.swift | 14 +++++++++----- .../Classes/ViewRelated/Tags/TagsViewModel.swift | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Tags/TagsService.swift b/WordPress/Classes/ViewRelated/Tags/TagsService.swift index 7c7dc785719b..f2fb090bcad9 100644 --- a/WordPress/Classes/ViewRelated/Tags/TagsService.swift +++ b/WordPress/Classes/ViewRelated/Tags/TagsService.swift @@ -6,7 +6,7 @@ import WordPressAPI import WordPressAPIInternal protocol TaxonomyServiceProtocol { - func getTags(page: Int, recentlyUsed: Bool) async throws -> [AnyTermWithViewContext] + func getTags(page: Int, recentlyUsed: Bool) async throws -> (terms: [AnyTermWithViewContext], hasMore: Bool) func searchTags(with query: String) async throws -> [AnyTermWithViewContext] func createTag(name: String, description: String) async throws -> AnyTermWithViewContext func updateTag(_ term: AnyTermWithViewContext, name: String, description: String) async throws -> AnyTermWithViewContext @@ -38,7 +38,7 @@ class TagsService: TaxonomyServiceProtocol { func getTags( page: Int = 0, recentlyUsed: Bool = false - ) async throws -> [AnyTermWithViewContext] { + ) async throws -> (terms: [AnyTermWithViewContext], hasMore: Bool) { guard let remote else { throw TagsServiceError.noRemoteService } @@ -52,7 +52,8 @@ class TagsService: TaxonomyServiceProtocol { return try await withCheckedThrowingContinuation { continuation in remote.getTagsWith(paging, success: { remoteTags in - continuation.resume(returning: remoteTags.map { AnyTermWithViewContext(tag: $0) }) + let terms = remoteTags.map { AnyTermWithViewContext(tag: $0) } + continuation.resume(returning: (terms, terms.count == pageSize)) }, failure: { error in continuation.resume(throwing: error) }) @@ -254,7 +255,10 @@ class AnyTermService: TaxonomyServiceProtocol { self.client = client } - func getTags(page: Int = 0, recentlyUsed: Bool = false) async throws -> [AnyTermWithViewContext] { + func getTags( + page: Int = 0, + recentlyUsed: Bool = false + ) async throws -> (terms: [AnyTermWithViewContext], hasMore: Bool) { let perPage: UInt32 = 100 let params = TermListParams( page: UInt32(page + 1), @@ -268,7 +272,7 @@ class AnyTermService: TaxonomyServiceProtocol { params: params ) - return response.data + return (response.data, response.nextPageParams != nil) } func searchTags(with query: String) async throws -> [AnyTermWithViewContext] { diff --git a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift index 3e09e0113334..abe84952dd15 100644 --- a/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift @@ -152,16 +152,16 @@ class TagsViewModel: ObservableObject { } let page = pageIndex ?? 0 - let remoteTags = try await self.tagsService.getTags( + let result = try await self.tagsService.getTags( page: page, recentlyUsed: !self.isBrowseMode ) - let hasMore = remoteTags.count == 100 + let hasMore = result.hasMore let nextPage = hasMore ? page + 1 : nil return TagsPaginatedResponse.Page( - items: remoteTags, + items: result.terms, total: nil, hasMore: hasMore, nextPage: nextPage