From 84b4d9dc866d718c72f50662198152bb17c41cf0 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Wed, 1 Feb 2017 12:03:31 +0300 Subject: [PATCH] add base network service --- Cartfile | 1 + Cartfile.resolved | 5 +- LeadKit.podspec | 3 +- LeadKit/LeadKit.xcodeproj/project.pbxproj | 21 ++++ .../Classes/Services/NetworkService.swift | 119 ++++++++++++++++++ .../AlamofireManager+Extensions.swift | 8 +- .../AlamofireRequest+Extensions.swift | 6 +- .../Observable+ToastErrorLogging.swift | 39 ++++++ 8 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 LeadKit/LeadKit/Classes/Services/NetworkService.swift create mode 100644 LeadKit/LeadKit/Extensions/Observable/Observable+ToastErrorLogging.swift diff --git a/Cartfile b/Cartfile index eaf37639..f318e647 100644 --- a/Cartfile +++ b/Cartfile @@ -2,3 +2,4 @@ github "CocoaLumberjack/CocoaLumberjack" ~> 3.0.0 github "ReactiveX/RxSwift" "3.0.1" github "RxSwiftCommunity/RxAlamofire" "3.0.1" github "Hearst-DD/ObjectMapper" ~> 2.1 +github "scalessec/Toast-Swift" ~> 2.0.0 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index 7c9515ec..2144c94c 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,5 +1,6 @@ -github "Alamofire/Alamofire" "4.2.0" +github "Alamofire/Alamofire" "4.3.0" github "CocoaLumberjack/CocoaLumberjack" "3.0.0" -github "Hearst-DD/ObjectMapper" "2.2.1" +github "Hearst-DD/ObjectMapper" "2.2.2" github "ReactiveX/RxSwift" "3.0.1" +github "scalessec/Toast-Swift" "2.0.0" github "RxSwiftCommunity/RxAlamofire" "3.0.1" diff --git a/LeadKit.podspec b/LeadKit.podspec index d5a31e84..8758f8a4 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "0.3.2" + s.version = "0.4.0" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKit" s.license = "Apache License, Version 2.0" @@ -14,4 +14,5 @@ Pod::Spec.new do |s| s.dependency "RxCocoa", '3.0.1' s.dependency "RxAlamofire", '3.0.0' s.dependency "ObjectMapper", '~> 2.1' + s.dependency "Toast-Swift", '~> 2.0.0' end diff --git a/LeadKit/LeadKit.xcodeproj/project.pbxproj b/LeadKit/LeadKit.xcodeproj/project.pbxproj index 7866a701..d776544c 100644 --- a/LeadKit/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit/LeadKit.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ 7827C9391DE4ADB2009DA4E6 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7827C9331DE4ADB2009DA4E6 /* RxSwift.framework */; }; 7834236A1DB8D0E100A79643 /* StoryboardProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 783423691DB8D0E100A79643 /* StoryboardProtocol.swift */; }; 7837F60F1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7837F60E1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift */; }; + 783AF06B1E41CE6C00EC5ADE /* Observable+ToastErrorLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 783AF06A1E41CE6C00EC5ADE /* Observable+ToastErrorLogging.swift */; }; + 783AF06D1E41CF5B00EC5ADE /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 783AF06C1E41CF5B00EC5ADE /* NetworkService.swift */; }; + 783AF06F1E41D84A00EC5ADE /* ToastSwiftFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 783AF06E1E41D84A00EC5ADE /* ToastSwiftFramework.framework */; }; 786D78E81D53C378006B2CEA /* AlamofireRequest+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 786D78E71D53C378006B2CEA /* AlamofireRequest+Extensions.swift */; }; 786D78EC1D53C46E006B2CEA /* AlamofireManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 786D78EB1D53C46E006B2CEA /* AlamofireManager+Extensions.swift */; }; 7873D14F1E1127BC001816EB /* LeadKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7873D14E1E1127BC001816EB /* LeadKitError.swift */; }; @@ -100,6 +103,9 @@ 7827C9331DE4ADB2009DA4E6 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = ../../../Carthage/Build/iOS/RxSwift.framework; sourceTree = ""; }; 783423691DB8D0E100A79643 /* StoryboardProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardProtocol.swift; sourceTree = ""; }; 7837F60E1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EstimatedViewHeightProtocol.swift; sourceTree = ""; }; + 783AF06A1E41CE6C00EC5ADE /* Observable+ToastErrorLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Observable+ToastErrorLogging.swift"; sourceTree = ""; }; + 783AF06C1E41CF5B00EC5ADE /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 783AF06E1E41D84A00EC5ADE /* ToastSwiftFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ToastSwiftFramework.framework; path = ../../../Carthage/Build/iOS/ToastSwiftFramework.framework; sourceTree = ""; }; 786D78E71D53C378006B2CEA /* AlamofireRequest+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AlamofireRequest+Extensions.swift"; sourceTree = ""; }; 786D78EB1D53C46E006B2CEA /* AlamofireManager+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AlamofireManager+Extensions.swift"; sourceTree = ""; }; 7873D14E1E1127BC001816EB /* LeadKitError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeadKitError.swift; sourceTree = ""; }; @@ -163,6 +169,7 @@ buildActionMask = 2147483647; files = ( 7827C9361DE4ADB2009DA4E6 /* ObjectMapper.framework in Frameworks */, + 783AF06F1E41D84A00EC5ADE /* ToastSwiftFramework.framework in Frameworks */, 7827C9351DE4ADB2009DA4E6 /* CocoaLumberjack.framework in Frameworks */, 7827C9391DE4ADB2009DA4E6 /* RxSwift.framework in Frameworks */, 7827C9341DE4ADB2009DA4E6 /* Alamofire.framework in Frameworks */, @@ -191,6 +198,7 @@ 7827C9311DE4ADB2009DA4E6 /* RxAlamofire.framework */, 7827C9321DE4ADB2009DA4E6 /* RxCocoa.framework */, 7827C9331DE4ADB2009DA4E6 /* RxSwift.framework */, + 783AF06E1E41D84A00EC5ADE /* ToastSwiftFramework.framework */, ); path = Frameworks; sourceTree = ""; @@ -249,6 +257,14 @@ path = Sequence; sourceTree = ""; }; + 783AF0591E40824300EC5ADE /* Services */ = { + isa = PBXGroup; + children = ( + 783AF06C1E41CF5B00EC5ADE /* NetworkService.swift */, + ); + path = Services; + sourceTree = ""; + }; 786D78E61D53C355006B2CEA /* Alamofire */ = { isa = PBXGroup; children = ( @@ -272,6 +288,7 @@ isa = PBXGroup; children = ( 787609211E1403830093CE36 /* Observable+DeferredJust.swift */, + 783AF06A1E41CE6C00EC5ADE /* Observable+ToastErrorLogging.swift */, ); path = Observable; sourceTree = ""; @@ -329,6 +346,7 @@ 78A74EAA1C6B401800FE9724 /* Classes */ = { isa = PBXGroup; children = ( + 783AF0591E40824300EC5ADE /* Services */, 78B0FC7B1C6B2BAE00358B64 /* Logging */, 78753E2A1DE58BED006BC0FB /* Cursors */, ); @@ -670,6 +688,7 @@ "$(SRCROOT)/../Carthage/Build/iOS/Alamofire.framework", "$(SRCROOT)/../Carthage/Build/iOS/ObjectMapper.framework", "$(SRCROOT)/../Carthage/Build/iOS/RxAlamofire.framework", + "$(SRCROOT)/../Carthage/Build/iOS/ToastSwiftFramework.framework", ); name = "Carthage copy-frameworks"; outputPaths = ( @@ -714,6 +733,7 @@ 78C36F7E1D801E3E00E7EBEA /* Double+Rounding.swift in Sources */, 78CFEE551C5C45E500F50370 /* NibNameProtocol.swift in Sources */, 787609221E1403830093CE36 /* Observable+DeferredJust.swift in Sources */, + 783AF06B1E41CE6C00EC5ADE /* Observable+ToastErrorLogging.swift in Sources */, 78CFEE561C5C45E500F50370 /* ReuseIdentifierProtocol.swift in Sources */, 78A0FCC81DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift in Sources */, 78B036411DA4D7060021D5CC /* UIImage+Extensions.swift in Sources */, @@ -723,6 +743,7 @@ 78C36F811D8021DD00E7EBEA /* UIColor+Hex.swift in Sources */, 78CFEE5B1C5C45E500F50370 /* ViewModelProtocol.swift in Sources */, EF5FB5691E0141610030E4BE /* UIView+Rotation.swift in Sources */, + 783AF06D1E41CF5B00EC5ADE /* NetworkService.swift in Sources */, 780F56CC1E0D7ACA004530B6 /* ObservableMappable.swift in Sources */, 780D23431DA412470084620D /* CGImage+Alpha.swift in Sources */, 78CFEE5A1C5C45E500F50370 /* ViewHeightProtocol.swift in Sources */, diff --git a/LeadKit/LeadKit/Classes/Services/NetworkService.swift b/LeadKit/LeadKit/Classes/Services/NetworkService.swift new file mode 100644 index 00000000..f7ddb42c --- /dev/null +++ b/LeadKit/LeadKit/Classes/Services/NetworkService.swift @@ -0,0 +1,119 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import RxSwift +import RxCocoa +import Alamofire +import ObjectMapper +import RxAlamofire + +/// Base network service implementation build on top of LeadKit extensions for Alamofire. +/// Has an ability to automatically show / hide network activity indicator +/// and shows errors in DEBUG mode +open class NetworkService { + + private let disposeBag = DisposeBag() + private let requestCount = Variable(0) + + public let sessionManager: Alamofire.SessionManager + + /// Let netwrok service automatically show / hide activity indicator + public func bindActivityIndicator() { + return requestCount.asDriver() + .map { $0 != 0 } + .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible) + .addDisposableTo(disposeBag) + } + + /// Creates new instance of NetworkService with given Alamofire session manager + /// + /// - Parameter sessionManager: Alamofire.SessionManager to use for requests + public init(sessionManager: Alamofire.SessionManager) { + self.sessionManager = sessionManager + } + + /// Perform reactive request to get mapped ObservableMappable model and http response + /// + /// - Parameter parameters: api parameters to pass Alamofire + /// - Returns: Observable of tuple containing (HTTPURLResponse, ObservableMappable) + public func rxRequest(with parameters: ApiRequestParameters) + -> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T { + + return sessionManager.rx.responseObservableModel(requestParameters: parameters) + .counterTracking(for: self) + .showErrorsInToastInDebugMode() + } + + /// Perform reactive request to get mapped ImmutableMappable model and http response + /// + /// - Parameter parameters: api parameters to pass Alamofire + /// - Returns: Observable of tuple containing (HTTPURLResponse, ImmutableMappable) + public func rxRequest(with parameters: ApiRequestParameters) + -> Observable<(response: HTTPURLResponse, model: T)> { + + return sessionManager.rx.responseModel(requestParameters: parameters) + .counterTracking(for: self) + .showErrorsInToastInDebugMode() + } + + /// Perform reactive request to get UIImage and http response + /// + /// - Parameter url: An object adopting `URLConvertible` + /// - Returns: Observable of tuple containing (HTTPURLResponse, UIImage?) + public func rxLoadImage(url: String) -> Observable<(HTTPURLResponse, UIImage?)> { + let request = RxAlamofire.requestData(.get, url, headers: [:]) + + return request + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .map { (response, data) -> (HTTPURLResponse, UIImage?) in + (response, UIImage(data: data)) + } + .counterTracking(for: self) + .showErrorsInToastInDebugMode() + } + + fileprivate func increaseRequestCounter() { + requestCount.value += 1 + } + + fileprivate func decreaseRequestCounter() { + requestCount.value -= 1 + } + +} + +public extension Observable { + + /// Increase and descrease NetworkService request counter on subscribe and dispose + /// (used to show / hide activity indicator) + /// + /// - Parameter networkService: NetworkService to operate on it + /// - Returns: The source sequence with the side-effecting behavior applied. + func counterTracking(for networkService: NetworkService) -> Observable { + return `do`(onSubscribe: { + networkService.increaseRequestCounter() + }, onDispose: { + networkService.decreaseRequestCounter() + }) + } + +} diff --git a/LeadKit/LeadKit/Extensions/Alamofire/AlamofireManager+Extensions.swift b/LeadKit/LeadKit/Extensions/Alamofire/AlamofireManager+Extensions.swift index e96241da..0b33ef58 100644 --- a/LeadKit/LeadKit/Extensions/Alamofire/AlamofireManager+Extensions.swift +++ b/LeadKit/LeadKit/Extensions/Alamofire/AlamofireManager+Extensions.swift @@ -45,7 +45,9 @@ public extension Reactive where Base: Alamofire.SessionManager { /// - Parameter mappingQueue: The dispatch queue to use for mapping /// - Returns: Observable with HTTP URL Response and target object func responseModel(requestParameters: ApiRequestParameters, - mappingQueue: DispatchQueue = DispatchQueue.global()) -> Observable<(HTTPURLResponse, T)> { + mappingQueue: DispatchQueue = DispatchQueue.global()) + -> Observable<(response: HTTPURLResponse, model: T)> { + return apiRequest(requestParameters: requestParameters) .flatMap { $0.rx.apiResponse(mappingQueue: mappingQueue) } } @@ -56,8 +58,8 @@ public extension Reactive where Base: Alamofire.SessionManager { /// - Parameter mappingQueue: The dispatch queue to use for mapping /// - Returns: Observable with HTTP URL Response and target object func responseObservableModel(requestParameters: ApiRequestParameters, - mappingQueue: DispatchQueue = DispatchQueue.global()) -> Observable<(HTTPURLResponse, T)> - where T.ModelType == T { + mappingQueue: DispatchQueue = DispatchQueue.global()) + -> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T { return apiRequest(requestParameters: requestParameters) .flatMap { $0.rx.apiResponse(mappingQueue: mappingQueue) } diff --git a/LeadKit/LeadKit/Extensions/Alamofire/AlamofireRequest+Extensions.swift b/LeadKit/LeadKit/Extensions/Alamofire/AlamofireRequest+Extensions.swift index 4515b9f7..7fa659f3 100644 --- a/LeadKit/LeadKit/Extensions/Alamofire/AlamofireRequest+Extensions.swift +++ b/LeadKit/LeadKit/Extensions/Alamofire/AlamofireRequest+Extensions.swift @@ -32,7 +32,7 @@ public extension Reactive where Base: DataRequest { /// - Parameter mappingQueue: The dispatch queue to use for mapping /// - Returns: Observable with HTTP URL Response and target object func apiResponse(mappingQueue: DispatchQueue = DispatchQueue.global()) - -> Observable<(HTTPURLResponse, T)> { + -> Observable<(response: HTTPURLResponse, model: T)> { return responseJSONOnQueue(mappingQueue) .map { resp, value in @@ -47,10 +47,10 @@ public extension Reactive where Base: DataRequest { /// - Parameter mappingQueue: The dispatch queue to use for mapping /// - Returns: Observable with HTTP URL Response and target object func apiResponse(mappingQueue: DispatchQueue = DispatchQueue.global()) - -> Observable<(HTTPURLResponse, T)> where T.ModelType == T { + -> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T { return responseJSONOnQueue(mappingQueue) - .flatMap { resp, value -> Observable<(HTTPURLResponse, T)> in + .flatMap { resp, value -> Observable<(response: HTTPURLResponse, model: T)> in let json = try cast(value) as [String: Any] return T.createFrom(map: Map(mappingType: .fromJSON, JSON: json)) diff --git a/LeadKit/LeadKit/Extensions/Observable/Observable+ToastErrorLogging.swift b/LeadKit/LeadKit/Extensions/Observable/Observable+ToastErrorLogging.swift new file mode 100644 index 00000000..586c0212 --- /dev/null +++ b/LeadKit/LeadKit/Extensions/Observable/Observable+ToastErrorLogging.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import RxSwift +import Toast_Swift + +public extension Observable { + + /// Method which shows toast with localized description of error in DEBUG mode + /// + /// - Returns: The source sequence with the side-effecting behavior applied. + func showErrorsInToastInDebugMode() -> Observable { + return `do`(onError: { (error) in + #if DEBUG + UIApplication.shared.keyWindow?.makeToast(error.localizedDescription) + #endif + }) + } + +}