Skip to content

Commit

Permalink
Merge pull request #5 from GoodRequest/swiftui
Browse files Browse the repository at this point in the history
feat: GoodNetworking + SwiftUI dynamic wrappers
  • Loading branch information
plajdo authored Jan 11, 2024
2 parents bfd951a + 8c7ba4b commit 963ced5
Show file tree
Hide file tree
Showing 19 changed files with 535 additions and 31 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

# GoodNetworking

[![iOS Version](https://img.shields.io/badge/iOS_Version->=_12.0-brightgreen?logo=apple&logoColor=green)]()
[![Swift Version](https://img.shields.io/badge/Swift_Version-5.5-green?logo=swift)](https://docs.swift.org/swift-book/)
[![iOS Version](https://img.shields.io/badge/iOS_Version->=_13.0-brightgreen?logo=apple&logoColor=green)]()
[![Swift Version](https://img.shields.io/badge/Swift_Version-5.9-green?logo=swift)](https://docs.swift.org/swift-book/)
[![Supported devices](https://img.shields.io/badge/Supported_Devices-iPhone/iPad-green)]()
[![Contains Test](https://img.shields.io/badge/Tests-YES-blue)]()
[![Dependency Manager](https://img.shields.io/badge/Dependency_Manager-SPM-red)](#swiftpackagemanager)
Expand Down Expand Up @@ -38,9 +38,9 @@ let package = Package(

Define two enums:
- one for the base API address called `ApiServer`
- one that follows the `GREndpointManager` protocol, more information [here](https://goodrequest.github.io/GoodNetworking/documentation/goodnetworking/grendpointmanager/)
- one that follows the `Endpoint` protocol, more information [here](https://goodrequest.github.io/GoodNetworking/documentation/goodnetworking/models/endpoint/)

Create a RequestManager using `GRSession<RequestEndpoint, APIServer>`
Create a RequestManager using `GRSession`

```swift
import GoodNetworking
Expand Down
16 changes: 16 additions & 0 deletions Sources/GoodNetworking/Extensions/AFErrorExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// AFErrorExtensions.swift
// GoodNetworking
//
// Created by Filip Šašala on 03/01/2024.
//

import Alamofire

extension AFError: Equatable {

public static func == (lhs: AFError, rhs: AFError) -> Bool {
false // every AFError is unique
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// ArrayEncoding.swift
//
// GoodNetworking
//
// Created by Andrej Jasso on 18/10/2023.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CodableExtensions.swift
//
// GoodNetworking
//
// Created by Dominik Pethö on 11/9/18.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DataRequestExtensions.swift
//
// GoodNetworking
//
// Created by Dominik Pethö on 4/30/19.
//
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion Sources/GoodNetworking/GRImageDownloader.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// GRImageDownloader.swift
//
// GoodNetworking
//
// Created by Andrej Jasso on 24/05/2022.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//
// GREndpoint.swift
// Endpoint.swift
// GoodNetworking
//
// Created by Filip Šašala on 10/12/2023.
//

import Alamofire
Expand Down Expand Up @@ -40,15 +43,15 @@ public protocol Endpoint {
/// Enum that represents the type of parameters to be sent with the request.
public enum EndpointParameters {

typealias CustomEncodable = (Encodable & WithCustomEncoder)
public typealias CustomEncodable = (Encodable & WithCustomEncoder)

/// Case for sending `Parameters`.
case parameters(Parameters)

/// Case for sending an instance of `Encodable`.
case model(Encodable)

var dictionary: Parameters? {
public var dictionary: Parameters? {
switch self {
case .parameters(let parameters):
return parameters
Expand All @@ -72,6 +75,14 @@ public enum EndpointParameters {

}

// MARK: - EndpointBindable

public protocol EndpointBindable {

static func endpoint(_ data: Self) -> Endpoint

}

// MARK: - Compatibility

@available(*, deprecated, renamed: "Endpoint", message: "Renamed to Endpoint.")
Expand Down
49 changes: 49 additions & 0 deletions Sources/GoodNetworking/Models/Query.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Query.swift
// GoodNetworking
//
// Created by Filip Šašala on 10/12/2023.
//

import Alamofire
import Combine
import Foundation

#if canImport(GoodStructs)
import GoodStructs
public typealias Response<R> = GoodStructs.GRResult<R, AFError>
#else
public typealias Response<R> = Swift.Result<R, AFError>
#endif

public protocol Query: EndpointBindable, Encodable, Equatable {

associatedtype Result: Decodable

var result: Response<Result>? { get set }

}

extension Query {

internal func dataTaskPublisher(using session: NetworkSession) -> AnyPublisher<Response<Result>, Never> {
return session.request(endpoint: Self.endpoint(self))
.goodify(type: Result.self)
.receive(on: DispatchQueue.main)
.map { .success($0) }
.catch { Just(.failure($0)) }
#if canImport(GoodStructs)
.prepend(.loading)
#endif
.eraseToAnyPublisher()
}

}

extension Query {

public static func ==(lhs: any Query, rhs: any Query) -> Bool {
false // every query is unique
}

}
72 changes: 72 additions & 0 deletions Sources/GoodNetworking/Models/Resource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Resource.swift
// GoodNetworking
//
// Created by Filip Šašala on 04/01/2024.
//

import Alamofire
import Combine
import Foundation

public protocol Resource: EndpointBindable, Codable, Hashable {

static var placeholder: Self { get }

}

extension Resource {

internal func dataTaskPublisher(
using session: NetworkSession
) -> AnyPublisher<ResourceState<Self>, Never> {
session.request(endpoint: Self.endpoint(self))
.goodify(type: Self.self)
.receive(on: DispatchQueue.main)
.map { .available($0) }
.catch { Just(.stale(self, $0)).eraseToAnyPublisher() }
.prepend(.uploading(self))
.eraseToAnyPublisher()
}

}

public enum ResourceState<R: Resource>: Equatable {

case unavailable
case loading

case available(R)
case pending(R)
case uploading(R)
case stale(R, AFError)

public var isAvailable: Bool {
switch self {
case .unavailable, .loading:
return false

default:
return true
}
}

public var resource: R? {
switch self {
case .unavailable, .loading:
return nil

case .available(let resource), .pending(let resource), .uploading(let resource):
return resource

case .stale(let resource, _):
return resource
}
}

public var unwrapped: R {
guard let resource else { preconditionFailure("Accessing unavailable resource") }
return resource
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// GRSessionLogger.swift
//
// GoodNetworking
//
// Created by Andrej Jasso on 24/05/2022.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
//
// GRSession.swift
//
// NetworkSession.swift
// GoodNetworking
//
// Created by Dominik Pethö on 8/17/20.
//

import Foundation
import Alamofire
import Foundation

/// Executes network requests for the client app.
public class NetworkSession {

// MARK: - Static

public static let `default` = NetworkSession()
public static var `default` = NetworkSession()

// MARK: - Private

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//
// GRSessionConfiguration.swift
//
// NetworkSessionConfiguration.swift
// GoodNetworking
//
// Created by Andrej Jasso on 15/11/2022.
//

import Foundation
import Alamofire
import Foundation

/// The GRSessionConfiguration class represents the configuration used to create a GRSession object. This class has the following properties:
public final class NetworkSessionConfiguration {
Expand Down
62 changes: 62 additions & 0 deletions Sources/GoodNetworking/Wrapper/Fetch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Fetch.swift
// GoodNetworking
//
// Created by Filip Šašala on 08/12/2023.
//

import Alamofire
import Combine
import SwiftUI

/// Fetches data from remote endpoint immediately after initialization.
///
/// Example usage:
/// ```swift
/// @Fetch private var userData = UserDataRequest()
///
/// switch data.result {
/// case .none, .loading:
/// ProgressView()
/// case .success(let userResponse):
/// Text(userResponse.name)
/// case .failure(let error):
/// Text(error.localizedDescription)
/// }
/// ```
@propertyWrapper public struct Fetch<Q: Query>: DynamicProperty {

@ObservedObject @Observable private var observableQuery: Q
@ObservedObject @Observable private var dataTask: AnyCancellable?

private let session: NetworkSession

public var wrappedValue: Q {
get { observableQuery }
nonmutating set {
let oldValue = observableQuery
guard newValue != oldValue else { return }

observableQuery = newValue

dataTask?.cancel()
dataTask = nil
dataTask = makeDataTask(from: newValue)
}
}

public init(wrappedValue: Q, session: NetworkSession = .default) {
self.session = session

self._observableQuery = ObservedObject(wrappedValue: Observable(wrappedValue))
self._dataTask = ObservedObject(wrappedValue: Observable(nil))

self.dataTask = makeDataTask(from: wrappedValue)
}

private func makeDataTask(from query: Q) -> AnyCancellable {
query.dataTaskPublisher(using: session)
.sink { [self] in observableQuery.result = $0 }
}

}
18 changes: 18 additions & 0 deletions Sources/GoodNetworking/Wrapper/Observable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Observable.swift
// GoodNetworking
//
// Created by Filip Šašala on 10/12/2023.
//

import Combine

@propertyWrapper public final class Observable<T>: ObservableObject {

@Published public var wrappedValue: T

public init(_ wrappedValue: T) {
self.wrappedValue = wrappedValue
}

}
Loading

0 comments on commit 963ced5

Please sign in to comment.