Skip to content

Commit 2a18c7b

Browse files
committed
Added privacy manifest
Updated to support swift 6 concurrency. Completions now required to be @sendable. WordpressDate now Sendable Added support for watchOS, tvOS and visionOS Removed data(from url:) extension to URLSession as it has been backported to iOS 14 Removed async from WordpressSite itemStream and added the ability to terminate the stream correctly. Fixed stream so it uses the supplied urlSession from WordpressRequest Updated Readme
1 parent 4bbf920 commit 2a18c7b

9 files changed

+86
-69
lines changed

Package.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -7,26 +7,28 @@ let package = Package(
77
name: "WordpressReader",
88
platforms: [
99
.iOS(.v14),
10-
.macOS(.v11)
10+
.macOS(.v11),
11+
.watchOS(.v7),
12+
.tvOS(.v14),
13+
.visionOS(.v1)
1114
],
1215
products: [
1316
// Products define the executables and libraries a package produces, and make them visible to other packages.
1417
.library(
1518
name: "WordpressReader",
1619
targets: ["WordpressReader"]),
1720
],
18-
dependencies: [
19-
// Dependencies declare other packages that this package depends on.
20-
// .package(url: /* package url */, from: "1.0.0"),
21-
],
2221
targets: [
2322
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2423
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2524
.target(
2625
name: "WordpressReader",
27-
dependencies: []),
26+
resources: [.copy("PrivacyInfo.xcprivacy")]
27+
),
2828
.testTarget(
2929
name: "WordpressReaderTests",
30-
dependencies: ["WordpressReader"]),
31-
]
30+
dependencies: ["WordpressReader"]
31+
),
32+
],
33+
swiftLanguageVersions: [.v5, .version("6")]
3234
)

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,28 @@
1111
# Overview
1212
A simple asynchronous way to download and decode public Wordpress content.
1313

