Skip to content

Instantly share code, notes, and snippets.

@olejorgensen
Last active September 19, 2021 19:13
Show Gist options
  • Save olejorgensen/71a063fbf77c2747f956ae8feb7a820b to your computer and use it in GitHub Desktop.
Save olejorgensen/71a063fbf77c2747f956ae8feb7a820b to your computer and use it in GitHub Desktop.
Swift Json Service Template
import Foundation
import Combine
protocol JsonServiceProtocol {
var decoder: JSONDecoder { get }
var errorPublisher: PassthroughSubject<Error, Never> { get }
func publish(err: Error)
}
class BaseService: NSObject, JsonServiceProtocol {
// MARK:- Properties
let decoder: JSONDecoder
let errorPublisher = PassthroughSubject<Error, Never>()
//MARK:- Init, Deinit
override init() {
self.decoder = ServiceSettings.iso8601FullDecoder
super.init()
}
init(decoder: JSONDecoder) {
self.decoder = decoder
}
//MARK:- Methods
func publish(err: Error) {
publish(error: err as NSError)
}
func publish(error: NSError) {
RunLoop.main.perform {
Swift.print(error)
Swift.print(error.localizedDescription)
Swift.print(error.debugDescription)
self.errorPublisher.send(error)
}
}
}
extension DateFormatter {
static let iso8601Full: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
import Foundation
import CoreLocation
import Combine
import MapKit
class GistService: BaseService {
// MARK:- Properties
let didRecieveYourObject = PassthroughSubject<YourObject, Never>()
private var isBusy = false
// MARK:- Metoder
func fetchYourObject(someQueryOrFilter: String) {
if isBusy {
return
}
guard let trimmedQuery = someQueryOrFilter.trimmedOrNil else { return }
guard let escapedQuery = trimmedQuery.toQueryValue else { return }
var path = "\(ServiceSettings.baseUrl)/yourobject?q=\(escapedQuery)"
guard let url = URL(string: path) else {
self.publish(err: ServiceErrors.makeInvalidQueryError(query: trimmedQuery))
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
self.isBusy = true
do {
if let err = error {
self.publish(err: err)
} else {
if let httpResponse = response as? HTTPURLResponse, let data = data {
if httpResponse.statusCode != 200 {
self.publish(err: ServiceErrors.makeHttpResponseError(urlString: path, statusCode: httpResponse.statusCode))
} else {
var yourObject = try self.decoder.decode(YourObject.self, from: data)
RunLoop.main.perform {
self.didRecieveYourObject.send(yourObject)
}
}
} else {
self.publish(err: ServiceErrors.Network)
}
}
} catch let err {
self.publish(err: err)
}
self.isBusy = false
}).resume()
}
}
import Foundation
class ServiceErrors {
static let ServiceErrorDomain = "ServiceErrors"
public static let Unknown = NSError(
domain: ServiceErrorDomain,
code: 20000,
userInfo: [ "Message" : "unknown error message" ]
)
public static let NoResult = NSError(
domain: ServiceErrorDomain,
code: 20001,
userInfo: [ "Message" : "no results message" ]
)
public static let Network = NSError(
domain: ServiceErrorDomain,
code: 20002,
userInfo: [ "Message" : "network error" ]
)
public static func makeHttpResponseError(urlString: String, statusCode: Int) -> Error {
return NSError(
domain: ServiceErrorDomain,
code: 20003,
userInfo: [
"Message" : "friendly network error message",
"URL" : urlString,
"StatusCode" : statusCode
]
)
}
public static func makeInvalidRequestError(urlString: String) -> Error {
return NSError(
domain: ServiceErrorDomain,
code: 20004,
userInfo: [
"Message" : "friendly invalid request error message",
"URL" : urlString
]
)
}
public static func makeInvalidQueryError(query: String) -> Error {
return NSError(
domain: ServiceErrorDomain,
code: 20005,
userInfo: [
"Message" : "friendly invalid query error message",
"Query" : query
]
)
}
}
class ServiceSettings {
static let iso8601FullDecoder: JSONDecoder = {
var jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
return jsonDecoder
}()
static let baseUrl: String = "https://some.service.org"
static let danishDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "da_DK")
formatter.dateStyle = .medium
return formatter
}()
/// Most services only supports English floats
static let englishDecimalFormat: NumberFormatter = {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 6
formatter.minimumFractionDigits = 6
return formatter
}()
/// Present service floats in current locale
static let decimalFormatUI: NumberFormatter = {
let formatter = englishDecimalFormat.copy() as! NumberFormatter
formatter.locale = Locale.current
return formatter
}()
}
import Foundation
public extension String {
/**
Trim using CharacterSet.whitespacesAndNewlines
*/
var trimmedOrNil: String? {
get {
let trimmed = self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
/**
Encode using CharacterSet.urlQueryAllowed
*/
var toQueryValue: String? {
get {
return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment