Skip to content

Commit 0df05f7

Browse files
committed
Add SafeClient
1 parent eb61d06 commit 0df05f7

File tree

4 files changed

+227
-1
lines changed

4 files changed

+227
-1
lines changed

ReactiveAPI.xcodeproj/project.pbxproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
EC28BB411E2B8A31002F34FE /* SafeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB401E2B8A31002F34FE /* SafeClient.swift */; };
11+
EC28BB421E2B8A31002F34FE /* SafeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB401E2B8A31002F34FE /* SafeClient.swift */; };
12+
EC28BB431E2B8A31002F34FE /* SafeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB401E2B8A31002F34FE /* SafeClient.swift */; };
13+
EC28BB441E2B8A31002F34FE /* SafeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB401E2B8A31002F34FE /* SafeClient.swift */; };
14+
EC28BB581E2BE92D002F34FE /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB571E2BE92D002F34FE /* Activity.swift */; };
15+
EC28BB591E2BE92D002F34FE /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB571E2BE92D002F34FE /* Activity.swift */; };
16+
EC28BB5A1E2BE92D002F34FE /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB571E2BE92D002F34FE /* Activity.swift */; };
17+
EC28BB5B1E2BE92D002F34FE /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC28BB571E2BE92D002F34FE /* Activity.swift */; };
1018
EC7166CF1DE5F2F5006F1386 /* ReactiveAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC7166C51DE5F2F5006F1386 /* ReactiveAPI.framework */; };
1119
EC7166D41DE5F2F5006F1386 /* ReactiveAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7166D31DE5F2F5006F1386 /* ReactiveAPITests.swift */; };
1220
EC7166D61DE5F2F5006F1386 /* ReactiveAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = EC7166C81DE5F2F5006F1386 /* ReactiveAPI.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -83,6 +91,8 @@
8391
/* End PBXContainerItemProxy section */
8492

