Skip to content

Commit d60993a

Browse files
authored
Merge pull request #189 from rootstrap/fix/issue-178-networking-layer
Refactor Networking layer - Implementation proposal
2 parents 5be1f9b + 4c6fe1f commit d60993a

35 files changed

+1128
-267
lines changed

Podfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ inhibit_all_warnings!
55
target 'ios-base' do
66
pod 'Alamofire', '~> 5.2.0'
77
pod 'IQKeyboardManagerSwift', '~> 6.1.1'
8-
pod 'RSFontSizes', '~> 1.0.2'
8+
pod 'RSFontSizes', '~> 1.2.0'
99
pod 'R.swift', '~> 5.0.3'
1010
pod 'SwiftLint', '~> 0.43.1'
1111
pod 'Firebase/CoreOnly', '~> 8.6.0'

Podfile.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ PODS:
104104
- R.swift (5.0.3):
105105
- R.swift.Library (~> 5.0.0)
106106
- R.swift.Library (5.0.1)
107-
- RSFontSizes (1.0.2):
107+
- RSFontSizes (1.2.0):
108108
- Device (~> 3.1.2)
109109
- Swifter (1.5.0)
110110
- SwiftLint (0.43.1)
@@ -118,7 +118,7 @@ DEPENDENCIES:
118118
- Firebase/Crashlytics (~> 8.6.0)
119119
- IQKeyboardManagerSwift (~> 6.1.1)
120120
- R.swift (~> 5.0.3)
121-
- RSFontSizes (~> 1.0.2)
121+
- RSFontSizes (~> 1.2.0)
122122
- Swifter (~> 1.5.0)
123123
- SwiftLint (~> 0.43.1)
124124

@@ -165,10 +165,10 @@ SPEC CHECKSUMS:
165165
PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58
166166
R.swift: f5a87643b91ea569d23d6afb3eee9c743edde239
167167
R.swift.Library: cfe85d569d9bae6cb262922db130e7c3a7a5fad1
168-
RSFontSizes: cf14ae41c2807b66573f7064528ccff8c98251c9
168+
RSFontSizes: 78158061f9f6121c6715f746395b1d8390fcb18b
169169
Swifter: e71dd674404923d7f03ebb03f3f222d1c570bc8e
170170
SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52
171171

172-
PODFILE CHECKSUM: 0466151c1673f2ab6ca3ea6aebefa12d898a7fbd
172+
PODFILE CHECKSUM: ad8b2fa0cef07782327d22c0570f1659eddad970
173173

174174
COCOAPODS: 1.11.2

ios-base.xcodeproj/project.pbxproj

+215-77
Large diffs are not rendered by default.

ios-base/Common/Models/Session.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ struct Session: Codable {
3737
guard let stringHeaders = loweredHeaders as? [String: String] else {
3838
return nil
3939
}
40-
if let expiryString = stringHeaders[APIClient.HTTPHeader.expiry.rawValue],
40+
if let expiryString = stringHeaders[HTTPHeader.expiry.rawValue],
4141
let expiryNumber = Double(expiryString) {
4242
expiry = Date(timeIntervalSince1970: expiryNumber)
4343
}
44-
uid = stringHeaders[APIClient.HTTPHeader.uid.rawValue]
45-
client = stringHeaders[APIClient.HTTPHeader.client.rawValue]
46-
accessToken = stringHeaders[APIClient.HTTPHeader.token.rawValue]
44+
uid = stringHeaders[HTTPHeader.uid.rawValue]
45+
client = stringHeaders[HTTPHeader.client.rawValue]
46+
accessToken = stringHeaders[HTTPHeader.token.rawValue]
4747
}
4848
}

ios-base/Common/Models/User.swift

-15
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,4 @@ struct User: Codable {
2020
case email
2121
case image = "profile_picture"
2222
}
23-
24-
init?(dictionary: [String: Any]) {
25-
guard
26-
let id = dictionary[CodingKeys.id.rawValue] as? Int,
27-
let username = dictionary[CodingKeys.username.rawValue] as? String,
28-
let email = dictionary[CodingKeys.email.rawValue] as? String
29-
else {
30-
return nil
31-
}
32-
33-
self.id = id
34-
self.username = username
35-
self.email = email
36-
self.image = URL(string: dictionary[CodingKeys.image.rawValue] as? String ?? "")
37-
}
3823
}

ios-base/Extensions/FontExtension.swift

+4-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ extension UIFont {
3333
}
3434

3535
static func font(withName name: String = "", size: Sizes) -> UIFont {
36-
let size = Font.PointSize.proportional(to: (.screen6_5Inch,
37-
size.rawValue)).value()
38-
let font = UIFont(name: name,
39-
size: size)
40-
return font ?? UIFont.systemFont(ofSize: size)
36+
name.font(
37+
withWeight: .normal,
38+
size: PointSize.proportional(to: (.screen6_5Inch, size.rawValue))
39+
) ?? UIFont.systemFont(ofSize: size.rawValue)
4140
}
4241

