From ae151df1a7e330371c87d13bbfd594c46f4cdcfb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:42:24 +1300 Subject: [PATCH 01/14] Define PostSettingsDataProvider protocol --- .../PostSettingsDataProvider.swift | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift new file mode 100644 index 000000000000..26ef48dcd75e --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift @@ -0,0 +1,52 @@ +import Foundation +import WordPressData + +@MainActor +protocol PostSettingsDataProvider: AnyObject { + var blog: Blog { get } + var capabilities: PostSettingsCapabilities { get } + + // Post identity and state + var postContent: String { get } + var isScheduled: Bool { get } + var isDraftOrPending: Bool { get } + var isPost: Bool { get } + var postID: Int? { get } + var hasRemote: Bool { get } + var isDeleted: Bool { get } + + // Display strings + var navigationTitle: String { get } + var authorFallbackDisplayName: String { get } + var suggestedSlug: String? { get } + var permalinkTemplate: String? { get } + var lastEditedText: String? { get } + + // Settings management + func makeSettings() -> PostSettings + func makeFeaturedImageViewModel() -> PostSettingsFeaturedImageViewModel? + func resolveDisplayedCategories(for settings: PostSettings) -> [String] + func customTaxonomies() -> [SiteTaxonomy] + func resolveTerms(in settings: inout PostSettings) async + + // Persistence + func applyLocally(settings: PostSettings) + func save(settings: PostSettings) async throws + func publish(settings: PostSettings) async throws + + // Optional features + var isEligibleForSocialSharing: Bool { get } + func parentPageText(for pageID: Int?) -> String? + func suggestedTags() async throws -> [String] + var supportsJetpackMetadata: Bool { get } +} + +extension PostSettingsDataProvider { + var isEligibleForSocialSharing: Bool { false } + + func parentPageText(for pageID: Int?) -> String? { nil } + + func suggestedTags() async throws -> [String] { [] } + + var supportsJetpackMetadata: Bool { false } +} From 5c572bd05ca1bd8ddf4575ced2492a6fe2a51654 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:42:53 +1300 Subject: [PATCH 02/14] Add data provider conformances --- .../AbstractPostSettingsDataProvider.swift | 19 +++++++++++++++++++ .../CustomPostSettingsDataProvider.swift | 17 +++++++++++++++++ .../PostSettings/PostSettingsViewModel.swift | 3 +++ 3 files changed, 39 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift new file mode 100644 index 000000000000..c83bb3d68c23 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -0,0 +1,19 @@ +import Foundation +import WordPressData + +@MainActor +final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { + let post: AbstractPost + + var blog: Blog { + post.blog + } + + var capabilities: PostSettingsCapabilities { + post is Post ? .post() : .page() + } + + init(post: AbstractPost) { + self.post = post + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift new file mode 100644 index 000000000000..41987868537d --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -0,0 +1,17 @@ +import Foundation +import WordPressData + +@MainActor +final class CustomPostSettingsDataProvider: PostSettingsDataProvider { + let blog: Blog + let editorService: CustomPostEditorService + + var capabilities: PostSettingsCapabilities { + PostSettingsCapabilities(from: editorService.details) + } + + init(editorService: CustomPostEditorService, blog: Blog) { + self.editorService = editorService + self.blog = blog + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index c193f8fe46ee..2d811083452c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -17,6 +17,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { let context: Context let featuredImageViewModel: PostSettingsFeaturedImageViewModel? let client: WordPressClient? + let provider: PostSettingsDataProvider private let details: PostDetails private let editorService: CustomPostEditorService? @@ -262,6 +263,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { context: Context = .settings, preferences: UserPersistentRepository = UserDefaults.standard ) { + self.provider = AbstractPostSettingsDataProvider(post: post) self.details = .abstractPost(post) self.blog = post.blog self.capabilities = post is Post ? .post() : .page() @@ -305,6 +307,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { context: Context = .settings, preferences: UserPersistentRepository = UserDefaults.standard ) { + self.provider = CustomPostSettingsDataProvider(editorService: editorService, blog: blog) self.details = .customPost(editorService) self.blog = blog self.capabilities = PostSettingsCapabilities(from: editorService.details) From c3a0760a2e92420b12d01a19c37452261c012035 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:43:08 +1300 Subject: [PATCH 03/14] Move postContent and navigationTitle to data provider --- .../AbstractPostSettingsDataProvider.swift | 24 +++++++++++++ .../CustomPostSettingsDataProvider.swift | 21 +++++++++++ .../PostSettings/PostSettingsViewModel.swift | 35 ++----------------- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index c83bb3d68c23..63e999e25e89 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -13,7 +13,31 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { post is Post ? .post() : .page() } + var postContent: String { + post.content ?? "" + } + + var navigationTitle: String { + isPost ? Strings.postSettingsTitle : Strings.pageSettingsTitle + } + init(post: AbstractPost) { self.post = post } } + +// MARK: - Localized Strings + +private enum Strings { + static let postSettingsTitle = NSLocalizedString( + "postSettings.navigationTitle.post", + value: "Post Settings", + comment: "The title of the Post Settings screen." + ) + + static let pageSettingsTitle = NSLocalizedString( + "postSettings.navigationTitle.page", + value: "Page Settings", + comment: "The title of the Page Settings screen." + ) +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 41987868537d..5d50fb08f19a 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -10,8 +10,29 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { PostSettingsCapabilities(from: editorService.details) } + var postContent: String { + editorService.post?.content.raw ?? "" + } + + var navigationTitle: String { + String.localizedStringWithFormat( + Strings.customPostSettingsTitle, + editorService.details.name + ) + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog } } + +// MARK: - Localized Strings + +private enum Strings { + static let customPostSettingsTitle = NSLocalizedString( + "postSettings.navigationTitle.customPostType", + value: "%1$@ Settings", + comment: "The title of the Post Settings screen for custom post types. %1$@ is the post type name." + ) +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 2d811083452c..5b8a82c6403d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -48,24 +48,11 @@ final class PostSettingsViewModel: NSObject, ObservableObject { /// The content of the post, used for AI excerpt generation. var postContent: String { - switch details { - case .abstractPost(let post): - return post.content ?? "" - case .customPost(let service): - return service.post?.content.raw ?? "" - } + provider.postContent } var navigationTitle: String { - switch details { - case .abstractPost: - return isPost ? Strings.postSettingsTitle : Strings.pageSettingsTitle - case .customPost(let service): - return String.localizedStringWithFormat( - Strings.customPostSettingsTitle, - service.details.name - ) - } + provider.navigationTitle } var deletedAlertTitle: String { @@ -928,24 +915,6 @@ private enum PostDetails { // MARK: - Localized Strings private enum Strings { - static let postSettingsTitle = NSLocalizedString( - "postSettings.navigationTitle.post", - value: "Post Settings", - comment: "The title of the Post Settings screen." - ) - - static let pageSettingsTitle = NSLocalizedString( - "postSettings.navigationTitle.page", - value: "Page Settings", - comment: "The title of the Page Settings screen." - ) - - static let customPostSettingsTitle = NSLocalizedString( - "postSettings.navigationTitle.customPostType", - value: "%1$@ Settings", - comment: "The title of the Post Settings screen for custom post types. %1$@ is the post type name." - ) - static let saveFailedMessage = NSLocalizedString( "postSettings.saveFailed.message", value: "Failed to save changes", From 36ac506545207f2eb9d708287d901f958b68781e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:43:33 +1300 Subject: [PATCH 04/14] Move post state properties to data provider --- .../AbstractPostSettingsDataProvider.swift | 12 ++++++++++ .../CustomPostSettingsDataProvider.swift | 15 +++++++++++++ .../PostSettings/PostSettingsViewModel.swift | 22 +++---------------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index 63e999e25e89..cf386455023a 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -21,6 +21,18 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { isPost ? Strings.postSettingsTitle : Strings.pageSettingsTitle } + var isScheduled: Bool { + post.getOriginal().status == .scheduled + } + + var isDraftOrPending: Bool { + post.getOriginal().isStatus(in: [.draft, .pending]) + } + + var isPost: Bool { + post is Post + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 5d50fb08f19a..6c61f4e3d851 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -21,6 +21,21 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { ) } + var isScheduled: Bool { + editorService.post?.status == .future + } + + var isDraftOrPending: Bool { + if let post = editorService.post { + return post.status == .draft || post.status == .pending + } + return true + } + + var isPost: Bool { + editorService.details.slug == "post" + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 5b8a82c6403d..69d2734665e2 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -64,10 +64,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } var isScheduled: Bool { - switch details { - case .abstractPost(let post): return post.getOriginal().status == .scheduled - case .customPost(let service): return service.post?.status == .future - } + provider.isScheduled } var authorDisplayName: String { @@ -145,24 +142,11 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } var isDraftOrPending: Bool { - switch details { - case .abstractPost(let post): - return post.getOriginal().isStatus(in: [.draft, .pending]) - case .customPost(let service): - if let post = service.post { - return post.status == .draft || post.status == .pending - } - return true - } + provider.isDraftOrPending } var isPost: Bool { - switch details { - case .abstractPost(let post): - return post is Post - case .customPost(let service): - return service.details.slug == "post" - } + provider.isPost } var shouldShowStickyOption: Bool { From 0b5f2d396f024d7286f836ae0db6c9389ec675ad Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:43:50 +1300 Subject: [PATCH 05/14] Move author and permalink to data provider --- .../AbstractPostSettingsDataProvider.swift | 12 +++++++++++ .../CustomPostSettingsDataProvider.swift | 12 +++++++++++ .../PostSettings/PostSettingsViewModel.swift | 21 +++---------------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index cf386455023a..d428c601151b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -33,6 +33,18 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { post is Post } + var authorFallbackDisplayName: String { + post.author?.makePlainText() ?? "" + } + + var suggestedSlug: String? { + post.suggested_slug + } + + var permalinkTemplate: String? { + post.permalinkTemplateURL + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 6c61f4e3d851..3a490a727374 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -36,6 +36,18 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { editorService.details.slug == "post" } + var authorFallbackDisplayName: String { + "" + } + + var suggestedSlug: String? { + editorService.post?.generatedSlug + } + + var permalinkTemplate: String? { + editorService.post?.permalinkTemplate + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 69d2734665e2..0a93bcdfd01d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -68,12 +68,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } var authorDisplayName: String { - switch details { - case .abstractPost(let post): - return settings.author?.displayName ?? post.author?.makePlainText() ?? "" - case .customPost: - return settings.author?.displayName ?? "" - } + settings.author?.displayName ?? provider.authorFallbackDisplayName } var authorAvatarURL: URL? { @@ -115,21 +110,11 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } var suggestedSlug: String? { - switch details { - case .abstractPost(let post): - return post.suggested_slug - case .customPost(let service): - return service.post?.generatedSlug - } + provider.suggestedSlug } var permalinkTemplate: String? { - switch details { - case .abstractPost(let post): - return post.permalinkTemplateURL - case .customPost(let service): - return service.post?.permalinkTemplate - } + provider.permalinkTemplate } var postFormatText: String { From 6457dac15a76863a087b0ad95987f7ebc78af494 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:43:59 +1300 Subject: [PATCH 06/14] Move post metadata to data provider --- .../AbstractPostSettingsDataProvider.swift | 18 ++++++++++ .../CustomPostSettingsDataProvider.swift | 13 ++++++++ .../PostSettings/PostSettingsViewModel.swift | 33 ++++--------------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index d428c601151b..4c17e4572c95 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -45,6 +45,24 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { post.permalinkTemplateURL } + var lastEditedText: String? { + guard let date = post.dateModified ?? post.dateCreated else { + return nil + } + return date.toMediumString() + } + + var postID: Int? { + guard let postID = post.postID?.intValue, postID > 0 else { + return nil + } + return postID + } + + var hasRemote: Bool { + post.hasRemote() + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 3a490a727374..81cad400110e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -48,6 +48,19 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { editorService.post?.permalinkTemplate } + var lastEditedText: String? { + editorService.post?.modifiedGmt.toMediumString() + } + + var postID: Int? { + guard let id = editorService.post?.id else { return nil } + return id > 0 ? Int(id) : nil + } + + var hasRemote: Bool { + editorService.post != nil + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 0a93bcdfd01d..6ffdc33b1df1 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -142,43 +142,22 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } var lastEditedText: String? { - switch details { - case .abstractPost(let post): - guard let date = post.dateModified ?? post.dateCreated else { - return nil - } - return date.toMediumString() - case .customPost(let service): - return service.post?.modifiedGmt.toMediumString() - } + provider.lastEditedText } var postID: Int? { - switch details { - case .abstractPost(let post): - guard let postID = post.postID?.intValue, postID > 0 else { - return nil - } - return postID - case .customPost(let service): - guard let id = service.post?.id else { return nil } - return id > 0 ? Int(id) : nil - } + provider.postID } - /// The underlying Page, if this is a Core Data-backed page. + /// The underlying Page for the parent page picker. var page: Page? { - abstractPost as? Page + // FIXME: This will be improved once we add parent page selection support to custom posts. + (provider as? AbstractPostSettingsDataProvider)?.post as? Page } /// Whether the post has a remote representation (used for permalink preview). var hasRemote: Bool { - switch details { - case .abstractPost(let post): - return post.hasRemote() - case .customPost(let service): - return service.post != nil - } + provider.hasRemote } enum SocialSharingSectionState { From 9cfaa386f816b9c1df19dcf8ec220cd86660c3d6 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:44:08 +1300 Subject: [PATCH 07/14] Move isDeleted to data provider --- .../PostSettings/AbstractPostSettingsDataProvider.swift | 7 +++++++ .../Post/PostSettings/CustomPostSettingsDataProvider.swift | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index 4c17e4572c95..13d427cc1f5e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -63,6 +63,13 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { post.hasRemote() } + var isDeleted: Bool { + guard let context = post.managedObjectContext else { + return true + } + return (try? context.existingObject(with: post.objectID)) == nil + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 81cad400110e..43479afea15d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -61,6 +61,10 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { editorService.post != nil } + var isDeleted: Bool { + false + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog From 1ec5967674f234faab81224b1324bd971a0aefed Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:44:18 +1300 Subject: [PATCH 08/14] Move category, taxonomy, and parent page resolution to data provider --- .../AbstractPostSettingsDataProvider.swift | 30 +++++++++++++++ .../CustomPostSettingsDataProvider.swift | 8 ++++ .../PostSettings/PostSettingsViewModel.swift | 37 ++----------------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index 13d427cc1f5e..6f278d9c9384 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -70,6 +70,36 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { return (try? context.existingObject(with: post.objectID)) == nil } + func resolveDisplayedCategories(for settings: PostSettings) -> [String] { + settings.getCategoryNames(for: post) + } + + func customTaxonomies() -> [SiteTaxonomy] { + let postType: String? = switch post { + case is Post: "post" + case is Page: "page" + default: nil + } + guard let postType else { + return [] + } + let taxonomies = try? blog.taxonomies + .filter { + $0.slug != "post_tag" && $0.slug != "category" && $0.supportedPostTypes.contains(postType) + } + .sorted(using: KeyPathComparator(\.name)) + return taxonomies ?? [] + } + + func parentPageText(for pageID: Int?) -> String? { + guard let page = post as? Page, + let context = page.managedObjectContext, + let pageID else { + return nil + } + return Page.parentPageText(in: context, parentID: NSNumber(value: pageID)) + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 43479afea15d..32c16d0f80df 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -65,6 +65,14 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { false } + func resolveDisplayedCategories(for settings: PostSettings) -> [String] { + settings.getCategoryNames(for: blog) + } + + func customTaxonomies() -> [SiteTaxonomy] { + editorService.taxonomies + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 6ffdc33b1df1..dae97c0208c4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -339,27 +339,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } private func refreshCustomTaxonomies() { - switch details { - case .abstractPost(let post): - let postType: String? = switch post { - case is Post: "post" - case is Page: "page" - default: nil - } - guard let postType else { - customTaxonomies = [] - return - } - let taxonomies = try? blog.taxonomies - .filter { - $0.slug != "post_tag" && $0.slug != "category" && $0.supportedPostTypes.contains(postType) - } - .sorted(using: KeyPathComparator(\.name)) - customTaxonomies = taxonomies ?? [] - - case .customPost(let service): - customTaxonomies = service.taxonomies - } + customTaxonomies = provider.customTaxonomies() } // MARK: - Term Resolution @@ -436,12 +416,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } private func refreshDisplayedCategories() { - switch details { - case .abstractPost(let post): - displayedCategories = settings.getCategoryNames(for: post) - case .customPost: - displayedCategories = settings.getCategoryNames(for: blog) - } + displayedCategories = provider.resolveDisplayedCategories(for: settings) } private func refreshDisplayedTags() { @@ -449,13 +424,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } private func refreshParentPageText() { - if let page, - let context = page.managedObjectContext, - let parentPageID = settings.parentPageID { - parentPageText = Page.parentPageText(in: context, parentID: NSNumber(value: parentPageID)) - } else { - parentPageText = nil - } + parentPageText = provider.parentPageText(for: settings.parentPageID) } // MARK: - Actions From 532b6b6392bfaa62e18492a292f7a6a2c140f7eb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:44:33 +1300 Subject: [PATCH 09/14] Move term resolution to data provider --- .../AbstractPostSettingsDataProvider.swift | 19 ++++++ .../CustomPostSettingsDataProvider.swift | 18 +++++ .../PostSettings/PostSettingsViewModel.swift | 66 ++++--------------- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index 6f278d9c9384..546f921d407b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -100,6 +100,25 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { return Page.parentPageText(in: context, parentID: NSNumber(value: pageID)) } + func resolveTerms(in settings: inout PostSettings) async { + let pendingNames = settings.tags.filter { $0.id == 0 }.map(\.name) + guard !pendingNames.isEmpty else { + return + } + + let service = TagsService(blog: blog) + let resolved = await service.resolveTerms(named: pendingNames) + for (name, existing) in resolved { + if let index = settings.tags.firstIndex(where: { $0.name == name }) { + settings.tags[index] = PostSettings.Term(id: Int(existing.id), name: existing.name) + } + } + } + + func suggestedTags() async throws -> [String] { + try await TagSuggestionsService().getSuggestedTags(for: post) + } + init(post: AbstractPost) { self.post = post } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 32c16d0f80df..91a5abbdc912 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -73,6 +73,24 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { editorService.taxonomies } + func resolveTerms(in settings: inout PostSettings) async { + do { + let tagsService = AnyTermService(client: editorService.client, endpoint: .tags) + settings.tags = try await TermResolutionService(taxonomyService: tagsService) + .resolveNames(for: settings.tags) + + for taxonomy in editorService.taxonomies { + guard let slugTerms = settings.otherTerms[taxonomy.slug] else { continue } + let termService = AnyTermService(client: editorService.client, endpoint: taxonomy.endpoint) + settings.otherTerms[taxonomy.slug] = try await TermResolutionService(taxonomyService: termService) + .resolveNames(for: slugTerms) + } + } catch { + // TODO: We need better error handling + Loggers.app.log(level: .error, "Failed to resolve taxonomy terms: \(error)") + } + } + init(editorService: CustomPostEditorService, blog: Blog) { self.editorService = editorService self.blog = blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index dae97c0208c4..750d31cd727f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -229,7 +229,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { refreshCustomTaxonomies() refreshParentPageText() refreshSocialSharingState() - resolveAbstractPostTerms() + resolveTerms() WPAnalytics.track(.postSettingsShown) } @@ -290,7 +290,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { refreshDisplayedCategories() refreshDisplayedTags() refreshCustomTaxonomies() - resolveTermNames() + resolveTerms() WPAnalytics.track(.postSettingsShown) } @@ -313,15 +313,13 @@ final class PostSettingsViewModel: NSObject, ObservableObject { // MARK: - Suggested Tags private func refreshSuggestedTags() { - guard let abstractPost, isSuggestedTagsRefreshNeeded else { - return - } + guard isSuggestedTagsRefreshNeeded else { return } isSuggestedTagsRefreshNeeded = false let task = Task { @MainActor [weak self] in do { - let tags = try await TagSuggestionsService().getSuggestedTags(for: abstractPost) guard let self else { return } + let tags = try await provider.suggestedTags() if !tags.isEmpty { withAnimation { self.suggestedTags = tags @@ -344,58 +342,20 @@ final class PostSettingsViewModel: NSObject, ObservableObject { // MARK: - Term Resolution - /// Resolves tags with `id == 0` in AbstractPost by searching the server. - /// AbstractPost stores tags as name-only strings, so they all start with - /// `id == 0` and need their IDs resolved. - private func resolveAbstractPostTerms() { - let pendingNames = settings.tags.filter { $0.id == 0 }.map(\.name) - guard !pendingNames.isEmpty else { return } - - // No need to set `isResolvingTags`, because the tag name is available to be displayed on screen. - + private func resolveTerms() { Task { [weak self] in guard let self else { return } - let service = TagsService(blog: blog) - let resolved = await service.resolveTerms(named: pendingNames) - for (name, existing) in resolved { - if let index = settings.tags.firstIndex(where: { $0.name == name }) { - settings.tags[index] = PostSettings.Term(id: Int(existing.id), name: existing.name) - } - } - refreshDisplayedTags() - } - } - - private func resolveTermNames() { - guard let editorService else { return } + isResolvingTags = true + isResolvingCustomTerms = true - isResolvingTags = true - isResolvingCustomTerms = !settings.otherTerms.isEmpty + var currentSettings = self.settings + await provider.resolveTerms(in: ¤tSettings) - Task { [weak self] in - guard let self else { return } - - do { - let tagsService = AnyTermService(client: editorService.client, endpoint: .tags) - let resolvedTags = try await TermResolutionService(taxonomyService: tagsService) - .resolveNames(for: settings.tags) - self.settings.tags = resolvedTags - self.refreshDisplayedTags() - self.isResolvingTags = false - - for taxonomy in editorService.taxonomies { - guard let slugTerms = self.settings.otherTerms[taxonomy.slug] else { continue } - let termService = AnyTermService(client: editorService.client, endpoint: taxonomy.endpoint) - let resolved = try await TermResolutionService(taxonomyService: termService) - .resolveNames(for: slugTerms) - self.settings.otherTerms[taxonomy.slug] = resolved - } - self.isResolvingCustomTerms = false - } catch { - // TODO: We need better error handling - Loggers.app.log(level: .error, "Failed to resolve taxonomy terms: \(error)") - } + self.settings = currentSettings + isResolvingTags = false + isResolvingCustomTerms = false + refreshDisplayedTags() } } From f4328a0b28df6243ec7682e8b79eef86c7549c3c Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:44:46 +1300 Subject: [PATCH 10/14] Move social sharing and Jetpack metadata flags to data provider --- .../AbstractPostSettingsDataProvider.swift | 21 +++++++++++++++++ .../CustomPostSettingsDataProvider.swift | 5 ++++ .../PostSettingsDataProvider.swift | 4 ---- .../PostSettings/PostSettingsViewModel.swift | 23 ++++++------------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index 546f921d407b..ff30141be165 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -1,9 +1,12 @@ import Foundation +import BuildSettingsKit import WordPressData +import WordPressKit @MainActor final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { let post: AbstractPost + let supportsJetpackMetadata = true var blog: Blog { post.blog @@ -119,9 +122,27 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { try await TagSuggestionsService().getSuggestedTags(for: post) } + var isEligibleForSocialSharing: Bool { + guard let post = post as? Post else { + return false + } + return BuildSettings.current.brand == .jetpack + && RemoteFeatureFlag.jetpackSocialImprovements.enabled() + && post.status != .publishPrivate + && !getPublicizeServices().isEmpty + && blog.supports(.publicize) + } + init(post: AbstractPost) { self.post = post } + + // MARK: - Private + + private func getPublicizeServices() -> [PublicizeService] { + let context = ContextManager.shared.mainContext + return (try? PublicizeService.allSupportedServices(in: context)) ?? [] + } } // MARK: - Localized Strings diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 91a5abbdc912..3d6012dddcbf 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -6,6 +6,11 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { let blog: Blog let editorService: CustomPostEditorService + // FIXME: meta support missing in AnyPostWithEditContext + let supportsJetpackMetadata = false + // FIXME: social sharing support missing in AnyPostWithEditContext + let isEligibleForSocialSharing = false + var capabilities: PostSettingsCapabilities { PostSettingsCapabilities(from: editorService.details) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift index 26ef48dcd75e..4c34796a4ac4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift @@ -42,11 +42,7 @@ protocol PostSettingsDataProvider: AnyObject { } extension PostSettingsDataProvider { - var isEligibleForSocialSharing: Bool { false } - func parentPageText(for pageID: Int?) -> String? { nil } func suggestedTags() async throws -> [String] { [] } - - var supportsJetpackMetadata: Bool { false } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 750d31cd727f..f4bde1dabcd4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -300,8 +300,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } func shouldShow(_ row: Row) -> Bool { - // FIXME: meta support missing in AnyPostWithEditContext - guard case .abstractPost = details else { return false } + guard provider.supportsJetpackMetadata else { return false } switch row { case .jetpackAccessLevel: return blog.supports(.wpComRESTAPI) @@ -571,7 +570,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { // MARK: - Social Sharing private func refreshSocialSharingState() { - guard let post = abstractPost as? Post, isPostEligibleForSocialSharing(post) else { + guard provider.isEligibleForSocialSharing else { socialSharingState = nil return } @@ -586,19 +585,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } } - private func isPostEligibleForSocialSharing(_ post: Post) -> Bool { - BuildSettings.current.brand == .jetpack - && RemoteFeatureFlag.jetpackSocialImprovements.enabled() - && post.status != .publishPrivate - && !getPublicizeServices().isEmpty - && blog.supports(.publicize) - } - - private func getPublicizeServices() -> [PublicizeService] { - let context = ContextManager.shared.mainContext - return (try? PublicizeService.allSupportedServices(in: context)) ?? [] - } - /// Convenience variable representing whether the No Connection view has been dismissed. /// Note: the value is stored per site. private var isSocialConnectionSetupDismissed: Bool { @@ -620,6 +606,11 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } } + private func getPublicizeServices() -> [PublicizeService] { + let context = ContextManager.shared.mainContext + return (try? PublicizeService.allSupportedServices(in: context)) ?? [] + } + private func makeSocialSharingSetupViewModel() -> JetpackSocialNoConnectionViewModel { JetpackSocialNoConnectionViewModel( services: getPublicizeServices(), From 5d4ad51a60eeffca11ec92076121c56a33305f0d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:44:58 +1300 Subject: [PATCH 11/14] Move settings and featured image to data provider --- .../AbstractPostSettingsDataProvider.swift | 8 +++++ .../CustomPostSettingsDataProvider.swift | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index ff30141be165..e8f0d4ffb762 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -137,6 +137,14 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { self.post = post } + func makeSettings() -> PostSettings { + PostSettings(from: post) + } + + func makeFeaturedImageViewModel() -> PostSettingsFeaturedImageViewModel? { + PostSettingsFeaturedImageViewModel(post: post) + } + // MARK: - Private private func getPublicizeServices() -> [PublicizeService] { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 3d6012dddcbf..4262d593a2d4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -100,6 +100,37 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { self.editorService = editorService self.blog = blog } + + func makeSettings() -> PostSettings { + var initialSettings = editorService.settings + // Resolve author display name from Blog's cached authors + if let authorId = initialSettings.author?.id, + let authors = blog.authors, + let author = authors.first(where: { $0.userID.intValue == authorId }) { + initialSettings.author = PostSettings.Author( + id: authorId, + displayName: author.displayName ?? "–", + avatarURL: author.avatarURL.flatMap(URL.init) + ) + } + return initialSettings + } + + func makeFeaturedImageViewModel() -> PostSettingsFeaturedImageViewModel? { + guard capabilities.supportsFeaturedImage else { return nil } + + let initialSettings = editorService.settings + let featuredImage = initialSettings.featuredImageID.flatMap { + Media.existingOrStubMediaWith( + mediaID: NSNumber(value: $0), + inBlog: blog + ) + } + return PostSettingsFeaturedImageViewModel( + blog: blog, + featuredImage: featuredImage + ) + } } // MARK: - Localized Strings From ca5227a229661b882c0fd207f91cdb61d8e93ba4 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:45:10 +1300 Subject: [PATCH 12/14] Unify PostSettingsViewModel initializers to use provider --- .../CustomPostEditorViewController.swift | 3 +- .../Post/PostEditor+MoreOptions.swift | 3 +- .../Post/PostSettings/PostSettingsView.swift | 3 +- .../PostSettings/PostSettingsViewModel.swift | 97 ++----------------- .../PublishPostViewController.swift | 6 +- 5 files changed, 18 insertions(+), 94 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index f9c10daae4c6..2267a2ae6ca3 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -193,7 +193,8 @@ private extension CustomPostEditorViewController { } func showPostSettings() { - let viewModel = PostSettingsViewModel(editorService: editorService, blog: blog) + let provider = CustomPostSettingsDataProvider(editorService: editorService, blog: blog) + let viewModel = PostSettingsViewModel(provider: provider) viewModel.onEditorPostSaved = { /* No-op: shared editorService is already up-to-date */ } let settingsVC = PostSettingsViewController(viewModel: viewModel) let navigation = UINavigationController(rootViewController: settingsVC) diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift index 5dc9a56cbb18..7b010a3a591d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift @@ -10,7 +10,8 @@ extension PostEditor { func displayPostSettings() { // Use the new SwiftUI-based Post Settings let originalFeaturedImageID = post.featuredImage?.mediaID - let viewModel = PostSettingsViewModel(post: post) + let provider = AbstractPostSettingsDataProvider(post: post) + let viewModel = PostSettingsViewModel(provider: provider) viewModel.onEditorPostSaved = { [weak self] in self?.didSavePostSettings(originalFeaturedImageID: originalFeaturedImageID) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index b48e3e5b0fae..3b20c8bd65e5 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -35,7 +35,8 @@ final class PostSettingsViewController: UIHostingController { } static func showStandaloneEditor(for post: AbstractPost, from presentingVC: UIViewController) { - let viewModel = PostSettingsViewModel(post: post, isStandalone: true) + let provider = AbstractPostSettingsDataProvider(post: post) + let viewModel = PostSettingsViewModel(provider: provider, isStandalone: true) let postSettingsVC = PostSettingsViewController(viewModel: viewModel) let navigation = UINavigationController(rootViewController: postSettingsVC) presentingVC.present(navigation, animated: true) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index f4bde1dabcd4..4b0320a74ad3 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -19,14 +19,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject { let client: WordPressClient? let provider: PostSettingsDataProvider - private let details: PostDetails - private let editorService: CustomPostEditorService? - - private var abstractPost: AbstractPost? { - if case .abstractPost(let post) = details { return post } - return nil - } - @Published var settings: PostSettings { didSet { refresh(from: oldValue, to: settings) @@ -190,40 +182,33 @@ final class PostSettingsViewModel: NSObject, ObservableObject { case publishing } - // MARK: - AbstractPost Initializer + // MARK: - Initializer init( - post: AbstractPost, + provider: PostSettingsDataProvider, isStandalone: Bool = false, context: Context = .settings, preferences: UserPersistentRepository = UserDefaults.standard ) { - self.provider = AbstractPostSettingsDataProvider(post: post) - self.details = .abstractPost(post) - self.blog = post.blog - self.capabilities = post is Post ? .post() : .page() + self.provider = provider + self.blog = provider.blog + self.capabilities = provider.capabilities self.isStandalone = isStandalone self.context = context self.preferences = preferences - self.client = try? WordPressClientFactory.shared.instance(for: .init(blog: post.blog)) - self.editorService = nil + self.client = try? WordPressClientFactory.shared.instance(for: .init(blog: provider.blog)) - // Initialize settings from the post - let initialSettings = PostSettings(from: post) + let initialSettings = provider.makeSettings() self.settings = initialSettings self.originalSettings = initialSettings - - // Initialize featured image view model - self.featuredImageViewModel = PostSettingsFeaturedImageViewModel(post: post) + self.featuredImageViewModel = provider.makeFeaturedImageViewModel() super.init() - // Observe selection changes from featured image view model featuredImageViewModel?.$selection.dropFirst().sink { [weak self] media in self?.settings.featuredImageID = media?.mediaID?.intValue }.store(in: &cancellables) - // Initialize all cached properties refreshDisplayedCategories() refreshDisplayedTags() refreshCustomTaxonomies() @@ -234,67 +219,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject { WPAnalytics.track(.postSettingsShown) } - // MARK: - CustomPostEditorService Initializer - - init( - editorService: CustomPostEditorService, - blog: Blog, - context: Context = .settings, - preferences: UserPersistentRepository = UserDefaults.standard - ) { - self.provider = CustomPostSettingsDataProvider(editorService: editorService, blog: blog) - self.details = .customPost(editorService) - self.blog = blog - self.capabilities = PostSettingsCapabilities(from: editorService.details) - self.isStandalone = false - self.context = context - self.preferences = preferences - self.client = editorService.client - self.editorService = editorService - - var initialSettings = editorService.settings - // Resolve author display name from Blog's cached authors - if let authorId = initialSettings.author?.id, - let authors = blog.authors, - let author = authors.first(where: { $0.userID.intValue == authorId }) { - initialSettings.author = PostSettings.Author( - id: authorId, - displayName: author.displayName ?? "–", - avatarURL: author.avatarURL.flatMap(URL.init) - ) - } - self.settings = initialSettings - self.originalSettings = initialSettings - - if capabilities.supportsFeaturedImage { - let featuredImage = initialSettings.featuredImageID.flatMap { - Media.existingOrStubMediaWith( - mediaID: NSNumber(value: $0), - inBlog: blog - ) - } - self.featuredImageViewModel = PostSettingsFeaturedImageViewModel( - blog: blog, - featuredImage: featuredImage - ) - } else { - self.featuredImageViewModel = nil - } - - super.init() - - featuredImageViewModel?.$selection.dropFirst().sink { [weak self] media in - self?.settings.featuredImageID = media?.mediaID?.intValue - }.store(in: &cancellables) - - refreshDisplayedCategories() - refreshDisplayedTags() - refreshCustomTaxonomies() - resolveTerms() - - WPAnalytics.track(.postSettingsShown) - } - func onAppear() { refreshSuggestedTags() } @@ -775,11 +699,6 @@ extension PostFormat { } } -private enum PostDetails { - case abstractPost(AbstractPost) - case customPost(CustomPostEditorService) -} - // MARK: - Localized Strings private enum Strings { diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 11e8f1cb493a..6dd43b2538aa 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -26,7 +26,8 @@ final class PublishPostViewController: UIHostingController { var onCompletion: ((PublishingSheetResult) -> Void)? init(post: AbstractPost, isStandalone: Bool) { - let viewModel = PostSettingsViewModel(post: post, isStandalone: isStandalone, context: .publishing) + let provider = AbstractPostSettingsDataProvider(post: post) + let viewModel = PostSettingsViewModel(provider: provider, isStandalone: isStandalone, context: .publishing) self.viewModel = viewModel let uploadsViewModel = PostMediaUploadsViewModel(post: post) @@ -57,7 +58,8 @@ final class PublishPostViewController: UIHostingController { editorService: CustomPostEditorService, blog: Blog ) { - let viewModel = PostSettingsViewModel(editorService: editorService, blog: blog, context: .publishing) + let provider = CustomPostSettingsDataProvider(editorService: editorService, blog: blog) + let viewModel = PostSettingsViewModel(provider: provider, context: .publishing) self.viewModel = viewModel self.uploadsViewModel = nil From 81789d238fed14c6d708b92ea006f88dc34fb5fb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:45:25 +1300 Subject: [PATCH 13/14] Unify save and publish in PostSettingsViewModel --- .../AbstractPostSettingsDataProvider.swift | 21 +++++ .../CustomPostSettingsDataProvider.swift | 12 +++ .../PostSettings/PostSettingsViewModel.swift | 88 ++----------------- 3 files changed, 38 insertions(+), 83 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index e8f0d4ffb762..b59a1d8be591 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -145,6 +145,27 @@ final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { PostSettingsFeaturedImageViewModel(post: post) } + func applyLocally(settings: PostSettings) { + settings.apply(to: post) + } + + func save(settings: PostSettings) async throws { + let coordinator = PostCoordinator.shared + if coordinator.isSyncAllowed(for: post) && post.status == settings.status { + let revision = post.createRevision() + settings.apply(to: revision) + coordinator.setNeedsSync(for: revision) + } else { + let changes = settings.makeUpdateParameters(from: post) + try await coordinator.save(post, changes: changes) + } + } + + func publish(settings: PostSettings) async throws { + let changes = settings.makeUpdateParameters(from: post) + try await PostCoordinator.shared.publish(post.getOriginal(), parameters: changes) + } + // MARK: - Private private func getPublicizeServices() -> [PublicizeService] { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index 4262d593a2d4..af9b428632e2 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -131,6 +131,18 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { featuredImage: featuredImage ) } + + func applyLocally(settings: PostSettings) { + editorService.applyLocally(settings: settings) + } + + func save(settings: PostSettings) async throws { + try await editorService.save(settings: settings, publish: false) + } + + func publish(settings: PostSettings) async throws { + try await editorService.save(settings: settings, publish: true) + } } // MARK: - Localized Strings diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 4b0320a74ad3..ad788924a975 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -317,25 +317,14 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } func buttonSaveTapped() { - switch details { - case .abstractPost(let post): - buttonSaveTappedForAbstractPost(post) - case .customPost: - buttonSaveTappedForCustomPost() - } - } - - private func buttonSaveTappedForAbstractPost(_ post: AbstractPost) { - // Check if the post still exists - guard let context = post.managedObjectContext, - let _ = try? context.existingObject(with: post.objectID) else { + guard !provider.isDeleted else { isShowingDeletedAlert = true return } guard isStandalone else { // Apply settings and return to the editor (editor-specific) - settings.apply(to: post) + provider.applyLocally(settings: settings) didSaveChanges() wpAssert(onEditorPostSaved != nil, "configuration missing") onEditorPostSaved?() @@ -343,47 +332,11 @@ final class PostSettingsViewModel: NSObject, ObservableObject { return } - isSaving = true - Task { - do { - let settings = getSettingsToSave(for: self.settings) - let coordinator = PostCoordinator.shared - if coordinator.isSyncAllowed(for: post) && post.status == settings.status { - let revision = post.createRevision() - settings.apply(to: revision) - coordinator.setNeedsSync(for: revision) - } else { - let changes = settings.makeUpdateParameters(from: post) - try await coordinator.save(post, changes: changes) - } - didSaveChanges() - onDismiss?() - } catch { - isSaving = false - } - } - } - - private func buttonSaveTappedForCustomPost() { - guard let editorService else { - wpAssertionFailure("missing editor service") - return - } - - guard isStandalone else { - let settingsToSave = getSettingsToSave(for: settings) - editorService.applyLocally(settings: settingsToSave) - didSaveChanges() - onEditorPostSaved?() - onDismiss?() - return - } - isSaving = true Task { do { let settingsToSave = getSettingsToSave(for: settings) - try await editorService.save(settings: settingsToSave, publish: false) + try await provider.save(settings: settingsToSave) didSaveChanges() onEditorPostSaved?() onDismiss?() @@ -408,18 +361,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } func buttonPublishTapped() { - switch details { - case .abstractPost(let post): - publishAbstractPost(post) - case .customPost: - publishCustomPost() - } - } - - private func publishAbstractPost(_ post: AbstractPost) { - // Check if the post still exists - guard let context = post.managedObjectContext, - let _ = try? context.existingObject(with: post.objectID) else { + guard !provider.isDeleted else { isShowingDeletedAlert = true return } @@ -427,27 +369,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { isSaving = true Task { do { - let coordinator = PostCoordinator.shared - let changes = settings.makeUpdateParameters(from: post) - try await coordinator.publish(post.getOriginal(), parameters: changes) - onPostPublished?() - } catch { - isSaving = false - // `PostCoordinator` handles errors by showing an alert when needed - } - } - } - - private func publishCustomPost() { - guard let editorService else { - wpAssertionFailure("missing editor service") - return - } - - isSaving = true - Task { - do { - try await editorService.save(settings: settings, publish: true) + try await provider.publish(settings: settings) onPostPublished?() } catch { isSaving = false From dfacacd28a761db9a9804995c33e7387cd4006c7 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 11 Mar 2026 22:54:45 +1300 Subject: [PATCH 14/14] Add hasTermName flag to avoid unnecessary loading spinners --- .../Post/PostSettings/AbstractPostSettingsDataProvider.swift | 1 + .../Post/PostSettings/CustomPostSettingsDataProvider.swift | 1 + .../Post/PostSettings/PostSettingsDataProvider.swift | 1 + .../ViewRelated/Post/PostSettings/PostSettingsView.swift | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift index b59a1d8be591..0a94c657c15d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/AbstractPostSettingsDataProvider.swift @@ -7,6 +7,7 @@ import WordPressKit final class AbstractPostSettingsDataProvider: PostSettingsDataProvider { let post: AbstractPost let supportsJetpackMetadata = true + let hasTermNames = true var blog: Blog { post.blog diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift index af9b428632e2..3336de516246 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsDataProvider.swift @@ -10,6 +10,7 @@ final class CustomPostSettingsDataProvider: PostSettingsDataProvider { let supportsJetpackMetadata = false // FIXME: social sharing support missing in AnyPostWithEditContext let isEligibleForSocialSharing = false + let hasTermNames = false var capabilities: PostSettingsCapabilities { PostSettingsCapabilities(from: editorService.details) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift index 4c34796a4ac4..f6b1cd96c277 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsDataProvider.swift @@ -39,6 +39,7 @@ protocol PostSettingsDataProvider: AnyObject { func parentPageText(for pageID: Int?) -> String? func suggestedTags() async throws -> [String] var supportsJetpackMetadata: Bool { get } + var hasTermNames: Bool { get } } extension PostSettingsDataProvider { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 3b20c8bd65e5..206993319d4b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -216,7 +216,7 @@ struct PostSettingsFormContentView: View { viewModel.didSelectTags(tags) } } label: { - PostSettingsTagsRow(tags: viewModel.displayedTags, isLoading: viewModel.isResolvingTags) + PostSettingsTagsRow(tags: viewModel.displayedTags, isLoading: !viewModel.provider.hasTermNames && viewModel.isResolvingTags) } .accessibilityIdentifier("post_settings_tags") } @@ -252,7 +252,7 @@ struct PostSettingsFormContentView: View { PostSettingsCustomTaxonomyRow( taxonomy: taxonomy, terms: viewModel.settings.getTerms(forTaxonomySlug: taxonomy.slug).map(\.name), - isLoading: viewModel.isResolvingCustomTerms + isLoading: !viewModel.provider.hasTermNames && viewModel.isResolvingCustomTerms ) } }