8593
/* Begin PBXFileReference section */
94+
EC28BB401E2B8A31002F34FE /* SafeClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafeClient.swift; sourceTree = "<group>"; };
95+
EC28BB571E2BE92D002F34FE /* Activity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Activity.swift; sourceTree = "<group>"; };
8696
EC7166C51DE5F2F5006F1386 /* ReactiveAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8797
EC7166C81DE5F2F5006F1386 /* ReactiveAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactiveAPI.h; sourceTree = "<group>"; };
8898
EC7166C91DE5F2F5006F1386 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -186,6 +196,8 @@
186196
isa = PBXGroup;
187197
children = (
188198
EC7166EC1DE5F496006F1386 /* Client.swift */,
199+
EC28BB401E2B8A31002F34FE /* SafeClient.swift */,
200+
EC28BB571E2BE92D002F34FE /* Activity.swift */,
189201
EC7166EE1DE5F496006F1386 /* HTTPMethod.swift */,
190202
EC7166F01DE5F496006F1386 /* Request.swift */,
191203
EC7166EF1DE5F496006F1386 /* RequestParameters.swift */,
@@ -363,6 +375,7 @@
363375
};
364376
EC7ADFE01E1AD27000D8F7E0 = {
365377
CreatedOnToolsVersion = 8.1;
378+
LastSwiftMigration = 0820;
366379
ProvisioningStyle = Automatic;
367380
};
368381
};
@@ -477,9 +490,11 @@
477490
files = (
478491
EC7166F71DE5F496006F1386 /* Utilities.swift in Sources */,
479492
EC7166F61DE5F496006F1386 /* Request.swift in Sources */,
493+
EC28BB581E2BE92D002F34FE /* Activity.swift in Sources */,
480494
EC7166F51DE5F496006F1386 /* RequestParameters.swift in Sources */,
481495
ECC282CB1E1AACA50032EEB2 /* Client.swift in Sources */,
482496
EC7166F41DE5F496006F1386 /* HTTPMethod.swift in Sources */,
497+
EC28BB411E2B8A31002F34FE /* SafeClient.swift in Sources */,
483498
);
484499
runOnlyForDeploymentPostprocessing = 0;
485500
};
@@ -497,9 +512,11 @@
497512
files = (
498513
EC7ADFC51E1AD1D300D8F7E0 /* RequestParameters.swift in Sources */,
499514
EC7ADFC61E1AD1D300D8F7E0 /* Request.swift in Sources */,
515+
EC28BB591E2BE92D002F34FE /* Activity.swift in Sources */,
500516
EC7ADFC41E1AD1D300D8F7E0 /* HTTPMethod.swift in Sources */,
501517
EC7ADFC71E1AD1D300D8F7E0 /* Utilities.swift in Sources */,
502518
EC7ADFC31E1AD1D300D8F7E0 /* Client.swift in Sources */,
519+
EC28BB421E2B8A31002F34FE /* SafeClient.swift in Sources */,
503520
);
504521
runOnlyForDeploymentPostprocessing = 0;
505522
};
@@ -509,16 +526,20 @@
509526
files = (
510527
EC7ADFD81E1AD23100D8F7E0 /* RequestParameters.swift in Sources */,
511528
EC7ADFD91E1AD23100D8F7E0 /* Request.swift in Sources */,
529+
EC28BB5A1E2BE92D002F34FE /* Activity.swift in Sources */,
512530
EC7ADFD71E1AD23100D8F7E0 /* HTTPMethod.swift in Sources */,
513531
EC7ADFDA1E1AD23100D8F7E0 /* Utilities.swift in Sources */,
514532
EC7ADFD61E1AD23100D8F7E0 /* Client.swift in Sources */,
533+
EC28BB431E2B8A31002F34FE /* SafeClient.swift in Sources */,
515534
);
516535
runOnlyForDeploymentPostprocessing = 0;
517536
};
518537
EC7ADFDC1E1AD27000D8F7E0 /* Sources */ = {
519538
isa = PBXSourcesBuildPhase;
520539
buildActionMask = 2147483647;
521540
files = (
541+
EC28BB5B1E2BE92D002F34FE /* Activity.swift in Sources */,
542+
EC28BB441E2B8A31002F34FE /* SafeClient.swift in Sources */,
522543
);
523544
runOnlyForDeploymentPostprocessing = 0;
524545
};
@@ -794,6 +815,7 @@
794815
isa = XCBuildConfiguration;
795816
buildSettings = {
796817
APPLICATION_EXTENSION_API_ONLY = YES;
818+
CLANG_ENABLE_MODULES = YES;
797819
CODE_SIGN_IDENTITY = "";
798820
DEFINES_MODULE = YES;
799821
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -806,6 +828,7 @@
806828
PRODUCT_NAME = ReactiveAPI;
807829
SDKROOT = watchos;
808830
SKIP_INSTALL = YES;
831+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
809832
SWIFT_VERSION = 3.0;
810833
TARGETED_DEVICE_FAMILY = 4;
811834
WATCHOS_DEPLOYMENT_TARGET = 2.0;
@@ -816,6 +839,7 @@
816839
isa = XCBuildConfiguration;
817840
buildSettings = {
818841
APPLICATION_EXTENSION_API_ONLY = YES;
842+
CLANG_ENABLE_MODULES = YES;
819843
CODE_SIGN_IDENTITY = "";
820844
DEFINES_MODULE = YES;
821845
DYLIB_COMPATIBILITY_VERSION = 1;

Sources/Activity.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Activity.swift
3+
// ReactiveAPI
4+
//
5+
// Created by Srdan Rasic on 15/01/2017.
6+
// Copyright © 2017 Reactive Kit. All rights reserved.
7+
//
8+
9+
import ReactiveKit
10+
import UIKit
11+
12+
public class Activity {
13+
14+
fileprivate let _isActive = Property(false)
15+
16+
private var count = 0 {
17+
didSet {
18+
if oldValue == 1, count == 0 {
19+
_isActive.value = false
20+
} else if oldValue == 0, count == 1 {
21+
_isActive.value = true
22+
}
23+
}
24+
}
25+
26+
public var isActive: Bool {
27+
return _isActive.value
28+
}
29+
30+
public init(updateNetworkActivityIndicator: Bool = true) {
31+
if updateNetworkActivityIndicator {
32+
_ = _isActive.observeNext { isActive in
33+
UIApplication.shared.isNetworkActivityIndicatorVisible = isActive
34+
}
35+
}
36+
}
37+
38+
public func increase() {
39+
count += 1
40+
}
41+
42+
public func decrease() {
43+
count -= 1
44+
}
45+
}
46+
47+
extension Activity: SubjectProtocol {
48+
49+
public func on(_ event: Event<Bool, NoError>) {
50+
switch event {
51+
case .next(let active):
52+
if active {
53+
increase()
54+
} else {
55+
decrease()
56+
}
57+
default:
58+
break
59+
}
60+
}
61+
62+
public func observe(with observer: @escaping (Event<Bool, NoError>) -> Void) -> Disposable {
63+
return _isActive.observeIn(ImmediateOnMainExecutionContext).observe(with: observer)
64+
}
65+
}

Sources/Client.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ open class Client {
5454
let request = prepare(request: request)
5555

5656
let headers = defaultHeaders.merging(contentsOf: request.headers ?? [:])
57-
57+
5858
var urlRequest = URLRequest(url: URL(string: self.baseURL / request.path)!)
5959
urlRequest.httpMethod = request.method.rawValue
6060
headers.forEach { urlRequest.addValue($1, forHTTPHeaderField: $0) }
@@ -130,3 +130,19 @@ open class Client {
130130
.timeout(after: timeoutInterval, with: .client("Network request timed out. Please check your connection."))
131131
}
132132
}
133+
134+
extension Client.Error {
135+
136+
public var localizedDescription: String {
137+
switch self {
138+
case .network(let error):
139+
return error.localizedDescription
140+
case .parser(let error):
141+
return error.localizedDescription
142+
case .remote(let error):
143+
return error.localizedDescription
144+
case .client(let message):
145+
return message
146+
}
147+
}
148+
}

Sources/SafeClient.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//
2+
// SafeClient.swift
3+
// ReactiveAPI
4+
//
5+
// Created by Srdan Rasic on 15/01/2017.
6+
// Copyright © 2017 Reactive Kit. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import ReactiveKit
11+
12+
public struct UserFriendlyError {
13+
14+
public let message: String
15+
public let retry: PublishSubject<Void, NoError>?
16+
17+
public init(message: String, canRetry: Bool) {
18+
self.message = message
19+
20+
if canRetry {
21+
retry = PublishSubject()
22+
} else {
23+
retry = nil
24+
}
25+
}
26+
}
27+
28+
public class SafeClient {
29+
30+
public let base: Client
31+
public let errors = SafePublishSubject<UserFriendlyError>()
32+
public let activity = Activity()
33+
34+
public init(wrapping client: Client) {
35+
base = client
36+
}
37+
38+
public func response<Resource, Error: Swift.Error>(for request: Request<Resource, Error>, canUserRetry: Bool = true, autoRetryTimes: Int = 0, trackActivity: Bool = true) -> SafeSignal<Resource> {
39+
if trackActivity {
40+
return base
41+
.response(for: request)
42+
.retry(times: autoRetryTimes)
43+
.feedActivity(into: activity)
44+
.suppressAndFeedError(into: errors, canUserRetry: canUserRetry, map: { $0.localizedDescription })
45+
.debug(request.path)
46+
} else {
47+
return base
48+
.response(for: request)
49+
.retry(times: autoRetryTimes)
50+
.suppressAndFeedError(into: errors, canUserRetry: canUserRetry, map: { $0.localizedDescription })
51+
.debug(request.path)
52+
}
53+
}
54+
55+
public func unsafeResponse<Resource, Error: Swift.Error>(for request: Request<Resource, Error>, trackActivity: Bool = true) -> Signal<Resource, Client.Error> {
56+
if trackActivity {
57+
return base.response(for: request).feedActivity(into: activity)
58+
} else {
59+
return base.response(for: request)
60+
}
61+
}
62+
}
63+
64+
extension Request {
65+
66+
public func response(using client: SafeClient, canUserRetry: Bool = true, autoRetryTimes: Int = 0, trackActivity: Bool = true) -> SafeSignal<Resource> {
67+
return client.response(for: self, canUserRetry: canUserRetry, autoRetryTimes: autoRetryTimes, trackActivity: trackActivity)
68+
}
69+
70+
public func unsafeResponse(using client: SafeClient, trackActivity: Bool = true) -> Signal<Resource, Client.Error> {
71+
return client.unsafeResponse(for: self, trackActivity: trackActivity)
72+
}
73+
}
74+
75+
extension SignalProtocol {
76+
77+
public func feedError<S: SubjectProtocol>(into subject: S, canUserRetry: Bool, map: @escaping (Error) -> String) -> Signal<Element, Error> where S.Element == UserFriendlyError {
78+
return Signal { observer in
79+
let serialDisposable = SerialDisposable(otherDisposable: nil)
80+
var attempt: (() -> Void)? = nil
81+
attempt = {
82+
let disposables = CompositeDisposable()
83+
serialDisposable.otherDisposable?.dispose()
84+
serialDisposable.otherDisposable = disposables
85+
disposables += self.observe { event in
86+
switch event {
87+
case .next(let element):
88+
observer.next(element)
89+
case .completed:
90+
attempt = nil
91+
observer.completed()
92+
case .failed(let error):
93+
if canUserRetry {
94+
let ce = UserFriendlyError(message: map(error), canRetry: true)
95+
disposables += ce.retry!.observe { event in
96+
switch event {
97+
case .next:
98+
attempt?()
99+
case .completed, .failed:
100+
attempt = nil
101+
observer.failed(error)
102+
}
103+
}
104+
subject.next(ce)
105+
} else {
106+
attempt = nil
107+
subject.next(UserFriendlyError(message: map(error), canRetry: false))
108+
observer.failed(error)
109+
}
110+
}
111+
}
112+
}
113+
attempt?()
114+
return serialDisposable
115+
}
116+
}
117+
118+
public func suppressAndFeedError<S: SubjectProtocol>(into subject: S, canUserRetry: Bool, map: @escaping (Error) -> String) -> Signal<Element, NoError> where S.Element == UserFriendlyError {
119+
return feedError(into: subject, canUserRetry: canUserRetry, map: map).suppressError(logging: true)
120+
}
121+
}

0 commit comments

Comments
 (0)