Skip to content

Commit

Permalink
@Requested property wrapper and reimplementation of RequestView (#46
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
carson-katri authored May 6, 2021
1 parent 83bad6e commit 6266d88
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 106 deletions.
2 changes: 1 addition & 1 deletion Sources/Request/Request/Request+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension AnyRequest: Publisher {
public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
updatePublisher?
.receive(subscriber: UpdateSubscriber(request: self))
buildSession()
buildPublisher()
.subscribe(subscriber)
}

Expand Down
22 changes: 18 additions & 4 deletions Sources/Request/Request/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,28 @@ public struct AnyRequest<ResponseType> where ResponseType: Decodable {

/// Performs the `Request`, and calls the `onData`, `onString`, `onJson`, and `onError` callbacks when appropriate.
public func call() {
buildSession()
buildPublisher()
.subscribe(self)
if let updatePublisher = self.updatePublisher {
updatePublisher
.subscribe(UpdateSubscriber(request: self))
}
}

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

rootParam.buildParam(&request)
(rootParam as? SessionParam)?.buildConfiguration(configuration)


return (configuration, request)
}

internal func buildPublisher() -> AnyPublisher<(data: Data, response: URLResponse), Error> {
// PERFORM REQUEST
return URLSession(configuration: configuration).dataTaskPublisher(for: request)
let session = buildSession()
return URLSession(configuration: session.configuration).dataTaskPublisher(for: session.request)
.mapError { $0 }
.eraseToAnyPublisher()
}
Expand All @@ -130,3 +135,12 @@ public struct AnyRequest<ResponseType> where ResponseType: Decodable {
self.update(publisher: Timer.publish(every: seconds, on: .main, in: .common).autoconnect())
}
}

extension AnyRequest: Equatable {
public static func == (lhs: AnyRequest<ResponseType>, rhs: AnyRequest<ResponseType>) -> Bool {
let lhsSession = lhs.buildSession()
let rhsSession = rhs.buildSession()
return lhsSession.configuration == rhsSession.configuration && lhsSession.request == rhsSession.request
}
}

12 changes: 11 additions & 1 deletion Sources/Request/Request/RequestParams/Url/Url.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

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

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

return path
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.init(try container.decode(String.self))
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(absoluteString)
}
}

extension Url: Equatable {
Expand Down
64 changes: 64 additions & 0 deletions Sources/Request/SwiftUI/PropertyWrappers/Requested.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Requested.swift
//
//
// Created by Carson Katri on 1/17/21.
//

import SwiftUI
import Combine

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@propertyWrapper
public struct Requested<Value: Decodable>: DynamicProperty {
@StateObject private var requestStore: RequestStore

final class RequestStore: ObservableObject {
@Published var status: RequestStatus<Value> = .loading
private var cancellable: AnyCancellable?
var request: AnyRequest<Value> {
didSet {
call()
}
}

init(request: AnyRequest<Value>) {
self.request = request
call()
}

func call() {
print("Calling")
self.status = .loading
cancellable = request
.objectPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
switch completion {
case let .failure(error):
self?.status = .failure(error)
case .finished: break
}
} receiveValue: { [weak self] result in
self?.status = .success(result)
}
}
}

public init(wrappedValue: AnyRequest<Value>) {
self._requestStore = .init(wrappedValue: .init(request: wrappedValue))
}

public var wrappedValue: AnyRequest<Value> {
get {
requestStore.request
}
nonmutating set {
requestStore.request = newValue
}
}

public var projectedValue: RequestStatus<Value> {
requestStore.status
}
}
12 changes: 12 additions & 0 deletions Sources/Request/SwiftUI/RequestStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// RequestStatus.swift
//
//
// Created by Carson Katri on 1/17/21.
//

public enum RequestStatus<Value: Decodable> {
case loading
case success(Value)
case failure(Error)
}
51 changes: 0 additions & 51 deletions Sources/Request/SwiftUI/RequestView/RequestView.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,79 +12,77 @@ import SwiftUI
/// It automatically has animations to transition from a placeholder and the image.
///
/// It takes a `Url` or `Request`, a placeholder, the `ContentMode` for displaying the image, and an `Animation` for switching
public struct RequestImage: View {
public struct RequestImage<Placeholder: View>: View {
private let request: Request
private let placeholder: Image
private let animation: Animation
private let placeholder: Placeholder
private let animation: Animation?
private let contentMode: ContentMode
#if os(OSX)
@State private var image: NSImage? = nil
#else
@State private var image: UIImage? = nil
#endif

#if os(OSX)
public init(_ url: Url, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
self.request = Request {
url
}
self.placeholder = placeholder
public init(_ url: Url, @ViewBuilder placeholder: () -> Placeholder, contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
self.request = Request { url }
self.placeholder = placeholder()
self.animation = animation
self.contentMode = contentMode
}
#else
public init(_ url: Url, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
self.request = Request {
url

public var body: some View {
Group {
if let image = image {
#if os(OSX)
Image(nsImage: image)
.resizable()
#else
Image(uiImage: image)
.resizable()
#endif
} else {
placeholder
.onAppear {
self.request.onData { data in
#if os(OSX)
self.image = NSImage(data: data)
#else
self.image = UIImage(data: data)
#endif
}
.call()
}
}
}
self.placeholder = placeholder
self.animation = animation
self.contentMode = contentMode
.aspectRatio(contentMode: contentMode)
.animation(animation)
}
}

extension RequestImage where Placeholder == Image {
#if os(OSX)
public init(_ url: Url, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation)
}
#else
public init(_ url: Url, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
self.init(Request { url }, placeholder: placeholder, contentMode: contentMode, animation: animation)
}
#endif

#if os(OSX)
public init(_ request: Request, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
public init(_ request: Request, placeholder: Image = Image(nsImage: NSImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
self.request = request
self.placeholder = placeholder
self.placeholder = placeholder.resizable()
self.animation = animation
self.contentMode = contentMode
}
#else
public init(_ request: Request, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation = .easeInOut) {
public init(_ request: Request, placeholder: Image = Image(uiImage: UIImage()), contentMode: ContentMode = .fill, animation: Animation? = .easeInOut) {
self.request = request
self.placeholder = placeholder
self.placeholder = placeholder.resizable()
self.animation = animation
self.contentMode = contentMode
}
#endif

public var body: some View {
if image != nil {
#if os(OSX)
return Image(nsImage: image!)
.resizable()
.aspectRatio(contentMode: contentMode)
.animation(animation)
#else
return Image(uiImage: image!)
.resizable()
.aspectRatio(contentMode: contentMode)
.animation(animation)
#endif
} else {
self.request.onData { data in
#if os(OSX)
self.image = NSImage(data: data)
#else
self.image = UIImage(data: data)
#endif
}
.call()
return placeholder
.resizable()
.aspectRatio(contentMode: contentMode)
.animation(animation)
}
}
}
Loading

0 comments on commit 6266d88

Please sign in to comment.