Skip to content
Merged
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
41 changes: 20 additions & 21 deletions Modules/Sources/WordPressCore/ApiCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@ import WordPressAPIInternal
import WordPressApiCache

extension WordPressApiCache {
static func bootstrap() -> WordPressApiCache? {
let instance: WordPressApiCache? = .onDiskCache() ?? .memoryCache()
instance?.startListeningForUpdates()
static func bootstrap() -> WordPressApiCache {
let instance: WordPressApiCache = .onDiskCache() ?? .memoryCache()
instance.startListeningForUpdates()
return instance
}

// TODO:
// - Log errors to sentry: https://github.com/wordpress-mobile/WordPress-iOS/pull/25157#discussion_r2785458461
Copy link
Contributor

Choose a reason for hiding this comment

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

It's pretty easy to do with wpAssertionFailure and error included in user info. I did use these to monitor sync errors for posts.

private static func onDiskCache() -> WordPressApiCache? {
let cacheURL: URL
do {
cacheURL = try FileManager.default
.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appending(path: "app.sqlite")
} catch {
NSLog("Failed to create api cache file: \(error)")
return nil
}
let cacheURL = URL.libraryDirectory.appending(path: "app.sqlite")

if let cache = WordPressApiCache.onDiskCache(file: cacheURL) {
return cache
Expand Down Expand Up @@ -56,17 +50,22 @@ extension WordPressApiCache {
return nil
}

return cache
}

private static func memoryCache() -> WordPressApiCache? {
do {
let cache = try WordPressApiCache()
_ = try cache.performMigrations()
return cache
var url = file
var values = URLResourceValues()
values.isExcludedFromBackup = true
try url.setResourceValues(values)
} catch {
NSLog("Failed to create memory cache: \(error)")
return nil
NSLog("Failed exclude the database file from iCloud backup: \(error)")
Copy link
Contributor

Choose a reason for hiding this comment

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

(nit) I'd suggest using wpAssertionFailure here in in other places and avoid force unwraps where possible.

}

return cache
}

private static func memoryCache() -> WordPressApiCache {
// Creating an in-memory database should always succeed.
let cache = try! WordPressApiCache()
_ = try! cache.performMigrations()
return cache
}
}
26 changes: 13 additions & 13 deletions Modules/Sources/WordPressCore/WordPressClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,26 +80,26 @@ public actor WordPressClient {
public let api: any WordPressClientAPI

private var _cache: WordPressApiCache?
public var cache: WordPressApiCache? {
public var cache: WordPressApiCache {
get {
if _cache == nil {
_cache = WordPressApiCache.bootstrap()
if let _cache {
return _cache
}
return _cache
let cache = WordPressApiCache.bootstrap()
_cache = cache
return cache
}
}

private var _service: WpSelfHostedService?
public var service: WpSelfHostedService? {
get {
if _service == nil, let cache {
do {
_service = try api.createSelfHostedService(cache: cache)
} catch {
NSLog("Failed to create service: \(error)")
}
public var service: WpSelfHostedService {
get throws {
if let _service {
return _service
}
return _service
let service = try api.createSelfHostedService(cache: cache)
Copy link
Contributor

Choose a reason for hiding this comment

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

(nit) ideally, this will be created once and you'd need to handle errors in one place. By making a method throws every invocation has to decide how to handle potential errors even though this code runs only once in practice.

_service = service
return service
}
}

Expand Down
12 changes: 6 additions & 6 deletions Modules/Sources/WordPressUI/Views/AdaptiveTabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ public class AdaptiveTabBar: UIControl {

// MARK: - Properties

var items: [any AdaptiveTabBarItem] = [] {
public var items: [any AdaptiveTabBarItem] = [] {
didSet { refreshTabs() }
}

private var buttons: [TabButton] = []

private(set) var selectedIndex: Int = 0 {
public private(set) var selectedIndex: Int = 0 {
didSet {
buttons[oldValue].isSelected = false
buttons[selectedIndex].isSelected = true
Expand All @@ -58,7 +58,7 @@ public class AdaptiveTabBar: UIControl {
private var indicatorCenterXConstraint: NSLayoutConstraint?
private var previousWidth: CGFloat?

public let tabBarHeight: CGFloat = 40
public static let tabBarHeight: CGFloat = 40

// MARK: - Initialization

Expand All @@ -80,7 +80,7 @@ public class AdaptiveTabBar: UIControl {
stackView.pinEdges()

NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: tabBarHeight),
heightAnchor.constraint(equalToConstant: AdaptiveTabBar.tabBarHeight),
stackView.heightAnchor.constraint(equalTo: heightAnchor)
])

Expand Down Expand Up @@ -161,7 +161,7 @@ public class AdaptiveTabBar: UIControl {

// Calculate preferred width for each button
let preferredWidths = buttons.map {
$0.systemLayoutSizeFitting(CGSize(width: UIView.noIntrinsicMetric, height: tabBarHeight)).width
$0.systemLayoutSizeFitting(CGSize(width: UIView.noIntrinsicMetric, height: AdaptiveTabBar.tabBarHeight)).width
}

let maxWidth = preferredWidths.max() ?? 0
Expand All @@ -188,7 +188,7 @@ public class AdaptiveTabBar: UIControl {

// MARK: - Selection

func setSelectedIndex(_ index: Int, animated: Bool = true) {
public func setSelectedIndex(_ index: Int, animated: Bool = true) {
guard items.indices.contains(index) else { return }

UIView.performWithoutAnimation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public final class AdaptiveTabBarController<Item: AdaptiveTabBarItem> {
private func setupFilterBar() {
// filterBarContainer.backgroundColor = .systemGroupedBackground // .secondarySystemGroupedBackground
filterBarContainer.contentView.addSubview(filterBar)
filterBar.pinEdges(.top, to: filterBarContainer.safeAreaLayoutGuide, insets: UIEdgeInsets(.top, -filterBar.tabBarHeight))
filterBar.pinEdges(.top, to: filterBarContainer.safeAreaLayoutGuide, insets: UIEdgeInsets(.top, -AdaptiveTabBar.tabBarHeight))
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like it includes some unrelated changes. Does it need a rebase?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This changes are related (see #25233 (comment)). I extracted the value to a constant, to be reused in the CPT list view.

filterBar.pinEdges([.horizontal, .bottom])

filterBar.addTarget(self, action: #selector(selectedFilterDidChange), for: .valueChanged)
Expand Down Expand Up @@ -98,7 +98,7 @@ public final class AdaptiveTabBarController<Item: AdaptiveTabBarItem> {
viewController.navigationItem.titleView = nil
viewController.view.addSubview(filterBarContainer)
filterBarContainer.pinEdges([.top, .horizontal])
viewController.additionalSafeAreaInsets = UIEdgeInsets(.top, filterBar.tabBarHeight)
viewController.additionalSafeAreaInsets = UIEdgeInsets(.top, AdaptiveTabBar.tabBarHeight)
}
}

Expand Down
99 changes: 99 additions & 0 deletions WordPress/Classes/Utility/SiteStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import SwiftUI
import WordPressData

@propertyWrapper
struct SiteStorage<Value: Codable>: DynamicProperty {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is clever. Does it work as an observable property?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added a SwiftUI preview to test this property wrapper. The preview code does not go through the .init(key:blog:) initialiser, because we don't have a blog id instance. But the core logic is tested.

@AppStorage private var data: Data
private let defaultValue: Value

var wrappedValue: Value {
get {
(try? JSONDecoder().decode(Value.self, from: data)) ?? defaultValue
}
nonmutating set {
data = (try? JSONEncoder().encode(newValue)) ?? Data()
}
}

var projectedValue: Binding<Value> {
Binding(get: { wrappedValue }, set: { wrappedValue = $0 })
}

init(wrappedValue: Value, _ key: String, blog: TaggedManagedObjectID<Blog>,
store: UserDefaults? = nil) {
self.defaultValue = wrappedValue
let scopedKey = SiteStorageAccess.scopedKey(key, blog: blog)
_data = AppStorage(wrappedValue: Data(), scopedKey, store: store)
}

fileprivate init(wrappedValue: Value, _ key: String, scope: String,
store: UserDefaults? = nil) {
self.defaultValue = wrappedValue
let scopedKey = SiteStorageAccess.scopedKey(key, scope: scope)
_data = AppStorage(wrappedValue: Data(), scopedKey, store: store)
}
}

enum SiteStorageAccess {
static func read<T: Decodable>(_ type: T.Type, key: String, blog: TaggedManagedObjectID<Blog>) -> T? {
let scopedKey = scopedKey(key, blog: blog)
guard let data = UserDefaults.standard.data(forKey: scopedKey) else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}

static func write<T: Encodable>(_ value: T, key: String, blog: TaggedManagedObjectID<Blog>) {
let scopedKey = scopedKey(key, blog: blog)
let data = (try? JSONEncoder().encode(value)) ?? Data()
UserDefaults.standard.set(data, forKey: scopedKey)
}

static func exists(key: String, blog: TaggedManagedObjectID<Blog>) -> Bool {
let scopedKey = scopedKey(key, blog: blog)
return UserDefaults.standard.object(forKey: scopedKey) != nil
}

fileprivate static var prefix: String { "site-storage" }
fileprivate static var separator: String { "|" }

fileprivate static func scopedKey(
_ key: String,
blog: TaggedManagedObjectID<Blog>
) -> String {
[prefix, blog.objectID.uriRepresentation().absoluteString, key]
.joined(separator: separator)
}

fileprivate static func scopedKey(
_ key: String,
scope: String
) -> String {
[prefix, scope, key]
.joined(separator: separator)
}
}

#if DEBUG

private struct SiteStoragePreviewContent: View {
@SiteStorage("counter", scope: "tests") private var counter = 0

var body: some View {
VStack(spacing: 20) {
Text("Counter: \(counter)")
.font(.headline)
Button("Increment") {
let key = SiteStorageAccess.scopedKey("counter", scope: "tests")

let newValue = counter + 1
let encoded = (try? JSONEncoder().encode(newValue)) ?? Data()
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}

#Preview {
SiteStoragePreviewContent()
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -589,13 +589,17 @@ private extension BlogDetailsTableViewModel {
rows.append(Row.pages(viewController: viewController))
}

rows.append(Row.media(viewController: viewController))
rows.append(Row.comments(viewController: viewController))

if FeatureFlag.customPostTypes.enabled && blog.supportsCoreRESTAPI {
let pinned = SiteStorageAccess.pinnedPostTypes(for: TaggedManagedObjectID(blog))
for type in pinned {
rows.append(Row.pinnedPostType(type, viewController: viewController))
}
rows.append(Row.customPostTypes(viewController: viewController))
}

rows.append(Row.media(viewController: viewController))
rows.append(Row.comments(viewController: viewController))

let title = isSplitViewDisplayed ? nil : Strings.contentSectionTitle
return Section(title: title, rows: rows, category: .content)
}
Expand Down Expand Up @@ -668,12 +672,16 @@ private extension BlogDetailsTableViewModel {
rows.append(Row.pages(viewController: viewController))
}

if blog.isSelfHosted {
rows.append(Row.comments(viewController: viewController))

if FeatureFlag.customPostTypes.enabled && blog.supportsCoreRESTAPI {
let pinned = SiteStorageAccess.pinnedPostTypes(for: TaggedManagedObjectID(blog))
for type in pinned {
rows.append(Row.pinnedPostType(type, viewController: viewController))
}
rows.append(Row.customPostTypes(viewController: viewController))
}

rows.append(Row.comments(viewController: viewController))

let title = Strings.publishSection
return Section(title: title, rows: rows, category: .content)
}
Expand Down Expand Up @@ -958,6 +966,7 @@ enum BlogDetailsRowKind {
case viewSite
case admin
case siteSettings
case pinnedPostType
case removeSite
}

Expand Down Expand Up @@ -1047,14 +1056,25 @@ extension Row {
static func customPostTypes(viewController: BlogDetailsViewController?) -> Row {
Row(
kind: .customPostTypes,
title: "Custom Post Types",
image: UIImage(systemName: "square.3.layers.3d"),
title: CustomPostTypesView.title,
image: UIImage(systemName: "ellipsis"),
action: { [weak viewController] _ in
viewController?.showCustomPostTypes()
}
)
}

static func pinnedPostType(_ type: PinnedPostType, viewController: BlogDetailsViewController?) -> Row {
Row(
kind: .pinnedPostType,
title: type.name,
image: UIImage(dashicon: type.icon),
action: { [weak viewController] _ in
viewController?.showPinnedPostType(type)
}
)
}

static func media(viewController: BlogDetailsViewController?) -> Row {
Row(
kind: .media,
Expand Down
Loading