Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Settings in SwiftUI: Initial #20838

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions WordPress/Classes/Extensions/SwiftUI+Backport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftUI

struct Backport<T: View> {
let view: T
}

extension View {
var backport: Backport<Self> { Backport(view: self) }
}

extension Backport {
@ViewBuilder
func refreshable(action: @Sendable @escaping () async -> Void) -> some View {
if #available(iOS 15, *) {
view.refreshable(action: action)
Copy link
Contributor Author

@kean kean Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove it once iOS 14 support is dropped.

} else {
view
}
}
}
138 changes: 138 additions & 0 deletions WordPress/Classes/ViewRelated/Blog/Settings/SiteSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import Foundation
import SwiftUI
import Combine
import SVProgressHUD

struct SiteSettingsView: View {
@ObservedObject private var blog: Blog
@ObservedObject private var settings: BlogSettings

@StateObject private var viewModel: SiteSettingsViewModel

@SwiftUI.Environment(\.presentationMode) private var presentationMode

init(blog: Blog) {
self.blog = blog
self.settings = blog.settings ?? BlogSettings(context: ContextManager.shared.mainContext) // Right-side should never happen
self._viewModel = StateObject(wrappedValue: SiteSettingsViewModel(blog: blog))
}

var body: some View {
List {
sections
}
.listStyle(.insetGrouped)
.onReceive(viewModel.onDismissableError) {
SVProgressHUD.showDismissibleError(withStatus: $0)
}
.backport.refreshable {
await viewModel.refresh()
}
.onAppear {
Task { await viewModel.refresh() }
}
.navigationTitle(Strings.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
}

private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
if presentationMode.wrappedValue.isPresented {
closeButton
}
}
}

private var sections: some View {
kean marked this conversation as resolved.
Show resolved Hide resolved
Section(header: Text(Strings.Sections.general)) {
siteTitleRow
}
}

// MARK: - General

private var siteTitleRow: some View {
withAdminNavigationLink(destination: {
SettingsTextEditView(
value: settings.name,
placeholder: Strings.General.siteTitlePlaceholder,
onCommit: viewModel.updateSiteTitle
)
.navigationTitle(Strings.General.siteTitle)
}) {
SettingsCell(title: Strings.General.siteTitle, value: settings.name ?? Strings.General.siteTitlePlaceholder)
}
}

// MARK: - Helpers

@ViewBuilder
private func withAdminNavigationLink<T: View, U: View>(
@ViewBuilder destination: () -> T,
@ViewBuilder content: () -> U
) -> some View {
if blog.isAdmin {
NavigationLink(destination: destination(), label: content)
} else {
content()
}
}

private var closeButton: some View {
Button(action: { presentationMode.wrappedValue.dismiss() }) {
Text(Strings.done)
.font(.body.weight(.medium))
.foregroundColor(Color.primary)
}
}
}

private struct SettingsCell: View {
let title: String
let value: String

var body: some View {
HStack {
Text(title)
.layoutPriority(1)
Spacer()
Text(value)
.foregroundColor(.secondary)
}
.lineLimit(1)
}
}

private struct SettingsTextEditView: UIViewControllerRepresentable {
let value: String?
let placeholder: String
var hint: String?
let onCommit: ((String)) -> Void

func makeUIViewController(context: Context) -> SettingsTextViewController {
let viewController = SettingsTextViewController(text: value ?? "", placeholder: placeholder, hint: hint ?? "")
viewController.onValueChanged = onCommit
return viewController
}

func updateUIViewController(_ uiViewController: SettingsTextViewController, context: Context) {
// Do nothing
}
}
kean marked this conversation as resolved.
Show resolved Hide resolved

