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
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions native/swift/Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
242132C82CE69CE80021D8E8 /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 242132C72CE69CE80021D8E8 /* WordPressAPI */; };
242D648E2C3602C1007CA96C /* ListViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D648D2C3602C1007CA96C /* ListViewData.swift */; };
242D64922C360687007CA96C /* RootListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64912C360687007CA96C /* RootListView.swift */; };
242D64942C3608C6007CA96C /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64932C3608C6007CA96C /* ListView.swift */; };
Expand All @@ -18,6 +19,7 @@
2479BF932B621E9B0014A01D /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* ListViewModel.swift */; };
24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */; };
24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C3352BAA874C00162AD1 /* LoginManager.swift */; };
24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 24E77D022CE44DD900F6998C /* WordPressAPI */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -41,6 +43,8 @@
buildActionMask = 2147483647;
files = (
2479BF912B621CCA0014A01D /* WordPressAPI in Frameworks */,
242132C82CE69CE80021D8E8 /* WordPressAPI in Frameworks */,
24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -123,6 +127,8 @@
name = Example;
packageProductDependencies = (
2479BF902B621CCA0014A01D /* WordPressAPI */,
24E77D022CE44DD900F6998C /* WordPressAPI */,
242132C72CE69CE80021D8E8 /* WordPressAPI */,
);
productName = Example;
productReference = 2479BF7D2B621CB60014A01D /* Example.app */;
Expand Down Expand Up @@ -153,7 +159,7 @@
);
mainGroup = 2479BF742B621CB60014A01D;
packageReferences = (
2479BF8F2B621CCA0014A01D /* XCLocalSwiftPackageReference "../../.." */,
242132C62CE69CE80021D8E8 /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */,
);
productRefGroup = 2479BF7E2B621CB60014A01D /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -409,17 +415,25 @@
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
2479BF8F2B621CCA0014A01D /* XCLocalSwiftPackageReference "../../.." */ = {
242132C62CE69CE80021D8E8 /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../..;
relativePath = "../../../../wordpress-rs";
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
242132C72CE69CE80021D8E8 /* WordPressAPI */ = {
isa = XCSwiftPackageProductDependency;
productName = WordPressAPI;
};
2479BF902B621CCA0014A01D /* WordPressAPI */ = {
isa = XCSwiftPackageProductDependency;
productName = WordPressAPI;
};
24E77D022CE44DD900F6998C /* WordPressAPI */ = {
isa = XCSwiftPackageProductDependency;
productName = WordPressAPI;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 2479BF752B621CB60014A01D /* Project object */;
Expand Down
16 changes: 10 additions & 6 deletions native/swift/Example/Example/ExampleApp.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import SwiftUI
import WordPressAPI
import Combine

private let userListParams = UserListParams(perPage: 5)
private let postListParams = PostListParams(perPage: 5)

@main
struct ExampleApp: App {
Expand All @@ -13,9 +17,9 @@ struct ExampleApp: App {
.data
.map { $0.asListViewData }
}),
RootListData(name: "Users", callback: {
try await WordPressAPI.globalInstance.users.paginatedWithEditContext(params: UserListParams(perPage: 100))
.map { $0.asListViewData }
RootListData(name: "Users", sequence: {
let sequence = try WordPressAPI.globalInstance.users.sequenceWithEditContext(params: userListParams)
return ListViewSequence(underlyingSequence: sequence)
}),
RootListData(name: "Plugins", callback: {
try await WordPressAPI.globalInstance.plugins.listWithEditContext(params: .init())
Expand All @@ -27,9 +31,9 @@ struct ExampleApp: App {
value.asListViewData
}
}),
RootListData(name: "Posts", callback: {
try await WordPressAPI.globalInstance.posts.paginatedWithEditContext(params: PostListParams(perPage: 100))
.map { $0.asListViewData }
RootListData(name: "Posts", sequence: {
let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams)
return ListViewSequence(underlyingSequence: sequence)
}),
RootListData(name: "Site Health Tests", callback: {
let items: [any ListViewDataConvertable] = [
Expand Down
19 changes: 18 additions & 1 deletion native/swift/Example/Example/ListViewData.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import Foundation
import WordPressAPI
import WordPressAPIInternal

struct ListViewData: Identifiable {
struct ListViewData: Identifiable, Comparable, Hashable {
let id: String
let title: String
let subtitle: String
let fields: [String: String]

static func < (lhs: ListViewData, rhs: ListViewData) -> Bool {
lhs.title < rhs.title
}
}

protocol ListViewDataConvertable: Identifiable {
Expand Down Expand Up @@ -151,3 +156,15 @@ extension PostWithEditContext: ListViewDataConvertable {
ListViewData(id: self.id, title: self.title.raw, subtitle: self.slug, fields: [:])
}
}

extension [PostWithEditContext] {
func asListViewData() -> [ListViewData] {
self.map { $0.asListViewData }
}
}

extension [ListViewDataConvertable] {
func asListViewData() -> [ListViewData] {
self.map { $0.asListViewData }
}
}
114 changes: 91 additions & 23 deletions native/swift/Example/Example/ListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,83 @@ import Foundation
import SwiftUI
import WordPressAPI

@Observable class ListViewModel {
@MainActor
protocol ListViewModel {

/// Guarantee only one object with each ID, but allow updating the object when new data comes in
var listItems: [String: ListViewData] { get }

var shouldPresentAlert: Bool { get set }

var error: MyError? { get set }

func task() async
}

@Observable class SequenceListViewModel: ListViewModel {
var listItems: [String: ListViewData] = [String: ListViewData](minimumCapacity: 250)

typealias SequenceProvider = () throws -> ListViewSequence

private let sequenceProvider: SequenceProvider

init(sequenceProvider: @escaping SequenceProvider) {
self.sequenceProvider = sequenceProvider
}

var shouldPresentAlert: Bool = false

var error: MyError?

var sequence: ListViewSequence?

func task() async {
do {
for try await page in try self.sequenceProvider() {
for item in page {
self.listItems[item.id] = item
}
}
} catch {
self.error = .init(underlyingError: error)
self.shouldPresentAlert = true
}
}

func reset() {

}
}

@Observable class TaskListViewModel: ListViewModel {

typealias FetchDataTask = () async throws -> [ListViewData]

var listItems: [ListViewData] = []
var listItems: [String: ListViewData] = [:]
private var dataCallback: FetchDataTask
private var dataTask: Task<Void, any Error>?
var isLoading: Bool = false

var error: MyError?
var shouldPresentAlert = false

let loginManager: LoginManager

init(loginManager: LoginManager, dataCallback: @escaping FetchDataTask) {
self.loginManager = loginManager
init(dataCallback: @escaping FetchDataTask) {
self.dataCallback = dataCallback
}

func startFetching() {
self.error = nil
func task() async {
self.isLoading = true
self.shouldPresentAlert = false

self.dataTask = Task { @MainActor in
self.isLoading = true
self.shouldPresentAlert = false

do {
self.listItems = try await dataCallback()
} catch {
self.error = MyError(underlyingError: error)
self.shouldPresentAlert = true
do {
for item in try await dataCallback() {
listItems[item.id] = item
}

self.isLoading = false
} catch {
self.error = MyError(underlyingError: error)
self.shouldPresentAlert = true
}
}

func stopFetching() {
self.dataTask?.cancel()
self.isLoading = false
}
}

Expand All @@ -60,3 +97,34 @@ struct MyError: LocalizedError {
underlyingError.localizedDescription
}
}

struct ListViewSequence: AsyncSequence {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this struct implementation equavilant of asyncSequence.compactMap({ ($0 as? [ListViewDataConvertable]).asListViewData() })?

typealias Element = [ListViewData]

private let underlyingSequence: any AsyncSequence

init(underlyingSequence: any AsyncSequence) {
self.underlyingSequence = underlyingSequence
}

struct ListViewIterator: AsyncIteratorProtocol {
var underlyingSequence: any AsyncIteratorProtocol

mutating func next() async throws -> Element? {
guard let nextElement = try await underlyingSequence.next() else {
return nil
}

guard let listViewData = nextElement as? [any ListViewDataConvertable] else {
debugPrint("Unable to convert data to `ListViewDataConvertable`")
return nil
}

return listViewData.asListViewData()
}
}

func makeAsyncIterator() -> ListViewIterator {
ListViewIterator(underlyingSequence: underlyingSequence.makeAsyncIterator())
}
}
9 changes: 5 additions & 4 deletions native/swift/Example/Example/UI/ListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct ListView: View {
var viewModel: ListViewModel

var body: some View {
List(viewModel.listItems) { item in
List(viewModel.listItems.values.sorted(), id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.subtitle).font(.footnote)
Expand All @@ -29,14 +29,15 @@ struct ListView: View {
}
}
)
.onAppear(perform: viewModel.startFetching)
.onDisappear(perform: viewModel.stopFetching)
.task {
await viewModel.task()
}
}
}

#Preview {

let viewModel = ListViewModel(loginManager: LoginManager(), dataCallback: {
let viewModel = TaskListViewModel(dataCallback: {
[
ListViewData(id: "1234", title: "Item 1", subtitle: "Subtitle", fields: [:])
]
Expand Down
49 changes: 36 additions & 13 deletions native/swift/Example/Example/UI/RootListView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI
import WordPressAPI
import Combine

struct RootListView: View {

Expand All @@ -16,28 +17,50 @@ struct RootListViewItem: View {
let item: RootListData

var body: some View {
VStack(alignment: .leading, spacing: 4.0) {
NavigationLink {
ListView(
viewModel: ListViewModel(
loginManager: LoginManager(),
dataCallback: self.item.callback
switch item {
case .callback(let name, let fetchDataTask):
VStack(alignment: .leading, spacing: 4.0) {
NavigationLink {
ListView(
viewModel: TaskListViewModel(dataCallback: fetchDataTask)
)
)
} label: {
Text(item.name)
} label: {
Text(name)
}
}

case .sequence(let name, let sequenceProvider):
VStack(alignment: .leading, spacing: 4.0) {
NavigationLink {
ListView(
viewModel: SequenceListViewModel(sequenceProvider: sequenceProvider)
)
} label: {
Text(name)
}
}
}
}
}

struct RootListData: Identifiable {
enum RootListData: Identifiable {

let name: String
let callback: ListViewModel.FetchDataTask
case callback(String, TaskListViewModel.FetchDataTask)
case sequence(String, SequenceListViewModel.SequenceProvider)

var id: String {
self.name
switch self {
case .callback(let id, _): id
case .sequence(let id, _): id
}
}

init(name: String, callback: @escaping TaskListViewModel.FetchDataTask) {
self = .callback(name, callback)
}

init(name: String, sequence: @escaping SequenceListViewModel.SequenceProvider) {
self = .sequence(name, sequence)
}
}

Expand Down
Loading