Skip to content

Commit 6266d88

Browse files
authored
@Requested property wrapper and reimplementation of RequestView (#46)
* Re-implement RequestView, add Requested property wrapper * Rework RequestImage implementation * Allow nil to be passed to RequestImage init * Don't reload on every appear, only the first * Don't rely on result to perform onAppear
1 parent 83bad6e commit 6266d88

File tree

8 files changed

+265
-106
lines changed

8 files changed

+265
-106
lines changed

Sources/Request/Request/Request+Combine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ extension AnyRequest: Publisher {
1616
public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
1717
updatePublisher?
1818
.receive(subscriber: UpdateSubscriber(request: self))
19-
buildSession()
19+
buildPublisher()
2020
.subscribe(subscriber)
2121
}
2222

Sources/Request/Request/Request.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,23 +93,28 @@ public struct AnyRequest<ResponseType> where ResponseType: Decodable {
9393

9494
/// Performs the `Request`, and calls the `onData`, `onString`, `onJson`, and `onError` callbacks when appropriate.
9595
public func call() {
96-
buildSession()
96+
buildPublisher()
9797
.subscribe(self)
9898
if let updatePublisher = self.updatePublisher {
9999
updatePublisher
100100
.subscribe(UpdateSubscriber(request: self))
101101
}
102102
}
103103

104-
internal func buildSession() -> AnyPublisher<(data: Data, response: URLResponse), Error> {
104+
internal func buildSession() -> (configuration: URLSessionConfiguration, request: URLRequest) {
105105
var request = URLRequest(url: URL(string: "https://")!)
106106
let configuration = URLSessionConfiguration.default
107107

108108
rootParam.buildParam(&request)
109109
(rootParam as? SessionParam)?.buildConfiguration(configuration)
110-
110+
111+
return (configuration, request)
112+
}
113+
114+
internal func buildPublisher() -> AnyPublisher<(data: Data, response: URLResponse), Error> {
111115
// PERFORM REQUEST
112-
return URLSession(configuration: configuration).dataTaskPublisher(for: request)
116+
let session = buildSession()
117+
return URLSession(configuration: session.configuration).dataTaskPublisher(for: session.request)
113118
.mapError { $0 }
114119
.eraseToAnyPublisher()
115120
}
@@ -130,3 +135,12 @@ public struct AnyRequest<ResponseType> where ResponseType: Decodable {
130135
self.update(publisher: Timer.publish(every: seconds, on: .main, in: .common).autoconnect())
131136
}
132137
}
138+
139+
extension AnyRequest: Equatable {
140+
public static func == (lhs: AnyRequest<ResponseType>, rhs: AnyRequest<ResponseType>) -> Bool {
141+
let lhsSession = lhs.buildSession()
142+
let rhsSession = rhs.buildSession()
143+
return lhsSession.configuration == rhsSession.configuration && lhsSession.request == rhsSession.request
144+
}
145+
}
146+

Sources/Request/Request/RequestParams/Url/Url.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
/// Sets the URL of the `Request`.
1111
/// - Precondition: Only use one URL in your `Request`. To group or chain requests, use a `RequestGroup` or `RequestChain`.
12-
public struct Url: RequestParam {
12+
public struct Url: RequestParam, Codable {
1313
private let type: ProtocolType?
1414
fileprivate let path: String
1515

@@ -34,6 +34,16 @@ public struct Url: RequestParam {
3434

3535
return path
3636
}
37+
38+
public init(from decoder: Decoder) throws {
39+
let container = try decoder.singleValueContainer()
40+
self.init(try container.decode(String.self))
41+
}
42+
43+
public func encode(to encoder: Encoder) throws {
44+
var container = encoder.singleValueContainer()
45+
try container.encode(absoluteString)
46+
}
3747
}
3848

3949
extension Url: Equatable {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// Requested.swift
3+
//
4+
//
5+
// Created by Carson Katri on 1/17/21.
6+
//
7+
8+
import SwiftUI
9+
import Combine
10+
11+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
12+
@propertyWrapper
13+
public struct Requested<Value: Decodable>: DynamicProperty {
14+
@StateObject private var requestStore: RequestStore
15+
16+
final class RequestStore: ObservableObject {
17+
@Published var status: RequestStatus<Value> = .loading
18+
private var cancellable: AnyCancellable?
19+
var request: AnyRequest<Value> {
20+
didSet {
21+
call()
22+
}
23+
}
24+
25+
init(request: AnyRequest<Value>) {
26+
self.request = request
27+
call()
28+
}
29+
30+
func call() {
31+
print("Calling")
32+
self.status = .loading
33+
cancellable = request
34+
.objectPublisher
35+
.receive(on: DispatchQueue.main)
36+
.sink { [weak self] completion in
37+
switch completion {
38+
case let .failure(error):
39+
self?.status = .failure(error)
40+
case .finished: break
41+
}
42+
} receiveValue: { [weak self] result in
43+
self?.status = .success(result)
44+
}
45+
}
46+
}
47+
48+
public init(wrappedValue: AnyRequest<Value>) {
49+
self._requestStore = .init(wrappedValue: .init(request: wrappedValue))
50+
}
51+
52+
public var wrappedValue: AnyRequest<Value> {
53+
get {
54+
requestStore.request
55+
}
56+
nonmutating set {
57+
requestStore.request = newValue
58+
}
59+
}
60+
61+
public var projectedValue: RequestStatus<Value> {
62+
requestStore.status
63+
}
64+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// RequestStatus.swift
3+
//
4+
//
5+
// Created by Carson Katri on 1/17/21.
6+
//
7+
8+
public enum RequestStatus<Value: Decodable> {
9+
case loading
10+
case success(Value)
11+
case failure(Error)
12+
}

Sources/Request/SwiftUI/RequestView/RequestView.swift

Lines changed: 0 additions & 51 deletions
This file was deleted.

Sources/Request/SwiftUI/RequestImage/RequestImage.swift renamed to Sources/Request/SwiftUI/Views/RequestImage.swift

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,79 +12,77 @@ import SwiftUI
1212
/// It automatically has animations to transition from a placeholder and the image.
1313
///
1414
/// It takes a `Url` or `Request`, a placeholder, the `ContentMode` for displaying the image, and an `Animation` for switching
15-
public struct RequestImage: View {
15+
public struct RequestImage<Placeholder: View>: View {
1616
private let request: Request
17-
private let placeholder: Image
18-
private let animation: Animation
17+
private let placeholder: Placeholder
18+
private let animation: Animation?
1919
private let contentMode: ContentMode
2020
#if os(OSX)
2121
@State private var image: NSImage? = nil
2222
#else
2323
@State private var image: UIImage? = nil
2424
#endif
2525

26-
#if os(OSX)
27-
public init(_ url: Url, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
28-
self.request = Request {
29-
url
30-
}
31-
self.placeholder = placeholder
26+
public init(_ url: Url, @ViewBuilder placeholder: () -> Placeholder, contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
27+
self.request = Request { url }
28+
self.placeholder = placeholder()
3229
self.animation = animation
3330
self.contentMode = contentMode
3431
}
35-
#else
36-
public init(_ url: Url, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
37-
self.request = Request {
38-
url
32+
33+
public var body: some View {
34+
Group {
35+
if let image = image {
36+
#if os(OSX)
37+
Image(nsImage: image)
38+
.resizable()
39+
#else
40+
Image(uiImage: image)
41+
.resizable()
42+
#endif
43+
} else {
44+
placeholder
45+
.onAppear {
46+
self.request.onData { data in
47+
#if os(OSX)
48+
self.image = NSImage(data: data)
49+
#else
50+
self.image = UIImage(data: data)
51+
#endif
52+
}
53+
.call()
54+
}
55+
}
3956
}
40-
self.placeholder = placeholder
41-
self.animation = animation
42-
self.contentMode = contentMode
57+
.aspectRatio(contentMode: contentMode)
58+
.animation(animation)
59+
}
60+
}
61+
62+
extension RequestImage where Placeholder == Image {
63+
#if os(OSX)
64+
public init(_ url: Url, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
65+
self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation)
66+
}
67+
#else
68+
public init(_ url: Url, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
69+
self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation)
4370
}
4471
#endif
4572

4673
#if os(OSX)
47-
public init(_ request: Request, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
74+
public init(_ request: Request, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
4875
self.request = request
49-
self.placeholder = placeholder
76+
self.placeholder = placeholder.resizable()
5077
self.animation = animation
5178
self.contentMode = contentMode
5279
}
5380
#else
54-
public init(_ request: Request, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
81+
public init(_ request: Request, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
5582
self.request = request
56-
self.placeholder = placeholder
83+
self.placeholder = placeholder.resizable()
5784
self.animation = animation
5885
self.contentMode = contentMode
5986
}
6087
#endif
61-
62-
public var body: some View {
63-
if image != nil {
64-
#if os(OSX)
65-
return Image(nsImage: image!)
66-
.resizable()
67-
.aspectRatio(contentMode: contentMode)
68-
.animation(animation)
69-
#else
70-
return Image(uiImage: image!)
71-
.resizable()
72-
.aspectRatio(contentMode: contentMode)
73-
.animation(animation)
74-
#endif
75-
} else {
76-
self.request.onData { data in
77-
#if os(OSX)
78-
self.image = NSImage(data: data)
79-
#else
80-
self.image = UIImage(data: data)
81-
#endif
82-
}
83-
.call()
84-
return placeholder
85-
.resizable()
86-
.aspectRatio(contentMode: contentMode)
87-
.animation(animation)
88-
}
89-
}
9088
}

0 commit comments

Comments
 (0)