4342
public enum Sizes: CGFloat {

ios-base/Helpers/Constants.swift

+1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ struct App {
2626
enum ErrorDomain: String {
2727
case generic = "GenericError"
2828
case parsing = "ParsingError"
29+
case network = "NetworkError"
2930
}

ios-base/Home/ViewModels/HomeViewModel.swift

+15-15
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,41 @@ class HomeViewModel {
3333
func loadUserProfile() {
3434
state = .network(state: .loading)
3535

36-
UserServices.getMyProfile(
37-
success: { [weak self] user in
36+
UserServices.getMyProfile { [weak self] (result: Result<User, Error>) in
37+
switch result {
38+
case .success(let user):
3839
self?.userEmail = user.email
3940
self?.state = .loadedProfile
40-
},
41-
failure: { [weak self] error in
41+
case .failure(let error):
4242
self?.state = .network(state: .error(error.localizedDescription))
4343
}
44-
)
44+
}
4545
}
4646

4747
func logoutUser() {
4848
state = .network(state: .loading)
4949

50-
AuthenticationServices.logout(
51-
success: { [weak self] in
50+
AuthenticationServices.logout { [weak self] result in
51+
switch result {
52+
case .success:
5253
self?.didlogOutAccount()
53-
},
54-
failure: { [weak self] error in
54+
case .failure(let error):
5555
self?.state = .network(state: .error(error.localizedDescription))
5656
}
57-
)
57+
}
5858
}
5959

6060
func deleteAccount() {
6161
state = .network(state: .loading)
6262

63-
AuthenticationServices.deleteAccount(
64-
success: { [weak self] in
63+
AuthenticationServices.deleteAccount { [weak self] result in
64+
switch result {
65+
case .success:
6566
self?.didlogOutAccount()
66-
},
67-
failure: { [weak self] error in
67+
case .failure(let error):
6868
self?.state = .network(state: .error(error.localizedDescription))
6969
}
70-
)
70+
}
7171
}
7272

7373
private func didlogOutAccount() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
internal enum APIClientError: Error {
4+
case invalidEmptyResponse
5+
case statusCodeInvalid
6+
7+
var domain: ErrorDomain {
8+
.network
9+
}
10+
11+
var code: Int {
12+
switch self {
13+
case .invalidEmptyResponse:
14+
return 1
15+
case .statusCodeInvalid:
16+
return 2
17+
}
18+
}
19+
20+
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import Foundation
2+
3+
/// A structure that represents a custom error returned by the API
4+
/// in the request response.
5+
internal struct APIError: Error {
6+
let statusCode: Int
7+
let underlayingError: RailsError
8+
9+
init?(
10+
response: Network.Response,
11+
decodingConfiguration: DecodingConfiguration
12+
) {
13+
let decoder = JSONDecoder(decodingConfig: decodingConfiguration)
14+
guard
15+
let data = response.data,
16+
let decodedError = try? decoder.decode(RailsError.self, from: data)
17+
else {
18+
return nil
19+
}
20+
21+
self.statusCode = response.statusCode
22+
self.underlayingError = decodedError
23+
}
24+
25+
/// Returns the first error returned by the API
26+
var firstError: String? {
27+
if let errors = underlayingError.errors, let firstMessage = errors.first {
28+
return "\(firstMessage.key) \(firstMessage.value.first ?? "")"
29+
} else if let errorString = underlayingError.error {
30+
return errorString
31+
}
32+
33+
return nil
34+
}
35+
36+
/// Returns an array containing all error values returned from the API
37+
var errors: [String] {
38+
var flattenedErrors = underlayingError.errors?
39+
.compactMap { $0.value }
40+
.flatMap { $0 }
41+
42+
if let errorString = underlayingError.error {
43+
flattenedErrors?.append(errorString)
44+
}
45+
46+
return flattenedErrors ?? []
47+
}
48+
}
49+
50+
/// A structure that represents a Ruby on Rails API error object
51+
internal struct RailsError: Decodable {
52+
53+
let errors: [String: [String]]?
54+
let error: String?
55+
56+
enum CodingKeys: String, CodingKey {
57+
case errors
58+
case error
59+
}
60+
61+
init(from decoder: Decoder) throws {
62+
let values = try decoder.container(keyedBy: CodingKeys.self)
63+
if let errors = try? values.decode([String: [String]].self, forKey: .errors) {
64+
self.errors = errors
65+
self.error = nil
66+
} else if let error = try? values.decode(String.self, forKey: .errors) {
67+
self.error = error
68+
self.errors = nil
69+
} else {
70+
error = try? values.decode(String.self, forKey: .error)
71+
errors = nil
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// Definition for API clients used in the application.
2+
/// i.e. `OtherAPIClient(networkProvider: URLSessionNetworkProvider())`
3+
///
4+
extension BaseAPIClient {
5+
static let `default` = BaseAPIClient(
6+
networkProvider: AlamofireNetworkProvider(),
7+
headersProvider: RailsAPIHeadersProvider(
8+
sessionHeadersProvider: SessionHeadersProvider()
9+
)
10+
)
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
extension JSONDecoder {
4+
5+
convenience init(decodingConfig: DecodingConfiguration) {
6+
self.init()
7+
8+
dateDecodingStrategy = decodingConfig.dateStrategy
9+
keyDecodingStrategy = decodingConfig.keyStrategy
10+
dataDecodingStrategy = decodingConfig.dataStrategy
11+
}
12+
13+
}

ios-base/Networking/Models/Base64Media.swift

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
//
88

99
import Foundation
10-
import Alamofire
1110

1211
class Base64Media: MultipartMedia {
1312
var base64: String

ios-base/Networking/Models/MultipartMedia.swift

-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
//
88

99
import Foundation
10-
import Alamofire
1110

1211
// Basic media MIME types, add more if needed.
1312
enum MimeType: String {
@@ -46,8 +45,4 @@ class MultipartMedia {
4645
self.data = data
4746
self.type = type
4847
}
49-
50-
func embed(inForm multipart: MultipartFormData) {
51-
multipart.append(data, withName: key, fileName: toFile, mimeType: type.rawValue)
52-
}
5348
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Foundation
2+
3+
internal struct EmptyResponse: Decodable {}
4+
5+
internal typealias CompletionCallback<T: Decodable> = (
6+
_ result: Result<T?, Error>,
7+
_ responseHeaders: [String: String]
8+
) -> Void
9+
10+
/// Defines the requirement for an API Client object
11+
internal protocol APIClient {
12+
13+
/// Returns the base encoding configration for all requests parameters.
14+
/// Will be overriden by the `Endpoint` encoding configuration, if any.
15+
var encodingConfiguration: EncodingConfiguration { get }
16+
17+
/// Returns the base decoding configration for all request reponses.
18+
/// Will be overriden by the `Endpoint` decoding configuration, if any.
19+
var decodingConfiguration: DecodingConfiguration { get }
20+
21+
/// Initializes an instance of the `APIClient` conformant object.
22+
/// Any API Client concrete instance should be injected with the network provider.
23+
init(networkProvider: NetworkProvider)
24+
25+
/// Performs the request by using the provided `NetworkProvider`.
26+
/// - Returns: A `Cancellable` request.
27+
func request<T: Decodable>(
28+
endpoint: Endpoint,
29+
completion: @escaping CompletionCallback<T>
30+
) -> Cancellable
31+
32+
/// Performs a multipart request to upload one or many `MultipartMedia` objects.
33+
/// The endpoint parameters will be encoded in the multipart form.
34+
/// Note: Multipart requests do not support `Content-Type = application/json` headers.
35+
/// If your API requires this header user base64 uploads instead.
36+
func multipartRequest<T: Decodable>(
37+
endpoint: Endpoint,
38+
paramsRootKey: String,
39+
media: [MultipartMedia],
40+
completion: @escaping CompletionCallback<T>
41+
) -> Cancellable
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/// Defines a cancellable work.
2+
internal protocol Cancellable {
3+
func cancel() -> Self
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
internal protocol HeadersProvider {
2+
var requestHeaders: [String: String] { get }
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
/// Interface that provides an opportunity to define the HTTP
4+
/// communication layer.
5+
internal protocol NetworkProvider {
6+
7+
/// Performs a HTTP request to the given `Endpoint`.
8+
/// - Parameters:
9+
/// - endpoint: An object conforming to `Endpoint` providing the request information.
10+
/// - completion: The closure executed after the request is completed.
11+
/// - Returns: A `Cancellable` request.
12+
func request(
13+
endpoint: Endpoint,
14+
completion: @escaping (Result<Network.Response, Error>) -> Void
15+
) -> Cancellable
16+
17+
/// Performs a multipart request to upload one or many `MultipartMedia` objects.
18+
/// - Parameters:
19+
/// - endpoint: An object conforming to `Endpoint` providing the request information.
20+
/// - completion: The closure executed after the request is completed.
21+
/// - Returns: A `Cancellable` request.
22+
func multipartRequest(
23+
endpoint: Endpoint,
24+
multipartFormKey: String,
25+
media: [MultipartMedia],
26+
completion: @escaping (Result<Network.Response, Error>) -> Void
27+
) -> Cancellable
28+
}

0 commit comments

Comments
 (0)