Skip to content

Instantly share code, notes, and snippets.

@swift2931
Last active December 2, 2023 10:33
Show Gist options
  • Save swift2931/d99229b14b946aec8622511017c87b38 to your computer and use it in GitHub Desktop.
Save swift2931/d99229b14b946aec8622511017c87b38 to your computer and use it in GitHub Desktop.
networking for SwiftUI
import SwiftUI
import Combine
protocol Service {
static var mods: [String: (inout URLRequest) -> Void] {get set}
var baseURL: String {get set}
func config(_ pat: String, _ mod: @escaping (inout URLRequest) -> Void)
func decorated(_ absURL: String, _ req: URLRequest) -> URLRequest
func match(_ pat: String, _ absURL: String) -> Bool
func makeRequest(_ relativeURL: String) -> URLRequest
}
extension Service {
func config(_ pat: String, _ mod: @escaping (inout URLRequest) -> Void) {
Self.mods[pat] = mod
}
func decorated(_ absURL: String, _ req: URLRequest) -> URLRequest {
Array(Self.mods.keys).reduce(into: req) { (result, pat) in
guard match(pat, absURL), let mod = Self.mods[pat] else {return}
mod(&result)
}
}
func match(_ pat: String, _ absURL: String) -> Bool {
true
}
func makeRequest(_ relativeURL: String) -> URLRequest {
let absURL = baseURL + relativeURL
return URLRequest(url: URL(string: absURL)!)
}
}
final class API: Service {
static var mods = [String : (inout URLRequest) -> Void]()
var baseURL = "http://dummy.restapiexample.com/api/v1/"
let but = JSON("employees")
}
enum HttpMethod: String {
case get, post, put, delete
}
protocol HttpBody {
var body: Data? {get}
}
protocol NetData {
init()
static func decode(_ data: Data) -> Self?
}
typealias Request<T> = AnyPublisher<(T, HTTPURLResponse), Error>
protocol Resource: ObservableObject {
associatedtype ResourceType: NetData
static var mods: [String: (Request<ResourceType>) -> Request<ResourceType>] {get set}
static var service: Service {get}
var cancellable: AnyCancellable? {get set}
var url: String {get set}
var data: ResourceType {get set}
var error: Error? {get set}
var response: HTTPURLResponse? {get set}
var urlRequestBase: URLRequest {get}
var contentType: String {get}
func load() -> Callback<ResourceType>
func load(using: Request<ResourceType>) -> Callback<ResourceType>
func request(_ method: HttpMethod, _ payload: HttpBody?) -> Request<ResourceType>
func config(_ pat: String, _ mod: @escaping (Request<ResourceType>) -> Request<ResourceType>)
func chained(_ req: Request<ResourceType>) -> Request<ResourceType>
}
extension Resource {
static var service: Service {
API()
}
var urlRequestBase: URLRequest {
var r = service.makeRequest(url)
r.addValue(contentType, forHTTPHeaderField: "Content-Type")
return r
}
func request(_ method: HttpMethod = .post, _ payload: HttpBody? = nil) -> Request<ResourceType> {
var r = urlRequestBase
r.httpMethod = method.rawValue.capitalized
r.httpBody = payload?.body
let req = service.decorated(r.url?.absoluteString ?? "", r)
return URLSession.shared.dataTaskPublisher(for: req).tryMap {
(data, response) in
guard let resp = response as? HTTPURLResponse, 200 ..< 300 ~= resp.statusCode else {
throw NetError.errorResponse
}
guard let d = ResourceType.decode(data) else {
throw NetError.decodeError
}
return (d, resp)
}.eraseToAnyPublisher()
}
@discardableResult
func load(using req: Request<ResourceType>) -> Callback<ResourceType> {
let cb = Callback<ResourceType>()
cancellable = req.receive(on: RunLoop.main).sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
cb.completion?()
case .failure(let e):
self.error = e
cb.failure?(e)
}
}) { (data, resp) in
self.data = data
self.response = resp
cb.success?(data)
}
return cb
}
@discardableResult
func load() -> Callback<ResourceType> {
load(using: request(.get))
}
func config(_ pat: String, _ mod: @escaping (Request<ResourceType>) -> Request<ResourceType>) {
Self.mods[pat] = mod
}
func chained(_ req: Request<ResourceType>) -> Request<ResourceType> {
req
}
}
final class Callback<T> {
var completion: (() -> Void)?
var success: ((T) -> Void)?
var failure: ((Error) -> Void)?
func onCompletion(_ cls: @escaping () -> Void) {
completion = cls
}
func onSuccess(_ cls: @escaping (T) -> Void) {
success = cls
}
func onFailure(_ cls: @escaping (Error) -> Void) {
failure = cls
}
}
enum NetError: Error {
case errorResponse, decodeError
}
extension MJ: HttpBody, NetData {
static func decode(_ data: Data) -> MJ? {
MJ(data: data)
}
var body: Data? {
self.data
}
}
final class JSON: Resource {
var cancellable: AnyCancellable?
static var mods = [String : (Request<MJ>) -> Request<MJ>]()
var url: String
var error: Error?
var response: HTTPURLResponse?
var contentType = "application/json"
@Published var data = MJ.raw("test") {
didSet {
print(data[0])
}
}
init(_ relURL: String) {
url = relURL
}
}
@crisrojas
Copy link

crisrojas commented Nov 27, 2023

Hey Jim, found your github through one of your articles on Medium. I used to think you were a troll, but I've just realized that you were right about everything (I currently have to work with VIPER daily, and ooh boy, you were right...), just didn't realized because I was trained from the beginning of my career on MVVM and never had the chance to understand why we despise so much sweet old MVC, so never had the chance to appreciate it as it deserves.

Bonfire looks 🔥 and I'm currently giving it a try in my playgrounds. Just curious about one thing, MJ implementation is missing, but I guess it must be something similar to the JSON enum you got in the repo where you use the Binance websocket api. Is there a reason you prefer that custom deserializing method over just decodable? Maybe dev is faster? simpler? or was it because when you create this and the other repo Codable wasn't yet a thing? do you use it today or you still use a custom entity?

Keep up the good work with your articles, I'm learning a ton from them and from your github projects/gist. Regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment