diff --git a/Sources/Common/Extensions/Combine/Result+IsStatus.swift b/Sources/Common/Extensions/Combine/Result+IsStatus.swift new file mode 100644 index 0000000..6b16755 --- /dev/null +++ b/Sources/Common/Extensions/Combine/Result+IsStatus.swift @@ -0,0 +1,25 @@ +// +// Result+IsStatus.swift +// +// +// Created by Miguel Angel on 08-08-21. +// + +import Combine + +extension Result { + + public var isSuccess: Bool { + switch self { + case .success: return true + default: return false + } + } + + public var isFailure: Bool { + switch self { + case .failure: return true + default: return false + } + } +} diff --git a/Sources/Common/Extensions/Foundation/Range+NSRange.swift b/Sources/Common/Extensions/Foundation/Range+NSRange.swift new file mode 100644 index 0000000..436c910 --- /dev/null +++ b/Sources/Common/Extensions/Foundation/Range+NSRange.swift @@ -0,0 +1,16 @@ +// +// Range+NSRange.swift +// +// +// Created by Miguel Angel on 09-08-21. +// + +import Foundation + +extension Range where Bound == String.Index { + + public var nsRange:NSRange { + return NSRange(location: self.lowerBound.encodedOffset, length: self.upperBound.encodedOffset - self.lowerBound.encodedOffset) + } + +} diff --git a/Sources/Common/Extensions/UIKit/SearchBar+Enabled.swift b/Sources/Common/Extensions/UIKit/SearchBar+Enabled.swift new file mode 100644 index 0000000..42126c0 --- /dev/null +++ b/Sources/Common/Extensions/UIKit/SearchBar+Enabled.swift @@ -0,0 +1,21 @@ +// +// SearchBar+Enabled.swift +// +// +// Created by Miguel Angel on 13-08-21. +// + +import UIKit + +extension UISearchBar { + public func enable() { + isUserInteractionEnabled = true + alpha = 1.0 + } + + public func disable() { + isUserInteractionEnabled = false + alpha = 0.5 + } +} + diff --git a/Sources/Common/Extensions/UIKit/UITapGesture+TapAttributedText.swift b/Sources/Common/Extensions/UIKit/UITapGesture+TapAttributedText.swift new file mode 100644 index 0000000..205d40f --- /dev/null +++ b/Sources/Common/Extensions/UIKit/UITapGesture+TapAttributedText.swift @@ -0,0 +1,39 @@ +// +// UITapGesture+TapAttributedText.swift +// +// +// Created by Miguel Angel on 09-08-21. +// + +import UIKit + +extension UITapGestureRecognizer { + + public func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: CGSize.zero) + let textStorage = NSTextStorage(attributedString: label.attributedText!) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = label.lineBreakMode + textContainer.maximumNumberOfLines = label.numberOfLines + let labelSize = label.bounds.size + textContainer.size = labelSize + + let locationOfTouchInLabel = self.location(in: label) + let textBoundingBox = layoutManager.usedRect(for: textContainer) + + let textContainerOffset = CGPoint( + x: (labelSize.width - textBoundingBox.size.width) * 0.3 - textBoundingBox.origin.x, + y: (labelSize.height - textBoundingBox.size.height) * 0.3 - textBoundingBox.origin.y + ) + + let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y) + let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + return NSLocationInRange(indexOfCharacter, targetRange) + } + +} diff --git a/Sources/Common/Extensions/UIKit/UITextField+SetupKeyboard.swift b/Sources/Common/Extensions/UIKit/UITextField+SetupKeyboard.swift new file mode 100644 index 0000000..137ca35 --- /dev/null +++ b/Sources/Common/Extensions/UIKit/UITextField+SetupKeyboard.swift @@ -0,0 +1,43 @@ +// +// UITextField+SetupKeyboard.swift +// +// +// Created by Miguel Angel on 09-08-21. +// + +import UIKit + +public enum KeyboardType { + case email, password, numbers, phoneNumber, normal +} + +extension UITextField { + + public func setupKeyboard(_ type: KeyboardType, returnKeyType: UIReturnKeyType) { + self.autocapitalizationType = .none + self.autocorrectionType = .no + self.spellCheckingType = .no + self.keyboardAppearance = .default + self.returnKeyType = returnKeyType + self.clearButtonMode = .always + + switch type { + case .email: + self.keyboardType = .emailAddress + + case .password: + self.keyboardType = .default + self.isSecureTextEntry = true + + case .numbers: + self.keyboardType = .numbersAndPunctuation + + case .phoneNumber: + self.keyboardType = .phonePad + + case .normal: + self.keyboardType = .default + } + } + +} diff --git a/Sources/Common/Extensions/UIKit/UIView+SetCornerRadius.swift b/Sources/Common/Extensions/UIKit/UIView+SetCornerRadius.swift new file mode 100644 index 0000000..f413d84 --- /dev/null +++ b/Sources/Common/Extensions/UIKit/UIView+SetCornerRadius.swift @@ -0,0 +1,17 @@ +// +// UIView+SetCornerRadius.swift +// +// +// Created by Miguel Angel on 03-08-21. +// + +import UIKit + +extension UIView { + + public func set(cornerRadius: CGFloat) { + self.clipsToBounds = true + self.layer.cornerRadius = cornerRadius + } + +} diff --git a/Sources/Common/Extensions/UIKit/UIView+SetSubviewAutolayout.swift b/Sources/Common/Extensions/UIKit/UIView+SetSubviewAutolayout.swift new file mode 100644 index 0000000..85ed04e --- /dev/null +++ b/Sources/Common/Extensions/UIKit/UIView+SetSubviewAutolayout.swift @@ -0,0 +1,22 @@ +// +// UIView+SetSubviewAutolayout.swift +// +// +// Created by Miguel Angel on 03-08-21. +// + +import UIKit + +extension UIView { + + public func setSubviewForAutoLayout(_ subview: UIView) { + subview.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(subview) + } + + public func setSubviewsForAutoLayout(_ subviews: [UIView]) { + subviews.forEach(setSubviewForAutoLayout(_:)) + } + +} + diff --git a/Sources/Common/PropertyWrappers/Localized.swift b/Sources/Common/PropertyWrappers/Localized.swift new file mode 100644 index 0000000..561ed66 --- /dev/null +++ b/Sources/Common/PropertyWrappers/Localized.swift @@ -0,0 +1,23 @@ +// +// Localized.swift +// +// +// Created by Miguel Angel on 03-08-21. +// + +import Foundation + +@propertyWrapper +public struct Localized { + private var key: String + private var stringsFileName: String + + public init(_ key: String, stringsFileName: String = "Localizable") { + self.key = key + self.stringsFileName = stringsFileName + } + + public var wrappedValue: String { + NSLocalizedString(key, tableName: stringsFileName, comment: "") + } +} diff --git a/Sources/Common/Protocols/Coordinator.swift b/Sources/Common/Protocols/Coordinator.swift new file mode 100644 index 0000000..2c414d7 --- /dev/null +++ b/Sources/Common/Protocols/Coordinator.swift @@ -0,0 +1,21 @@ +// +// Coordinator.swift +// +// +// Created by Miguel Angel on 03-08-21. +// + +import UIKit + +public protocol Coordinator { + func start() + func coordinate(to coordinator: Coordinator) +} + +public extension Coordinator { + + func coordinate(to coordinator: Coordinator) { + coordinator.start() + } + +} diff --git a/Sources/Common/Protocols/Exception.swift b/Sources/Common/Protocols/Exception.swift index ec2b971..4fde75a 100644 --- a/Sources/Common/Protocols/Exception.swift +++ b/Sources/Common/Protocols/Exception.swift @@ -7,6 +7,7 @@ import Foundation public protocol Exception: LocalizedError { var code: String { get } var category: ExceptionCategory { get } + var errorTitle: String? { get } var errorDescription: String? { get } } @@ -15,4 +16,29 @@ public enum ExceptionCategory { case network case storage case mappers + case unknown +} + +public enum GenericException: Exception { + case unknown(_ underlying: Error) + + public var category: ExceptionCategory { + .unknown + } + + public var code: String { + return "mdk.cmn.00" + } + + public var errorTitle: String? { + return "An Exception ocurred" + } + + public var errorDescription: String? { + switch self { + case .unknown(let error): + return "Error: \(error)" + } + } + } diff --git a/Sources/Common/Protocols/Mappers.swift b/Sources/Common/Protocols/Mappers.swift index 660334d..95b56be 100644 --- a/Sources/Common/Protocols/Mappers.swift +++ b/Sources/Common/Protocols/Mappers.swift @@ -53,6 +53,10 @@ extension MapperException: Exception { } } + public var errorTitle: String? { + return "An exception occurred" + } + public var errorDescription: String? { switch self { case .cantMapToModel: diff --git a/Sources/Providers/Network/Agents/NSUrlSessionAgent.swift b/Sources/Providers/Network/Agents/NSUrlSessionAgent.swift index 38dd98f..7e3e65e 100644 --- a/Sources/Providers/Network/Agents/NSUrlSessionAgent.swift +++ b/Sources/Providers/Network/Agents/NSUrlSessionAgent.swift @@ -10,29 +10,29 @@ final class NSUrlSessionAgent: NetworkAgent { public init () { } - public func run(_ endpoint: Endpoint) -> AnyPublisher where Endpoint: EndpointProvider { + public func run(_ endpoint: Endpoint) -> AnyPublisher where Endpoint: EndpointProvider { guard let url = URL(string: endpoint.path) else { - return AnyPublisher(Fail(error: .invalidURL)) + return AnyPublisher(Fail(error: NetworkException.invalidURL)) } guard Reachability.isNetworkReachable() else { - return AnyPublisher(Fail(error: .unreachable)) + return AnyPublisher(Fail(error: NetworkException.unreachable)) } - var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) + var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 6.0) request.httpMethod = httpMethod(from: endpoint.method) request.allHTTPHeaderFields = endpoint.headers if endpoint.parameters != nil { guard let postParams = try? JSONEncoder().encode(endpoint.parameters) else { - return AnyPublisher(Fail(error: .invalidPostParams)) + return AnyPublisher(Fail(error: NetworkException.invalidPostParams)) } request.httpBody = postParams } return URLSession.shared .dataTaskPublisher(for: request) - .retry(3) + .retry(1) .tryMap { data, response in let code = (response as? HTTPURLResponse)?.statusCode ?? -1 let statusCode = HTTPStatusCode(rawCode: code) diff --git a/Sources/Providers/Network/NetworkAgent.swift b/Sources/Providers/Network/NetworkAgent.swift index 9fe2367..27d8e76 100644 --- a/Sources/Providers/Network/NetworkAgent.swift +++ b/Sources/Providers/Network/NetworkAgent.swift @@ -7,7 +7,7 @@ import AltairMDKCommon import Combine public protocol NetworkAgent: AnyObject { - func run(_ endpoint: Endpoint) -> AnyPublisher + func run(_ endpoint: Endpoint) -> AnyPublisher } public enum NetworkException { @@ -37,6 +37,10 @@ extension NetworkException: Exception { } } + public var errorTitle: String? { + return "An exception occurred" + } + public var errorDescription: String? { switch self { case .unknown(let error): diff --git a/Sources/Providers/Storage/Agents/CoreData/CoreDataAgent.swift b/Sources/Providers/Storage/Agents/CoreData/CoreDataAgent.swift index b0c719b..83decf9 100644 --- a/Sources/Providers/Storage/Agents/CoreData/CoreDataAgent.swift +++ b/Sources/Providers/Storage/Agents/CoreData/CoreDataAgent.swift @@ -11,7 +11,6 @@ import Foundation // TODO: Add Logger for track the exceptions and replace print - @mzapatae at 01/06/21 final class CoreDataAgent: StorageAgent { - var managedContext: NSManagedObjectContext? required init(configuration: ConfigurationType) { @@ -30,7 +29,7 @@ final class CoreDataAgent: StorageAgent { private func initDB(urlModel: URL?, storeType: StoreType) throws { let coordinator = try CoreDataStoreCoordinator.persistentStoreCoordinator(urlModel: urlModel, storeType: storeType) - self.managedContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + self.managedContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) self.managedContext?.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy self.managedContext?.persistentStoreCoordinator = coordinator } @@ -47,107 +46,119 @@ final class CoreDataAgent: StorageAgent { return NSManagedObject(entity: entityDescription, insertInto: context) as? T } - func insert(object: Storable) -> AnyPublisher { - guard let managedObject = object as? NSManagedObject else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - context.insert(managedObject) - try context.save() - return Just(()).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Insert object error: \(error)") - return Fail(error: .insertObjectFail).eraseToAnyPublisher() + func insert(object: Storable) -> Future { + return Future() { promise in + guard let managedObject = object as? NSManagedObject else { return promise(.failure(StorageException.notInitialized)) } + guard let context = self.managedContext else { return promise(.failure(StorageException.objectNotSupported)) } + do { + context.insert(managedObject) + try context.save() + promise(.success(())) + } catch { + print("Insert object error: \(error)") + promise(.failure(StorageException.insertObjectFail)) + } } } - func insertAll(objects: [Storable]) -> AnyPublisher { - guard let managedObjects = objects as? [NSManagedObject] else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - managedObjects.forEach { context.insert($0) } - try context.save() - return Just(()).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Insert all objects error: \(error)") - return Fail(error: .insertObjectFail).eraseToAnyPublisher() + func insertAll(objects: [Storable]) -> Future { + return Future() { promise in + guard let managedObjects = objects as? [NSManagedObject] else { return promise(.failure(StorageException.objectNotSupported)) } + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + managedObjects.forEach { context.insert($0) } + try context.save() + promise(.success(())) + } catch { + print("Insert all objects error: \(error)") + promise(.failure(StorageException.insertObjectFail)) + } } } - - func update(object: Storable) -> AnyPublisher { - guard let managedObject = object as? NSManagedObject else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - if managedObject.isUpdated { - try context.save() - return Just(()).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } else { - print("Not exist and update for the object") - return Fail(error: .updateObjectFail).eraseToAnyPublisher() + + func update(object: Storable) -> Future { + return Future() { promise in + guard let managedObject = object as? NSManagedObject else { return promise(.failure(StorageException.objectNotSupported)) } + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + if managedObject.isUpdated { + try context.save() + promise(.success(())) + } else { + print("Not exist and update for the object") + promise(.failure(StorageException.updateObjectFail)) + } + } catch { + print("Update object error: \(error)") + promise(.failure(StorageException.updateObjectFail)) } - } catch { - print("Update object error: \(error)") - return Fail(error: .updateObjectFail).eraseToAnyPublisher() } } - func delete(object: Storable) -> AnyPublisher { - guard let managedObject = object as? NSManagedObject else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - context.delete(managedObject) - try context.save() - return Just(()).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Delete object error: \(error)") - return Fail(error: .deleteObjectFail).eraseToAnyPublisher() + func delete(object: Storable) -> Future { + return Future() { promise in + guard let managedObject = object as? NSManagedObject else { return promise(.failure(StorageException.objectNotSupported)) } + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + context.delete(managedObject) + try context.save() + promise(.success(())) + } catch { + print("Delete object error: \(error)") + promise(.failure(StorageException.deleteObjectFail)) + } } } - func deleteAll(_ model: Storable.Type, predicate: NSPredicate?) -> AnyPublisher { - guard let type = model as? NSManagedObject.Type else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - let fetchRequest = type.fetchRequest() as NSFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.predicate = predicate + func deleteAll(_ model: Storable.Type, predicate: NSPredicate?) -> Future { + return Future() { promise in + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + let fetchRequest = NSFetchRequest(entityName: model.entityName) + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.predicate = predicate - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - try context.execute(deleteRequest) - try context.save() - return Just(()).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Delete all objects error: \(error)") - return Fail(error: .deleteObjectFail).eraseToAnyPublisher() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + try context.execute(deleteRequest) + try context.save() + promise(.success(())) + } catch { + print("Delete all objects error: \(error)") + promise(.failure(StorageException.deleteObjectFail)) + } } + } - func readFirst(_ model: T.Type, predicate: NSPredicate?) -> AnyPublisher where T: Storable { - guard let type = model as? NSManagedObject.Type else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - let fetchRequest = type.fetchRequest() as NSFetchRequest - fetchRequest.predicate = predicate - - let result = try context.fetch(fetchRequest).first as? T - return Just(result).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Read First object error: \(error)") - return Fail(outputType: T?.self, failure: .readObjectFail).eraseToAnyPublisher() + func readFirst(_ model: T.Type, predicate: NSPredicate?) -> Future where T: Storable { + return Future() { promise in + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + let fetchRequest = NSFetchRequest(entityName: model.entityName) + fetchRequest.predicate = predicate + + let result = try context.fetch(fetchRequest).first as? T + promise(.success(result)) + } catch { + print("Read First object error: \(error)") + return promise(.failure(StorageException.readObjectFail)) + } } } - func readAll(_ model: T.Type, predicate: NSPredicate?) -> AnyPublisher<[T], StorageException> where T: Storable { - guard let type = model as? NSManagedObject.Type else { return Fail(error: .objectNotSupported).eraseToAnyPublisher() } - guard let context = managedContext else { return Fail(error: .notInitialized).eraseToAnyPublisher() } - do { - let fetchRequest = type.fetchRequest() as NSFetchRequest - fetchRequest.predicate = predicate - - let results = try context.fetch(fetchRequest) as? [T] ?? [] - return Just(results).setFailureType(to: StorageException.self).eraseToAnyPublisher() - } catch { - print("Read all objects error: \(error)") - return Fail(outputType: [T].self, failure: .readObjectFail).eraseToAnyPublisher() + func readAll(_ model: T.Type, predicate: NSPredicate?) -> Future<[T], Error> where T: Storable { + return Future() { promise in + guard (model as? NSManagedObject.Type) != nil else { return promise(.failure(StorageException.objectNotSupported)) } + guard let context = self.managedContext else { return promise(.failure(StorageException.notInitialized)) } + do { + let fetchRequest = NSFetchRequest(entityName: model.entityName) + fetchRequest.predicate = predicate + let results = try context.fetch(fetchRequest) as? [T] ?? [] + promise(.success(results)) + } catch { + print("Read all objects error: \(error)") + return promise(.failure(StorageException.readObjectFail)) + } } } diff --git a/Sources/Providers/Storage/StorageAgent.swift b/Sources/Providers/Storage/StorageAgent.swift index 764ad93..deebd24 100644 --- a/Sources/Providers/Storage/StorageAgent.swift +++ b/Sources/Providers/Storage/StorageAgent.swift @@ -11,13 +11,13 @@ import AltairMDKCommon public protocol StorageAgent: AnyObject { func create(_ model: T.Type) -> T? - func insert(object: Storable) -> AnyPublisher - func insertAll(objects: [Storable]) -> AnyPublisher - func update(object: Storable) -> AnyPublisher - func delete(object: Storable) -> AnyPublisher - func deleteAll(_ model: Storable.Type, predicate: NSPredicate?) -> AnyPublisher - func readFirst(_ model: T.Type, predicate: NSPredicate?) -> AnyPublisher - func readAll(_ model: T.Type, predicate: NSPredicate?) -> AnyPublisher<[T], StorageException> + func insert(object: Storable) -> Future + func insertAll(objects: [Storable]) -> Future + func update(object: Storable) -> Future + func delete(object: Storable) -> Future + func deleteAll(_ model: Storable.Type, predicate: NSPredicate?) -> Future + func readFirst(_ model: T.Type, predicate: NSPredicate?) -> Future + func readAll(_ model: T.Type, predicate: NSPredicate?) -> Future<[T], Error> } public enum StorageException { @@ -54,6 +54,10 @@ extension StorageException: Exception { } } + public var errorTitle: String? { + return "An exception occurred" + } + public var errorDescription: String? { switch self { case .unknown(let error):