private extension SiteSettingsView {
enum Strings {
static let title = NSLocalizedString("siteSettings.title", value: "Settings", comment: "Title for screen that allows configuration of your blog/site settings.")
static let done = NSLocalizedString("siteSettings.done", value: "Done", comment: "Label for Done button")

enum Sections {
static let general = NSLocalizedString("siteSettings.general.title", value: "General", comment: "Title for the general section in site settings screen")
}

enum General {
static let siteTitle = NSLocalizedString("siteSettings.general.siteTitle", value: "Site Title", comment: "Label for site title blog setting")
static let siteTitlePlaceholder = NSLocalizedString("siteSettings.general.siteTitlePlaceholder", value: "A title for the site", comment: "Placeholder text for the title of a site")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation
import SwiftUI
import Combine

final class SiteSettingsViewModel: ObservableObject {
private let blog: Blog
private let service: BlogService

let onDismissableError = PassthroughSubject<String, Never>()

init(blog: Blog,
service: BlogService = BlogService(coreDataStack: ContextManager.shared)) {
self.blog = blog
self.service = service
}

func refresh() async -> Void {
await withUnsafeContinuation { continuation in
service.syncSettings(for: blog, success: {
continuation.resume()
}, failure: { error in
continuation.resume()
DDLogError("Error while refreshing blog settings: \(error)")
})
}
}

func updateSiteTitle(_ value: String) {
guard value != blog.settings?.name else { return }
blog.settings?.name = value
save()
trackSettingsChange(fieldName: "site_title")
}

private func save() {
service.updateSettings(for: blog, success: {
NotificationCenter.default.post(name: .WPBlogSettingsUpdated, object: nil)
}, failure: { [weak self] error in
self?.onDismissableError.send(Strings.saveFailed)
DDLogError("Error while trying to update BlogSettings: \(error)")
})
}

private func trackSettingsChange(fieldName: String, value: Any? = nil) {
WPAnalytics.trackSettingsChange("site_settings", fieldName: fieldName, value: value)
}
}

private extension SiteSettingsViewModel {
enum Strings {
static let saveFailed = NSLocalizedString("siteSettings.updateFailedMessage", value: "Settings update failed", comment: "Message to show when setting save failed")
}
}
26 changes: 26 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@
0C391E612A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */; };
0C391E622A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */; };
0C391E642A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E632A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift */; };
0C60DF792A377C3800509C91 /* SiteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */; };
0C60DF7A2A377C3800509C91 /* SiteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF782A377C3800509C91 /* SiteSettingsView.swift */; };
0C60DF7E2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */; };
0C60DF7F2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */; };
0C60DF812A37CFED00509C91 /* SwiftUI+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF802A37CFED00509C91 /* SwiftUI+Backport.swift */; };
0C60DF822A37CFED00509C91 /* SwiftUI+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60DF802A37CFED00509C91 /* SwiftUI+Backport.swift */; };
0CB4056B29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; };
0CB4056C29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */; };
0CB4056E29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */; };
Expand Down Expand Up @@ -6054,6 +6060,9 @@
0C391E5D2A2FE5350040EA91 /* DashboardBlazeCampaignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignView.swift; sourceTree = "<group>"; };
0C391E602A3002950040EA91 /* DashboardBlazeCampaignStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignStatusView.swift; sourceTree = "<group>"; };
0C391E632A312DB20040EA91 /* DashboardBlazeCampaignViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignViewModelTests.swift; sourceTree = "<group>"; };
0C60DF782A377C3800509C91 /* SiteSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsView.swift; sourceTree = "<group>"; };
0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsViewModel.swift; sourceTree = "<group>"; };
0C60DF802A37CFED00509C91 /* SwiftUI+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Backport.swift"; sourceTree = "<group>"; };
0CB4056A29C78F06008EED0A /* BlogDashboardPersonalizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationService.swift; sourceTree = "<group>"; };
0CB4056D29C7BA63008EED0A /* BlogDashboardPersonalizationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationServiceTests.swift; sourceTree = "<group>"; };
0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -10025,6 +10034,15 @@
path = Mocks;
sourceTree = "<group>";
};
0C60DF7C2A37C8E800509C91 /* Settings */ = {
isa = PBXGroup;
children = (
0C60DF782A377C3800509C91 /* SiteSettingsView.swift */,
0C60DF7D2A37C90200509C91 /* SiteSettingsViewModel.swift */,
);
path = Settings;
sourceTree = "<group>";
};
0CB4056F29C8DCD7008EED0A /* BlogPersonalization */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -14755,6 +14773,7 @@
FA5C74091C596E69000B528C /* Site Management */,
FA73D7E72798766300DF24B3 /* Site Picker */,
3F43603823F36A76001DEE70 /* Site Settings */,
0C60DF7C2A37C8E800509C91 /* Settings */,
BE6AB7FB1BC62E0B00D980FC /* Style */,
);
path = Blog;
Expand Down Expand Up @@ -15225,6 +15244,7 @@
C7AFF873283C0ADC000E01DF /* UIApplication+Helpers.swift */,
C3AB4878292F114A001F7AF8 /* UIApplication+AppAvailability.swift */,
F49D7BEA29DF329500CB93A5 /* UIPopoverPresentationController+PopoverAnchor.swift */,
0C60DF802A37CFED00509C91 /* SwiftUI+Backport.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -21356,6 +21376,7 @@
E64384831C628FCC0052ADB5 /* WPStyleGuide+Sharing.swift in Sources */,
F504D2B025D60C5900A2764C /* StoryPoster.swift in Sources */,
981C82B62193A7B900A06E84 /* Double+Stats.swift in Sources */,
0C60DF792A377C3800509C91 /* SiteSettingsView.swift in Sources */,
175507B327A062980038ED28 /* PublicizeConnectionURLMatcher.swift in Sources */,
177074851FB209F100951A4A /* CircularProgressView.swift in Sources */,
3F8B45A9292C1F2C00730FA4 /* DashboardMigrationSuccessCell.swift in Sources */,
Expand Down Expand Up @@ -21568,6 +21589,7 @@
C81CCD81243BF7A600A83E27 /* TenorService.swift in Sources */,
8B51844525893F140085488D /* FilterBarView.swift in Sources */,
E105205B1F2B1CF400A948F6 /* BlogToBlogMigration_61_62.swift in Sources */,
0C60DF7E2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */,
FE3E83E526A58646008CE851 /* ListSimpleOverlayView.swift in Sources */,
98B88452261E4E09007ED7F8 /* LikeUserTableViewCell.swift in Sources */,
E16FB7E31F8B61040004DD9F /* WebKitViewController.swift in Sources */,
Expand Down Expand Up @@ -22315,6 +22337,7 @@
176CE91627FB44C100F1E32B /* StatsBaseCell.swift in Sources */,
E1C2260723901AAD0021D03C /* WordPressOrgRestApi+WordPress.swift in Sources */,
B53B02B31CAC3AAC003190A0 /* GravatarPickerViewController.swift in Sources */,
0C60DF812A37CFED00509C91 /* SwiftUI+Backport.swift in Sources */,
D81879D920ABC647000CFA95 /* ReaderTableConfiguration.swift in Sources */,
3F758FD524F6FB4900BBA2FC /* AnnouncementsStore.swift in Sources */,
E6D2E1691B8AAD9B0000ED14 /* ReaderListStreamHeader.swift in Sources */,
Expand Down Expand Up @@ -23776,6 +23799,7 @@
FABB20F02602FC2C00C8785C /* ReaderDetailToolbar.swift in Sources */,
80A2154429D1177A002FE8EB /* RemoteConfigDebugViewController.swift in Sources */,
FAD1263D2A0CF2F50004E24C /* String+NonbreakingSpace.swift in Sources */,
0C60DF7A2A377C3800509C91 /* SiteSettingsView.swift in Sources */,
FABB20F12602FC2C00C8785C /* RecentSitesService.swift in Sources */,
FA332AD129C1F97A00182FBB /* MovedToJetpackViewController.swift in Sources */,
8BD66ED52787530C00CCD95A /* PostsCardViewModel.swift in Sources */,
Expand Down Expand Up @@ -24310,6 +24334,7 @@
FABB226C2602FC2C00C8785C /* ParentPageSettingsViewController.swift in Sources */,
FABB226D2602FC2C00C8785C /* Queue.swift in Sources */,
FABB226E2602FC2C00C8785C /* SwitchTableViewCell.swift in Sources */,
0C60DF822A37CFED00509C91 /* SwiftUI+Backport.swift in Sources */,
3FBB2D2C27FB6CB200C57BBF /* SiteNameViewController.swift in Sources */,
17171375265FAA8A00F3A022 /* BloggingRemindersNavigationController.swift in Sources */,
FABB226F2602FC2C00C8785C /* ReaderTopicToReaderSiteTopic37to38.swift in Sources */,
Expand Down Expand Up @@ -24879,6 +24904,7 @@
FABB241A2602FC2C00C8785C /* PostToPost30To31.m in Sources */,
FABB241B2602FC2C00C8785C /* GutenGhostView.swift in Sources */,
FABB241C2602FC2C00C8785C /* ModelSettableCell.swift in Sources */,
0C60DF7F2A37C90200509C91 /* SiteSettingsViewModel.swift in Sources */,
FABB241D2602FC2C00C8785C /* FollowCommentsService.swift in Sources */,
4AA33F022999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift in Sources */,
FABB241E2602FC2C00C8785C /* ForcePopoverPresenter.swift in Sources */,
Expand Down