14-
# WordpressReaderExample
15-
Check out the [example app](https://github.com/ryanlintott/WordpressReaderExample) to see how you can use this package in your iOS app.
14+
# Demo App
15+
Check out [WordpressReaderExample](https://github.com/ryanlintott/WordpressReaderExample) to see how you can use this package in your iOS app.
16+
17+
# Installation and Usage
18+
This package is compatible with iOS 14+, macOS 11+, watchOS 7+, tvOS 14+, and visionOS.
1619

17-
# Installation
1820
1. In Xcode go to `File -> Add Packages`
1921
2. Paste in the repo's url: `https://github.com/ryanlintott/WordpressReader` and select by version.
20-
21-
# Usage
22-
Import the package using `import WordpressReader`
23-
24-
# Platforms
25-
This package is compatible with iOS 14 or later.
22+
3. Import the package using `import WordpressReader`
2623

2724
# Is this Production-Ready?
2825
Really it's up to you. I currently use this package in my own [Old English Wordhord app](https://oldenglishwordhord.com/app).
2926

27+
Additionally, if you find a bug or want a new feature add an issue and I will get back to you about it.
28+
3029
# Support
31-
If you like this package, buy me a coffee to say thanks!
30+
WordpressReader is open source and free but if you like using it, please consider supporting my work.
3231

3332
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/X7X04PU6T)
3433

3534
- - -
36-
# Details
35+
# Features
3736

3837
Create an instance of WordpressSite for any Wordpress.com website:
3938

Sources/WordpressReader/Extensions/URLSession+async.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,6 @@ import Foundation
1010
internal extension URLSession {
1111
typealias ProjectError = WordpressReaderError
1212

13-
@available(iOS, obsoleted: 15.0, message: "This extension is no longer necessary as it's built into URLSession")
14-
@available(macOS, obsoleted: 12.0, message: "This extension is no longer necessary as it's built into URLSession")
15-
@available(visionOS, obsoleted: 1.0, message: "This extension is no longer necessary as it's built into URLSession")
16-
/// Retrieves the contents of a URL and delivers the data asynchronously.
17-
/// - Parameter url: The URL to retrieve
18-
/// - Returns: An asynchronously-delivered tuple that contains the URL contents as a Data instance, and a URLResponse.
19-
/// - Throws: Error if there is a bad response.
20-
func data(from url: URL) async throws -> (Data, URLResponse) {
21-
try Task.checkCancellation()
22-
return try await withCheckedThrowingContinuation { continuation in
23-
dataTask(with: url) { data, response, error in
24-
if let error = error {
25-
continuation.resume(throwing: error)
26-
} else if let data = data, let response = response {
27-
continuation.resume(returning: (data, response))
28-
} else {
29-
continuation.resume(throwing: ProjectError.unknown())
30-
}
31-
}.resume()
32-
}
33-
}
34-
3513
/// Retrieves the contents of a URL and delivers the data decoded as a specified type asynchronously.
3614
/// - Parameters:
3715
/// - type: Type to decode from the data.
@@ -48,7 +26,6 @@ internal extension URLSession {
4826
dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
4927
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
5028
) async throws -> T {
51-
5229
let (data, response) = try await data(from: url)
5330

5431
guard let response = response as? HTTPURLResponse else {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyCollectedDataTypes</key>
6+
<array/>
7+
<key>NSPrivacyAccessedAPITypes</key>
8+
<array/>
9+
</dict>
10+
</plist>

Sources/WordpressReader/WordpressDate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99

1010
/// An empty enum for storing Wordpress date formatters
11-
enum WordpressDate: Sendable {
11+
enum WordpressDate {
1212
/// Date formatter for Wordpress style dates
1313
///
1414
/// Details: https://core.trac.wordpress.org/ticket/41032
@@ -23,7 +23,7 @@ enum WordpressDate: Sendable {
2323
/// Strategy for decoding Wordpress style dates.
2424
/// - Parameter decoder: Decoder to apply strategy to.
2525
/// - Returns: Decoded date.
26-
static func dateDecodingStrategy(_ decoder: Decoder) throws -> Date {
26+
static func dateDecodingStrategy(_ decoder: any Decoder) throws -> Date {
2727
let container = try decoder.singleValueContainer()
2828
let dateAsString = try container.decode(String.self)
2929

Sources/WordpressReader/WordpressRequest.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public extension WordpressRequest {
6161
}
6262
}
6363

64-
public extension WordpressRequest where T == WordpressPost {
64+
public extension WordpressRequest<WordpressPost> {
6565
/// Creates a Wordpress request for Posts.
6666
///
6767
/// A custom URL session, start page and max pages can all be set after creation. A fields query item will be ignored and replaced with fields based on the parameter labels for WordpressPost.
@@ -72,35 +72,35 @@ public extension WordpressRequest where T == WordpressPost {
7272
}
7373
}
7474

75-
public extension WordpressRequest where T == WordpressPage {
75+
public extension WordpressRequest<WordpressPage> {
7676
/// Creates a Wordpress request for Pages.
7777
///
7878
/// A custom URL session, start page and max pages can all be set after creation. A fields query item will be ignored and replaced with fields based on the parameter labels for WordpressPage.
7979
/// - Parameter queryItems: Query items used in this request. (default is an empty array)
8080
/// - Returns: A Wordpress request for Pages.
81-
static func pages(_ queryItems: Set<WordpressQueryItem> = []) -> WordpressRequest<WordpressPage> {
81+
static func pages(_ queryItems: Set<WordpressQueryItem> = []) -> Self {
8282
.init(queryItems: queryItems)
8383
}
8484
}
8585

86-
public extension WordpressRequest where T == WordpressCategory {
86+
public extension WordpressRequest<WordpressCategory> {
8787
/// Creates a Wordpress request for Categories.
8888
///
8989
/// A custom URL session, start page and max pages can all be set after creation. A fields query item will be ignored and replaced with fields based on the parameter labels for WordpressCategory.
9090
/// - Parameter queryItems: Query items used in this request. (default is an empty array)
9191
/// - Returns: A Wordpress request for Categories.
92-
static func categories(_ queryItems: Set<WordpressQueryItem> = []) -> WordpressRequest<WordpressCategory> {
92+
static func categories(_ queryItems: Set<WordpressQueryItem> = []) -> Self {
9393
.init(queryItems: queryItems)
9494
}
9595
}
9696

97-
public extension WordpressRequest where T == WordpressTag {
97+
public extension WordpressRequest<WordpressTag> {
9898
/// Creates a Wordpress request for Tags.
9999
///
100100
/// A custom URL session, start page and max pages can all be set after creation. A fields query item will be ignored and replaced with fields based on the parameter labels for WordpressTag.
101101
/// - Parameter queryItems: Query items used in this request. (default is an empty array)
102102
/// - Returns: A Wordpress request for Tags.
103-
static func tags(_ queryItems: Set<WordpressQueryItem> = []) -> WordpressRequest<WordpressTag> {
103+
static func tags(_ queryItems: Set<WordpressQueryItem> = []) -> Self {
104104
.init(queryItems: queryItems)
105105
}
106106
}

Sources/WordpressReader/WordpressSite+async-internal.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,24 +62,28 @@ extension WordpressSite {
6262
urlSession: URLSession = .shared,
6363
_ type: T.Type,
6464
urls: [URL]
65-
) async -> AsyncThrowingStream<[T], Error> {
65+
) -> AsyncThrowingStream<[T], Error> {
6666
AsyncThrowingStream { continuation in
67-
Task {
67+
let task = Task {
6868
try await withThrowingTaskGroup(of: [T].self) { group in
6969
for url in urls {
7070
group.addTask {
7171
try Task.checkCancellation()
72-
let batch = try await urlSession.fetchJsonData([T].self, url: url, dateDecodingStrategy: .wordpressDate)
73-
return batch
72+
return try await urlSession.fetchJsonData([T].self, url: url, dateDecodingStrategy: .wordpressDate)
7473
}
7574
}
7675

7776
for try await batch in group {
7877
continuation.yield(batch)
7978
}
79+
8080
continuation.finish()
8181
}
8282
}
83+
84+
continuation.onTermination = { _ in
85+
task.cancel()
86+
}
8387
}
8488
}
8589

@@ -95,6 +99,6 @@ extension WordpressSite {
9599
_ type: T.Type,
96100
urls: [URL]
97101
) async throws -> [T] {
98-
try await itemStream(urlSession: urlSession, type, urls: urls).reduce(into: [], { $0 += $1 })
102+
try await itemStream(urlSession: urlSession, type, urls: urls).reduce(into: [], +=)
99103
}
100104
}

Sources/WordpressReader/WordpressSite+async-public.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ extension WordpressSite {
4949
_ request: WordpressRequest<T>
5050
) async throws -> AsyncThrowingStream<[T], Error> {
5151
let urls = try await fetchPaginatedUrls(request)
52-
return await itemStream(T.self, urls: urls)
52+
return itemStream(urlSession: request.urlSession, T.self, urls: urls)
5353
}
5454

5555
/// Asynchronously returns an array of Wordpress items.

Sources/WordpressReader/WordpressSite+closures.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,25 @@ extension WordpressSite {
3232
}
3333
}
3434

35-
@available(*, renamed: "itemStream(_:)")
35+
@available(*, renamed: "stream(_:)")
36+
public func fetch<T: WordpressContent>(
37+
_ request: WordpressRequest<T>,
38+
batchCompletion: @Sendable @escaping (Result<[T], Error>) -> Void,
39+
completion: (@Sendable () -> Void)? = nil
40+
) {
41+
Task {
42+
do {
43+
for try await batch in try await stream(request) {
44+
batchCompletion(.success(batch))
45+
}
46+
} catch let error {
47+
batchCompletion(.failure(error))
48+
}
49+
completion?()
50+
}
51+
}
52+
53+
@available(*, renamed: "stream(_:)")
3654
public func fetchContent<T: WordpressContent>(
3755
_ type: T.Type,
3856
postedAfter: Date? = nil,
@@ -45,29 +63,29 @@ extension WordpressSite {
4563
perPage: Int? = nil,
4664
maxNumPages: Int? = nil,
4765
batchCompletion: @Sendable @escaping (Result<[T], Error>) -> Void,
48-
completion: (() -> Void)? = nil
66+
completion: (@Sendable () -> Void)? = nil
4967
) {
5068
Task {
5169
var queryItems: Set<WordpressQueryItem> = []
52-
if let postedAfter = postedAfter {
70+
if let postedAfter {
5371
queryItems.update(with: .postedAfter(postedAfter))
5472
}
55-
if let postedBefore = postedBefore {
73+
if let postedBefore {
5674
queryItems.update(with: .postedBefore(postedBefore))
5775
}
58-
if let modifiedAfter = modifiedAfter {
76+
if let modifiedAfter {
5977
queryItems.update(with: .modifiedAfter(modifiedAfter))
6078
}
61-
if let modifiedBefore = modifiedBefore {
79+
if let modifiedBefore {
6280
queryItems.update(with: .modifiedBefore(modifiedBefore))
6381
}
64-
if let orderBy = orderBy {
82+
if let orderBy {
6583
queryItems.update(with: .orderBy(orderBy))
6684
}
67-
if let order = order {
85+
if let order {
6886
queryItems.update(with: .order(order))
6987
}
70-
if let perPage = perPage {
88+
if let perPage {
7189
queryItems.update(with: .perPage(perPage))
7290
}
7391
var request = WordpressRequest<T>(queryItems: queryItems)
@@ -87,7 +105,11 @@ extension WordpressSite {
87105
}
88106

89107
@available(*, renamed: "itemStream(_:)")
90-
public func fetchItems<T: WordpressItem>(_ type: T.Type, batchCompletion: @Sendable @escaping (Result<[T], Error>) -> Void, completion: (() -> Void)? = nil) {
108+
public func fetchItems<T: WordpressItem>(
109+
_ type: T.Type,
110+
batchCompletion: @Sendable @escaping (Result<[T], Error>) -> Void,
111+
completion: (@Sendable () -> Void)? = nil
112+
) {
91113
Task {
92114
let request = WordpressRequest<T>()
93115
do {
@@ -103,7 +125,10 @@ extension WordpressSite {
103125
}
104126

105127
@available(*, renamed: "fetchItems(_:)")
106-
public func fetchAllItems<T: WordpressItem>(_ type: T.Type, completion: @Sendable @escaping (Result<[T], Error>) -> Void) {
128+
public func fetchAllItems<T: WordpressItem>(
129+
_ type: T.Type,
130+
completion: @Sendable @escaping (Result<[T], Error>) -> Void
131+
) {
107132
Task {
108133
do {
109134
let result = try await fetch(type)

0 commit comments

Comments
 (0)