diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8a78a69d --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +*.DS_Store +*.xcuserstate +build/ +DerivedData/ +*.pbxproj +*.xcworkspace/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +*.ipa +*.dSYM.zip +*.dSYM +timeline.xctimeline +playground.xcworkspace +.build/ +Pods/ +Carthage/Build +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output +Breakpoints_v2.xcbkptlist +*.xcodeproj/* +/Santander/Santander.xcodeproj \ No newline at end of file diff --git a/Santander/Gemfile b/Santander/Gemfile new file mode 100644 index 00000000..ea0699a4 --- /dev/null +++ b/Santander/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'cocoapods', '~> 1.6.1' \ No newline at end of file diff --git a/Santander/Gemfile.lock b/Santander/Gemfile.lock new file mode 100644 index 00000000..c47d26fe --- /dev/null +++ b/Santander/Gemfile.lock @@ -0,0 +1,76 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.0) + activesupport (4.2.11.1) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + atomos (0.1.3) + claide (1.0.2) + cocoapods (1.6.2) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.6.2) + cocoapods-deintegrate (>= 1.0.2, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.3.1, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.2.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.6.6) + nap (~> 1.0) + ruby-macho (~> 1.4) + xcodeproj (>= 1.8.1, < 2.0) + cocoapods-core (1.6.2) + activesupport (>= 4.0.2, < 6) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-deintegrate (1.0.4) + cocoapods-downloader (1.2.2) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.1.0) + cocoapods-trunk (1.3.1) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.1.0) + colored2 (3.1.2) + concurrent-ruby (1.1.5) + escape (0.0.4) + fourflusher (2.2.0) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + minitest (5.11.3) + molinillo (0.6.6) + nanaimo (0.2.6) + nap (1.1.0) + netrc (0.11.0) + ruby-macho (1.4.0) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + xcodeproj (1.9.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (~> 1.6.1) + +BUNDLED WITH + 2.0.2 diff --git a/Santander/Podfile b/Santander/Podfile new file mode 100644 index 00000000..47de9cd8 --- /dev/null +++ b/Santander/Podfile @@ -0,0 +1,19 @@ +use_frameworks! +inhibit_all_warnings! +platform :ios, '9.0' + +def app_pods + pod 'SnapKit' + pod 'Eureka' + pod 'TLCustomMask' + pod 'Moya', '~> 13.0' + pod 'JGProgressHUD', '~> 2.0' +end + +target 'Santander' do + app_pods +end + +target 'SantanderUnitTests' do + app_pods +end diff --git a/Santander/Podfile.lock b/Santander/Podfile.lock new file mode 100644 index 00000000..d0101bfa --- /dev/null +++ b/Santander/Podfile.lock @@ -0,0 +1,42 @@ +PODS: + - Alamofire (4.8.2) + - Eureka (4.3.1) + - JGProgressHUD (2.0.4) + - Moya (13.0.1): + - Moya/Core (= 13.0.1) + - Moya/Core (13.0.1): + - Alamofire (~> 4.1) + - Result (~> 4.1) + - Result (4.1.0) + - SnapKit (4.2.0) + - TLCustomMask (2.0.0) + +DEPENDENCIES: + - Eureka + - JGProgressHUD (~> 2.0) + - Moya (~> 13.0) + - SnapKit + - TLCustomMask + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - Alamofire + - Eureka + - JGProgressHUD + - Moya + - Result + - SnapKit + - TLCustomMask + +SPEC CHECKSUMS: + Alamofire: ae5c501addb7afdbb13687d7f2f722c78734c2d3 + Eureka: 28ea296f06710f6745266b71f17862048941a32d + JGProgressHUD: 62658b14e72cccf179efc7a13bcb54d30b92fc22 + Moya: f4a4b80ff2f8a4ffc208dfb31cd91636622fee6e + Result: bd966fac789cc6c1563440b348ab2598cc24d5c7 + SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a + TLCustomMask: 32bcd873851167c2d2a3726b048766b737873710 + +PODFILE CHECKSUM: 96e7b800b5a7b93d749bdfce5e50a90371f01bdb + +COCOAPODS: 1.6.2 diff --git a/Santander/README.md b/Santander/README.md new file mode 100644 index 00000000..b1523495 --- /dev/null +++ b/Santander/README.md @@ -0,0 +1,37 @@ +# Setup Guide + +## Requirements + +- iOS 9.0+ +- Xcode 10.0+ +- Swift 5.0 + +## Development Dependencies + +In order to work in this project, some dependencies are needed and are also described below. + +- [bundler](https://bundler.io) - Bundler encapsulates the project environment by locking our ruby dependencies versions. +- [xcodegen](https://github.com/yonaskolb/XcodeGen) - Generates the xcodeproj you'll be working on with the proper folder structure and specs. + +## Building Santander + +1. Install [Xcode developer tools](https://developer.apple.com/xcode/downloads/) from Apple. +1. Clone the repository: + ```shell + git clone git@github.com:orlandoamorim/TesteiOS.git + ``` +1. Pull in the project dependencies: + ### If you're running the project for the first time run this + ```shell + sh ./bootstrap.sh --install-bundler + ``` + + ### else run this + ```shell + sh ./bootstrap.sh + ``` +1. Open `Santander.xcworkspace` in Xcode. + +# Info + + - Company: [Zup](https://www.zup.com.br/) \ No newline at end of file diff --git a/Santander/Santander/Application/AppDelegate.swift b/Santander/Santander/Application/AppDelegate.swift new file mode 100644 index 00000000..1d372bcf --- /dev/null +++ b/Santander/Santander/Application/AppDelegate.swift @@ -0,0 +1,23 @@ +// +// AppDelegate.swift +// Santander +// +// Created by Orlando Amorim on 10/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + window?.makeKeyAndVisible() + window?.rootViewController = UINavigationController(rootViewController: ContainerViewController()) + return true + } +} + diff --git a/Santander/Santander/Application/Propeties/Info.plist b/Santander/Santander/Application/Propeties/Info.plist new file mode 100644 index 00000000..4f639edf --- /dev/null +++ b/Santander/Santander/Application/Propeties/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIAppFonts + + DINPro-Medium.otf + DINPro-Regular.otf + DINPro-Light.otf + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Santander/Santander/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/Contents.json b/Santander/Santander/Assets/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/Contents.json new file mode 100644 index 00000000..04165373 --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/arrow-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/arrow-icon.pdf new file mode 100644 index 00000000..487b393b Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/arrow-icon.imageset/arrow-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/Contents.json new file mode 100644 index 00000000..825d1dab --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "checkmark-selected-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/checkmark-selected-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/checkmark-selected-icon.pdf new file mode 100644 index 00000000..66ac3e60 Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/checkmark-selected-icon.imageset/checkmark-selected-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/Contents.json new file mode 100644 index 00000000..1a475609 --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "checkmark-unselected-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/checkmark-unselected-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/checkmark-unselected-icon.pdf new file mode 100644 index 00000000..71e334d1 Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/checkmark-unselected-icon.imageset/checkmark-unselected-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/Contents.json new file mode 100644 index 00000000..672b7e06 --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "clear-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/clear-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/clear-icon.pdf new file mode 100644 index 00000000..a9885067 Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/clear-icon.imageset/clear-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/Contents.json new file mode 100644 index 00000000..64ada0ab --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "download-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/download-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/download-icon.pdf new file mode 100644 index 00000000..6d6e6929 Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/download-icon.imageset/download-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/Contents.json new file mode 100644 index 00000000..c6fac41f --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "separator-line-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "template-rendering-intent" : "original", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/separator-line-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/separator-line-icon.pdf new file mode 100644 index 00000000..ac0c6a7f Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/separator-line-arrow-icon.imageset/separator-line-icon.pdf differ diff --git a/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/Contents.json b/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/Contents.json new file mode 100644 index 00000000..c7a6a6e0 --- /dev/null +++ b/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "share-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "compression-type" : "automatic", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf b/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf new file mode 100644 index 00000000..5617f428 Binary files /dev/null and b/Santander/Santander/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf differ diff --git a/Santander/Santander/Assets/Base.lproj/LaunchScreen.storyboard b/Santander/Santander/Assets/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bfa36129 --- /dev/null +++ b/Santander/Santander/Assets/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Santander/Santander/Assets/Fonts/DINPro-Light.otf b/Santander/Santander/Assets/Fonts/DINPro-Light.otf new file mode 100755 index 00000000..8a7f085a Binary files /dev/null and b/Santander/Santander/Assets/Fonts/DINPro-Light.otf differ diff --git a/Santander/Santander/Assets/Fonts/DINPro-Medium.otf b/Santander/Santander/Assets/Fonts/DINPro-Medium.otf new file mode 100755 index 00000000..b4608d06 Binary files /dev/null and b/Santander/Santander/Assets/Fonts/DINPro-Medium.otf differ diff --git a/Santander/Santander/Assets/Fonts/DINPro-Regular.otf b/Santander/Santander/Assets/Fonts/DINPro-Regular.otf new file mode 100755 index 00000000..84d57abb Binary files /dev/null and b/Santander/Santander/Assets/Fonts/DINPro-Regular.otf differ diff --git a/Santander/Santander/Library/Eureka Rules/RulePhoneNumber.swift b/Santander/Santander/Library/Eureka Rules/RulePhoneNumber.swift new file mode 100644 index 00000000..7cafb4d4 --- /dev/null +++ b/Santander/Santander/Library/Eureka Rules/RulePhoneNumber.swift @@ -0,0 +1,39 @@ +// +// RulePhoneNumber.swift +// Santander +// +// Created by Orlando Amorim on 13/08/19. +// + +import Eureka + +public class RulePhoneNumber: RuleType { + + public var regExpr: String + public var id: String? + public var validationError: ValidationError + public var allowsEmpty = true + + public init(regExpr: String = "^(\\([0-9]{2}\\))\\s([9]{1})?([0-9]{4})-([0-9]{4})$", + allowsEmpty: Bool = true, + msg: String = "Field value should be a valid phone number!", + id: String? = nil) { + self.validationError = ValidationError(msg: msg) + self.regExpr = regExpr + self.allowsEmpty = allowsEmpty + self.id = id + } + + public func isValid(value: String?) -> ValidationError? { + if let value = value, !value.isEmpty { + let predicate = NSPredicate(format: "SELF MATCHES %@", regExpr) + guard predicate.evaluate(with: value) else { + return validationError + } + return nil + } else if !allowsEmpty { + return validationError + } + return nil + } +} diff --git a/Santander/Santander/Library/Extensions/MoyaProvider.swift b/Santander/Santander/Library/Extensions/MoyaProvider.swift new file mode 100644 index 00000000..06c6051f --- /dev/null +++ b/Santander/Santander/Library/Extensions/MoyaProvider.swift @@ -0,0 +1,28 @@ +// +// MoyaProvider.swift +// Santander +// +// Created by Orlando Amorim on 17/08/19. +// + +import Moya + +extension MoyaProvider { + @discardableResult + func request(_ target: Target, decodeType: T.Type, result: @escaping (Result) -> Void) -> Cancellable? { + return request(target) { requestResult in + switch requestResult { + case .success(let response): + let decoder = JSONDecoder() + do { + let decodableObject = try decoder.decode(T.self, from: response.data) + result(.success(decodableObject)) + } catch let error { + result(.failure(error)) + } + case .failure(let error): + result(.failure(error)) + } + } + } +} diff --git a/Santander/Santander/Library/Extensions/String.swift b/Santander/Santander/Library/Extensions/String.swift new file mode 100644 index 00000000..b8ce853b --- /dev/null +++ b/Santander/Santander/Library/Extensions/String.swift @@ -0,0 +1,16 @@ +// +// String.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation +import Eureka + +extension String: SectionHeaderFooterRenderable { + public func viewForItem() -> HeaderFooterViewRepresentable { + return HeaderFooterView(stringLiteral: self) + } +} diff --git a/Santander/Santander/Library/Extensions/UIColor.swift b/Santander/Santander/Library/Extensions/UIColor.swift new file mode 100644 index 00000000..ac071f38 --- /dev/null +++ b/Santander/Santander/Library/Extensions/UIColor.swift @@ -0,0 +1,45 @@ +// +// UIColor.swift +// Santander +// +// Created by Orlando Amorim on 10/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit + +extension UIColor { + + struct Santander { + static let monza = UIColor(hexString: "#DA0101") + static let guardsmanRed = UIColor(hexString: "#C80404") + static let apricot = UIColor(hexString: "#EB7676") + static let silverChalice = UIColor(hexString: "#ACACAC") + static let havelockBlue = UIColor(hexString: "#6DA1DF") + static let gallery = UIColor(hexString: "#EFEEED") + static let torchRed = UIColor(hexString: "#FF1F1F") + static let sushi = UIColor(hexString: "#65BE30") + static let gray = UIColor(hexString: "#7E7E7E") + static let mineShaft = UIColor(hexString: "#333333") + static let pastelGreen = UIColor(hexString: "#74DA61") + static let emerald = UIColor(hexString: "#4AC16C") + static let lightningYellow = UIColor(hexString: "#FFC011") + static let burningOrange = UIColor(hexString: "#FF742C") + static let redOrange = UIColor(hexString: "#FF3634") + static let cloudy = UIColor(hexString: "#AFA9A3") + } + + convenience init(hexString: String, alpha: CGFloat? = 1.0) { + var hexInt: UInt32 = 0 + let scanner = Scanner(string: hexString) + scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") + scanner.scanHexInt32(&hexInt) + + let red = CGFloat((hexInt & 0xff0000) >> 16) / 255.0 + let green = CGFloat((hexInt & 0xff00) >> 8) / 255.0 + let blue = CGFloat((hexInt & 0xff) >> 0) / 255.0 + let alpha = alpha! + + self.init(red: red, green: green, blue: blue, alpha: alpha) + } +} diff --git a/Santander/Santander/Library/Extensions/UIFont.swift b/Santander/Santander/Library/Extensions/UIFont.swift new file mode 100644 index 00000000..c99e642c --- /dev/null +++ b/Santander/Santander/Library/Extensions/UIFont.swift @@ -0,0 +1,26 @@ +// +// UIFont.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit.UIFont + +extension UIFont { + + enum SantanderFontType { + case medium + case regular + case light + } + + static func santander(type: SantanderFontType = .regular, with size: CGFloat) -> UIFont { + switch type { + case .medium: return UIFont(name: "DINPro-Medium", size: size)! + case .regular: return UIFont(name: "DINPro-Regular", size: size)! + case .light: return UIFont(name: "DINPro-Light", size: size)! + } + } +} diff --git a/Santander/Santander/Library/Extensions/UIStackView.swift b/Santander/Santander/Library/Extensions/UIStackView.swift new file mode 100644 index 00000000..662ba337 --- /dev/null +++ b/Santander/Santander/Library/Extensions/UIStackView.swift @@ -0,0 +1,18 @@ +// +// UIStackView.swift +// Santander +// +// Created by Orlando Amorim on 10/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit.UIStackView + +extension UIStackView { + func removeAllArrangedSubviews() { + arrangedSubviews.forEach({ + removeArrangedSubview($0) + $0.removeFromSuperview() + }) + } +} diff --git a/Santander/Santander/Library/Extensions/UIView.swift b/Santander/Santander/Library/Extensions/UIView.swift new file mode 100644 index 00000000..53afae0e --- /dev/null +++ b/Santander/Santander/Library/Extensions/UIView.swift @@ -0,0 +1,23 @@ +// +// UIView.swift +// Santander +// +// Created by Orlando Amorim on 14/08/19. +// + +import UIKit + +extension UIView { + func round(corners: UIRectCorner, radius: CGFloat) { + let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + self.layer.mask = mask + } + + @available(iOS 11.0, *) + func round(corners: CACornerMask, radius: CGFloat) { + layer.maskedCorners = corners + layer.cornerRadius = radius + } +} diff --git a/Santander/Santander/Library/Extensions/ViewController.swift b/Santander/Santander/Library/Extensions/ViewController.swift new file mode 100644 index 00000000..c6ecec41 --- /dev/null +++ b/Santander/Santander/Library/Extensions/ViewController.swift @@ -0,0 +1,18 @@ +// +// ViewController.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit + +extension UIViewController { + func showAlert(title: String, message: String?) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alert.addAction(okAction) + present(alert, animated: true, completion: nil) + } +} diff --git a/Santander/Santander/Library/Protocols/SectionHeaderFooterRenderable.swift b/Santander/Santander/Library/Protocols/SectionHeaderFooterRenderable.swift new file mode 100644 index 00000000..cbb78db5 --- /dev/null +++ b/Santander/Santander/Library/Protocols/SectionHeaderFooterRenderable.swift @@ -0,0 +1,19 @@ +// +// SectionHeaderFooterRenderable.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Eureka + +// MARK: Section Header Footer Renderable Protocol + +/** + * Protocol used to set headers and footers to sections. + * Can be set with a view or a String + */ +public protocol SectionHeaderFooterRenderable { + func viewForItem() -> HeaderFooterViewRepresentable +} diff --git a/Santander/Santander/Library/Protocols/Service.swift b/Santander/Santander/Library/Protocols/Service.swift new file mode 100644 index 00000000..a98234ef --- /dev/null +++ b/Santander/Santander/Library/Protocols/Service.swift @@ -0,0 +1,13 @@ +// +// Service.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation + +protocol Service { + typealias ResultCompletion = (_ result: Result) -> () +} diff --git a/Santander/Santander/Models/Contact/Contact.swift b/Santander/Santander/Models/Contact/Contact.swift new file mode 100644 index 00000000..d3241c00 --- /dev/null +++ b/Santander/Santander/Models/Contact/Contact.swift @@ -0,0 +1,18 @@ +// +// Contact.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation + +struct ContactForm: Decodable { + + let cells: [FormCell] + + private enum CodingKeys: String, CodingKey { + case cells + } +} diff --git a/Santander/Santander/Models/Contact/FieldType.swift b/Santander/Santander/Models/Contact/FieldType.swift new file mode 100644 index 00000000..8ec69bea --- /dev/null +++ b/Santander/Santander/Models/Contact/FieldType.swift @@ -0,0 +1,57 @@ +// +// FieldType.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation + +enum FieldType: Int, Decodable { + case text = 1 + case phone = 2 + case email = 3 + + enum FieldTypeError: Error { + case decoding(String) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let typeInt = try? container.decode(Int.self), let type = FieldType(rawValue: typeInt) { + self = type + return + } + + if let typeString = try? container.decode(String.self), let type = try? FieldType(type: typeString) { + self = type + return + } + + throw FieldTypeError.decoding("The value \(container.codingPath) does not match with any type") + } + + var tag: String { + switch self { + case .text: + return "text" + case .phone: + return "phone" + case .email: + return "email" + } + } +} + +extension FieldType { + init(type: String) throws { + switch type { + case "telnumber": + self = .phone + default: + throw FieldTypeError.decoding("The value \(type) does not match with any type") + } + } +} diff --git a/Santander/Santander/Models/Contact/FormCell.swift b/Santander/Santander/Models/Contact/FormCell.swift new file mode 100644 index 00000000..1ddbe96e --- /dev/null +++ b/Santander/Santander/Models/Contact/FormCell.swift @@ -0,0 +1,47 @@ +// +// Cell.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation +import UIKit + +struct FormCell: Decodable { + let id: Int + let type: FormCellType + let message: String + let fieldType: FieldType? + var isHidden: Bool + let topSpacing: CGFloat + let fieldToPresent: Int? + let isRequired: Bool + + private enum CodingKeys: String, CodingKey { + case id + case type + case message + case fieldType = "typefield" + case isHidden = "hidden" + case topSpacing + case fieldToPresent = "show" + case isRequired = "required" + } + + var tag: String { + switch type { + case .field: + return fieldType != nil ? "field-\(fieldType!.tag)" : "field" + case .text: + return "text" + case .image: + return "image" + case .checkbox: + return "checkbox" + case .send: + return "send" + } + } +} diff --git a/Santander/Santander/Models/Contact/FormCellType.swift b/Santander/Santander/Models/Contact/FormCellType.swift new file mode 100644 index 00000000..4737e8d2 --- /dev/null +++ b/Santander/Santander/Models/Contact/FormCellType.swift @@ -0,0 +1,17 @@ +// +// CellType.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation + +enum FormCellType: Int, Decodable { + case field = 1 + case text = 2 + case image = 3 + case checkbox = 4 + case send = 5 +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+DownInfo.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+DownInfo.swift new file mode 100644 index 00000000..5b3ea7d3 --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+DownInfo.swift @@ -0,0 +1,15 @@ +// +// Funds+DownInfo.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import Foundation + +extension FundsScreen { + struct DownInfo: Decodable { + let name: String + let data: String? + } +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Info.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Info.swift new file mode 100644 index 00000000..fe2bdf33 --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Info.swift @@ -0,0 +1,15 @@ +// +// Funds+Info.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import Foundation + +extension FundsScreen { + struct Info: Decodable { + let name: String + let data: String + } +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo+Percentages.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo+Percentages.swift new file mode 100644 index 00000000..9e064d99 --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo+Percentages.swift @@ -0,0 +1,20 @@ +// +// FundsMoreInfoPercentages.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import Foundation + +extension FundsScreen.MoreInfo { + struct Percentages: Decodable { + let fund: Double + let cdi: Double + + private enum CodingKeys: String, CodingKey { + case fund + case cdi = "CDI" + } + } +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo.swift new file mode 100644 index 00000000..7c30cff5 --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+MoreInfo.swift @@ -0,0 +1,44 @@ +// +// FundPercentages.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import Foundation + +extension FundsScreen { + struct MoreInfo: Decodable { + let month: Percentages + let year: Percentages + let twelveMonths: Percentages + + enum CodingKeys: String, CodingKey, CaseIterable { + case month + case year + case twelveMonths = "12months" + } + + static func title(for key: CodingKeys) -> String { + switch key { + case .month: + return "No mês" + case .year: + return "No Ano" + case .twelveMonths: + return "12 meses" + } + } + + func value(for key: CodingKeys) -> Percentages { + switch key { + case .month: + return month + case .year: + return year + case .twelveMonths: + return twelveMonths + } + } + } +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Risk.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Risk.swift new file mode 100644 index 00000000..44590f3f --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen+Risk.swift @@ -0,0 +1,33 @@ +// +// FundsRisk.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit + +extension FundsScreen { + enum Risk: Int, CaseIterable, Decodable { + case one = 1 + case two = 2 + case three = 3 + case four = 4 + case five = 5 + + var color: UIColor { + switch self { + case .one: + return UIColor.Santander.pastelGreen + case .two: + return UIColor.Santander.emerald + case .three: + return UIColor.Santander.lightningYellow + case .four: + return UIColor.Santander.burningOrange + case .five: + return UIColor.Santander.redOrange + } + } + } +} diff --git a/Santander/Santander/Models/Investment/FundsScreen/FundsScreen.swift b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen.swift new file mode 100644 index 00000000..45127b6d --- /dev/null +++ b/Santander/Santander/Models/Investment/FundsScreen/FundsScreen.swift @@ -0,0 +1,21 @@ +// +// FundScreen.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import Foundation + +struct FundsScreen: Decodable { + let title: String + let fundName: String + let whatIs: String + let definition: String + let riskTitle: String + let risk: Risk + let infoTitle: String + let moreInfo: MoreInfo + let info: [Info] + let downInfo: [DownInfo] +} diff --git a/Santander/Santander/Networking/Endpoints/ContactTarget.swift b/Santander/Santander/Networking/Endpoints/ContactTarget.swift new file mode 100644 index 00000000..e342a0cf --- /dev/null +++ b/Santander/Santander/Networking/Endpoints/ContactTarget.swift @@ -0,0 +1,49 @@ +// +// ContactTarget.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Moya + +enum ContactTarget { + case getForm +} + +extension ContactTarget: TargetType { + + var baseURL: URL { + return try! ServerRoutes.baseRoute.asURL() + } + + var path: String { + switch self { + case .getForm: + return ServerRoutes.Contact.getForm + } + } + + var method: Moya.Method { + switch self { + case .getForm: + return .get + } + } + + var task: Task { + switch self { + case .getForm: + return .requestPlain + } + } + + var sampleData: Data { + return "".data(using: .utf8)! + } + + var headers: [String : String]? { + return nil + } +} diff --git a/Santander/Santander/Networking/Endpoints/FundsTarget.swift b/Santander/Santander/Networking/Endpoints/FundsTarget.swift new file mode 100644 index 00000000..8b91d21b --- /dev/null +++ b/Santander/Santander/Networking/Endpoints/FundsTarget.swift @@ -0,0 +1,49 @@ +// +// FundsTarget.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Moya + +enum InvestmentTarget { + case getFunds +} + +extension InvestmentTarget: TargetType { + + var baseURL: URL { + return try! ServerRoutes.baseRoute.asURL() + } + + var path: String { + switch self { + case .getFunds: + return ServerRoutes.Investment.getFunds + } + } + + var method: Moya.Method { + switch self { + case .getFunds: + return .get + } + } + + var task: Task { + switch self { + case .getFunds: + return .requestPlain + } + } + + var sampleData: Data { + return "".data(using: .utf8)! + } + + var headers: [String : String]? { + return nil + } +} diff --git a/Santander/Santander/Networking/Requests/Contact/ContactFormDataRequest.swift b/Santander/Santander/Networking/Requests/Contact/ContactFormDataRequest.swift new file mode 100644 index 00000000..601993c0 --- /dev/null +++ b/Santander/Santander/Networking/Requests/Contact/ContactFormDataRequest.swift @@ -0,0 +1,15 @@ +// +// ContactFormDataRequest.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation + +struct ContactFormDataRequest { + var name: String? + var email: String? + var phone: String? +} diff --git a/Santander/Santander/Networking/Routes/ServerRoutes.swift b/Santander/Santander/Networking/Routes/ServerRoutes.swift new file mode 100644 index 00000000..8668580a --- /dev/null +++ b/Santander/Santander/Networking/Routes/ServerRoutes.swift @@ -0,0 +1,24 @@ +// +// ServerRoutes.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit + +struct ServerRoutes { + + static let baseRoute = "https://floating-mountain-50292.herokuapp.com/" + + // Contact + struct Contact { + static let getForm = "cells.json" + } + + // Investment + struct Investment { + static let getFunds = "fund.json" + } +} diff --git a/Santander/Santander/Networking/Services/Contact/ContactAPI.swift b/Santander/Santander/Networking/Services/Contact/ContactAPI.swift new file mode 100644 index 00000000..bf780d87 --- /dev/null +++ b/Santander/Santander/Networking/Services/Contact/ContactAPI.swift @@ -0,0 +1,28 @@ +// +// ContactAPI.swift +// Santander +// +// Created by Orlando Amorim on 12/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import Moya + +class ContactAPI: ContactStoreProtocol { + + private let provider = MoyaProvider() + var cancelable: Cancellable? + + func getForm(result: @escaping (Result) -> Void) { + cancelable?.cancel() + cancelable = provider.request(.getForm, decodeType: ContactForm.self) { responseResult in + switch responseResult { + case .success(let contactForm): + result(.success(contactForm)) + case .failure(let error): + result(.failure(error)) + } + } + } +} diff --git a/Santander/Santander/Networking/Services/Investment/InvestmentAPI.swift b/Santander/Santander/Networking/Services/Investment/InvestmentAPI.swift new file mode 100644 index 00000000..2e5bcce0 --- /dev/null +++ b/Santander/Santander/Networking/Services/Investment/InvestmentAPI.swift @@ -0,0 +1,27 @@ +// +// InvestmentAPI.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Moya + +class InvestmentAPI: InvestmentStoreProtocol { + + private let provider = MoyaProvider() + var cancelable: Cancellable? + + func getFunds(result: @escaping (Result) -> Void) { + cancelable?.cancel() + cancelable = provider.request(.getFunds, decodeType: Investment.Funds.Response.self) { responseResult in + switch responseResult { + case .success(let funds): + result(.success(funds)) + case .failure(let error): + result(.failure(error)) + } + } + } +} diff --git a/Santander/Santander/Scenes/Contact/ContactInteractor.swift b/Santander/Santander/Scenes/Contact/ContactInteractor.swift new file mode 100644 index 00000000..6fb3d9ae --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactInteractor.swift @@ -0,0 +1,53 @@ +// +// ContactInteractor.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +protocol ContactBusinessLogic { + func getForm() + func sendForm(data: Contact.Form.Request) +} + +protocol ContactDataStore { + +} + +class ContactInteractor: ContactBusinessLogic, ContactDataStore { + + var presenter: ContactPresentationLogic? + var worker: ContactWorker = ContactWorker(contactStore: ContactAPI()) + + // MARK: Get Form + func getForm() { + worker.getForm { [weak self] result in + guard let self = self, let presenter = self.presenter else { + return + } + switch result { + case .success(let contactForm): + presenter.presentForm(contactForm) + case .failure(let error): + presenter.presentError(error) + } + } + } + + // MARK: Send Form + func sendForm(data: Contact.Form.Request) { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + guard let self = self, let presenter = self.presenter else { + return + } + presenter.presentSuccess() + } + } +} diff --git a/Santander/Santander/Scenes/Contact/ContactModels.swift b/Santander/Santander/Scenes/Contact/ContactModels.swift new file mode 100644 index 00000000..b984f735 --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactModels.swift @@ -0,0 +1,28 @@ +// +// ContactModels.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +enum Contact { + // MARK: Use cases + enum Form { + struct Request { + var sendFormData: ContactFormDataRequest + } + struct Response { + var result: Result + } + struct ViewModel { + + } + } +} diff --git a/Santander/Santander/Scenes/Contact/ContactPresenter.swift b/Santander/Santander/Scenes/Contact/ContactPresenter.swift new file mode 100644 index 00000000..d7b3a8ba --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactPresenter.swift @@ -0,0 +1,39 @@ +// +// ContactPresenter.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +protocol ContactPresentationLogic { + func presentForm(_ form: ContactForm) + func presentError(_ error: Error) + func presentSuccess() +} + +class ContactPresenter: ContactPresentationLogic { + + weak var viewController: ContactDisplayLogic? + + // MARK: Present Form + func presentForm(_ form: ContactForm) { + viewController?.displayForm(form) + } + + // MARK: Present Error + func presentError(_ error: Error) { + viewController?.displayError(error.localizedDescription) + } + + // MARK: Present Success + func presentSuccess() { + viewController?.displaySuccess() + } +} diff --git a/Santander/Santander/Scenes/Contact/ContactRouter.swift b/Santander/Santander/Scenes/Contact/ContactRouter.swift new file mode 100644 index 00000000..3b304d77 --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactRouter.swift @@ -0,0 +1,41 @@ +// +// ContactRouter.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +@objc protocol ContactRoutingLogic { + //func routeToSomewhere(segue: UIStoryboardSegue?) +} + +protocol ContactDataPassing { + var dataStore: ContactDataStore? { get } +} + +class ContactRouter: NSObject, ContactRoutingLogic, ContactDataPassing { + weak var viewController: ContactViewController? + var dataStore: ContactDataStore? + + // MARK: Routing + + // MARK: Navigation +// +// func navigateToSomewhere(source: ContactViewController, destination: SomewhereViewController) { +// source.show(destination, sender: nil) +// } +// + // MARK: Passing data + + //func passDataToSomewhere(source: ContactDataStore, destination: inout SomewhereDataStore) + //{ + // destination.name = source.name + //} +} diff --git a/Santander/Santander/Scenes/Contact/ContactViewController.swift b/Santander/Santander/Scenes/Contact/ContactViewController.swift new file mode 100644 index 00000000..9ba5852a --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactViewController.swift @@ -0,0 +1,338 @@ +// +// ContactViewController.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit +import Eureka +import JGProgressHUD + +protocol ContactDisplayLogic: class { + func displayForm(_ form: ContactForm) + func displayError(_ error: String) + func displaySuccess() +} + +class ContactViewController: SantanderBaseFormViewController { + + var interactor: ContactBusinessLogic? + var router: (NSObjectProtocol & ContactRoutingLogic & ContactDataPassing)? + var sendFormRequest = ContactFormDataRequest(name: nil, email: nil, phone: nil) + + private let progressHud: JGProgressHUD = { + let progressHud = JGProgressHUD(style: .light) + progressHud.textLabel.text = "Carregando..." + return progressHud + }() + + private var sendButton: SantanderButton = { + var button = SantanderButton() + button.setTitle("Enviar", for: .normal) + return button + }() + + private var successView: SuccessView = { + var view = SuccessView() + view.alpha = 0.0 + view.isUserInteractionEnabled = true + return view + }() + + private var isResetingForm: Bool = false + + // MARK: Object lifecycle + override init() { + super.init() + setup() + } + + // MARK: Setup + private func setup() { + let viewController = self + let interactor = ContactInteractor() + let presenter = ContactPresenter() + let router = ContactRouter() + viewController.interactor = interactor + viewController.router = router + interactor.presenter = presenter + presenter.viewController = viewController + router.viewController = viewController + router.dataStore = interactor + } + + // MARK: View lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupView() + getForm() + } + + // MARK: Setup View + private func setupView() { + tableView.keyboardDismissMode = .interactive + addSuccssView() + setupSuccessView() + } + + private func addSuccssView() { + view.addSubview(successView) + successView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + private func setupSuccessView() { + successView.onTap { [weak self] in + guard let self = self else { + return + } + self.dismissSuccessView() + } + } + + func getForm() { + progressHud.show(in: view) + interactor?.getForm() + } + + func sendForm() { + guard form.validate(includeHidden: false, includeDisabled: false).isEmpty else { + return + } + tableView.endEditing(true) + progressHud.show(in: view) + let requestData = Contact.Form.Request(sendFormData: sendFormRequest) + interactor?.sendForm(data: requestData) + } +} + +extension ContactViewController: ContactDisplayLogic { + + func displayForm(_ form: ContactForm) { + progressHud.dismiss() + self.form.removeAll() + let rows = form.cells.compactMap({ makeRow(with: $0) }) + makeSection(rows: rows) + } + + func displayError(_ error: String) { + progressHud.dismiss() + showAlert(title: "Atenção", message: error) + } + + func displaySuccess() { + isResetingForm = true + form.rows.forEach({ $0.baseValue = nil }) + form.rows.forEach({ $0.reload() }) + form.cleanValidationErrors() + isResetingForm = false + + progressHud.dismiss() + presentSuccessView() + } +} + +// MARK: Factory +extension ContactViewController { + + private func makeRow(with cell: FormCell) -> BaseRow? { + switch cell.type { + case .field: + if let fieldType = cell.fieldType { + switch fieldType { + case .text: + return makeNameRow(tag: cell.id, title: cell.message, isHidden: cell.isHidden, isRequired: cell.isRequired, topSpacing: cell.topSpacing) + case .phone: + return makePhoneRow(tag: cell.id, title: cell.message, isHidden: cell.isHidden, isRequired: cell.isRequired, topSpacing: cell.topSpacing) + case .email: + return makeEmailRow(tag: cell.id, title: cell.message, isHidden: cell.isHidden, isRequired: cell.isRequired, topSpacing: cell.topSpacing) + } + } + case .text: + return nil + case .image: + return nil + case .checkbox: + return makeCheckboxRow(tag: cell.id, title: cell.message, isHidden: cell.isHidden, isRequired: cell.isRequired, topSpacing: cell.topSpacing, fieldToPresent: cell.fieldToPresent) + case .send: + return makeSendButtonRow(tag: cell.id, title: cell.message, isHidden: cell.isHidden, isRequired: cell.isRequired, topSpacing: cell.topSpacing) + } + return nil + } + + private func makeNameRow(tag: Int, title: String, isHidden: Bool, isRequired: Bool, topSpacing: CGFloat) -> BaseRow { + let row = TextFloatLabelRow(tag: String(tag)) + row.title = title + row.hidden = Condition(booleanLiteral: isHidden) + row.cell.topSpacing = topSpacing + if isRequired { + row.add(rule: RuleRequired()) + } + row.validationOptions = .validatesAlways + + row.onRowValidationChanged { [weak self] cell, row in + guard let self = self, !self.isResetingForm else { + return + } + cell.floatLabelTextField.borderColor = row.isValid ? UIColor.Santander.sushi : UIColor.Santander.torchRed + } + + row.onChange { [weak self] row in + guard let self = self else { + return + } + self.sendFormRequest.name = row.value + + if row.value == nil { + row.cell.floatLabelTextField.borderColor = UIColor.Santander.gallery + } + } + return row + } + + private func makeEmailRow(tag: Int, title: String, isHidden: Bool, isRequired: Bool, topSpacing: CGFloat) -> BaseRow { + let row = EmailFloatLabelRow(tag: String(tag)) + row.title = title + row.hidden = Condition(booleanLiteral: isHidden) + row.cell.topSpacing = topSpacing + if isRequired { + row.add(rule: RuleRequired()) + } + row.add(rule: RuleEmail()) + row.validationOptions = .validatesAlways + + row.onRowValidationChanged { [weak self] cell, row in + guard let self = self, !self.isResetingForm else { + return + } + cell.floatLabelTextField.borderColor = row.isValid ? UIColor.Santander.sushi : UIColor.Santander.torchRed + } + + + row.onChange { [weak self] row in + guard let self = self else { + return + } + self.sendFormRequest.email = row.value + + if row.value == nil { + row.cell.floatLabelTextField.borderColor = UIColor.Santander.gallery + } + } + return row + } + + private func makePhoneRow(tag: Int, title: String, isHidden: Bool, isRequired: Bool, topSpacing: CGFloat) -> BaseRow { + let row = PhoneFloatLabelRow(tag: String(tag)) + row.title = title + row.hidden = Condition(booleanLiteral: isHidden) + row.cell.topSpacing = topSpacing + if isRequired { + row.add(rule: RuleRequired()) + } + row.add(rule: RulePhoneNumber()) + row.validationOptions = .validatesAlways + + row.onRowValidationChanged { [weak self] cell, row in + guard let self = self, !self.isResetingForm else { + return + } + cell.floatLabelTextField.borderColor = row.isValid ? UIColor.Santander.sushi : UIColor.Santander.torchRed + } + + row.onChange { [weak self] row in + guard let self = self else { + return + } + self.sendFormRequest.phone = row.value + + if row.value == nil { + row.cell.floatLabelTextField.borderColor = UIColor.Santander.gallery + } + } + return row + } + + private func makeCheckboxRow(tag: Int, title: String, isHidden: Bool, isRequired: Bool, topSpacing: CGFloat, fieldToPresent: Int? = nil) -> BaseRow { + let row = ViewRow(tag: String(tag)) + var state: CheckmarkButton.State = .unselected + if let fieldToPresent = fieldToPresent, let row = self.form.rowBy(tag: String(fieldToPresent)) { + state = row.isHidden ? .unselected : .selected + } + + let checkmarkButton = CheckmarkButton(text: title, state: state, frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 21.0)) + checkmarkButton.onTap { [weak self] state in + guard let fieldToPresent = fieldToPresent, let self = self, let row = self.form.rowBy(tag: String(fieldToPresent)) else { + return + } + let isHidden = state == .unselected + let condition = Condition(booleanLiteral: isHidden) + row.hidden = condition + row.validationOptions = .validatesOnDemand + row.baseValue = nil + row.evaluateHidden() + row.validationOptions = .validatesAlways + } + + row.cellSetup { cell, row in + cell.view = checkmarkButton + + cell.viewTopMargin = topSpacing + cell.viewLeftMargin = 40.0 + cell.viewRightMargin = 40.0 + cell.viewBottomMargin = 0.0 + } + + return row + } + + private func makeSendButtonRow(tag: Int, title: String, isHidden: Bool, isRequired: Bool, topSpacing: CGFloat) -> BaseRow { + let row = ViewRow(tag: String(tag)) + row.value = "Enviar" + + let sendButton = SantanderButton(title: "Enviar", frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 50.0)) + + row.cellSetup { cell, row in + cell.view = sendButton + cell.viewTopMargin = topSpacing + cell.viewLeftMargin = 30.0 + cell.viewRightMargin = 30.0 + cell.viewBottomMargin = 0.0 + } + + sendButton.onTap { [weak self] button in + guard let self = self else { + return + } + row.section?.form?.validate() + self.sendForm() + } + + return row + } +} + +extension ContactViewController { + private func presentSuccessView() { + UIView.animate(withDuration: 0.2, animations: { [weak self] in + guard let self = self else { return } + self.tableView.alpha = 0.0 + self.successView.alpha = 1.0 + }) + } + + private func dismissSuccessView() { + UIView.animate(withDuration: 0.2, animations: { [weak self] in + guard let self = self else { return } + self.tableView.alpha = 1.0 + self.successView.alpha = 0.0 + }) + } +} diff --git a/Santander/Santander/Scenes/Contact/ContactWorker.swift b/Santander/Santander/Scenes/Contact/ContactWorker.swift new file mode 100644 index 00000000..bb1ff688 --- /dev/null +++ b/Santander/Santander/Scenes/Contact/ContactWorker.swift @@ -0,0 +1,32 @@ +// +// ContactWorker.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright (c) 2019 Santander. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit +import Moya + +protocol ContactStoreProtocol { + func getForm(result: @escaping (Result) -> Void) +} + +class ContactWorker { + + private let provider = MoyaProvider() + var contactStore: ContactStoreProtocol + + init(contactStore: ContactStoreProtocol) { + self.contactStore = contactStore + } + + func getForm(result: @escaping (Result) -> Void) { + contactStore.getForm(result: result) + } +} diff --git a/Santander/Santander/Scenes/Container/ContainerViewController.swift b/Santander/Santander/Scenes/Container/ContainerViewController.swift new file mode 100644 index 00000000..87edee16 --- /dev/null +++ b/Santander/Santander/Scenes/Container/ContainerViewController.swift @@ -0,0 +1,139 @@ +// +// ContainerViewController.swift +// Santander +// +// Created by Orlando Amorim on 10/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import SnapKit + +class ContainerViewController: UIViewController { + + //MARK: - Views + private var containedSegmentedView: SegmentedControl = { + var segmentedControl = SegmentedControl() + segmentedControl.set(buttons: [("Investimento", true), ("Contato", false)]) + return segmentedControl + }() + + private lazy var investmentViewController: InvestmentViewController = { + let viewController = InvestmentViewController() + return viewController + }() + + private lazy var contactViewController: ContactViewController = { + let viewController = ContactViewController() + return viewController + }() + + private lazy var shareButton: UIBarButtonItem = { + let button = UIBarButtonItem() + button.image = UIImage(named: "share-icon") + return button + }() + + enum ContainerType: Int { + case funds = 0 + case contact = 1 + + var title: String { + switch self { + case .funds: + return "Investimento" + case .contact: + return "Contato" + } + } + } + + //MARK: - Vars + private var selectedType: ContainerType = .funds + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + setupSelectionHandler() + select(type: .funds) + } + + private func setupView() { + view.backgroundColor = .white + addContainedSegmentedView() + setupNavigationController() + } + + private func setupNavigationController() { + guard let navigationController = navigationController else { + return + } + navigationController.navigationBar.titleTextAttributes = + [.foregroundColor: UIColor.Santander.mineShaft, + .font: UIFont.santander(type: .medium, with: 16.0)] + if #available(iOS 11.0, *) { + navigationController.navigationBar.prefersLargeTitles = false + } + navigationController.navigationBar.backgroundColor = .black + navigationController.navigationBar.shadowImage = UIImage() + navigationController.navigationBar.isTranslucent = false + navigationController.view.backgroundColor = .white + navigationController.navigationBar.tintColor = UIColor.Santander.monza + } + + private func addContainedSegmentedView() { + view.addSubview(containedSegmentedView) + containedSegmentedView.snp.makeConstraints { make in + make.height.equalTo(57.0) + make.leading.trailing.equalToSuperview() + if #available(iOS 11.0, *) { + make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) + } else { + make.bottom.equalToSuperview() + } + } + } + + private func setupSelectionHandler() { + containedSegmentedView.onTap { [weak self] index in + guard let self = self, let type = ContainerType(rawValue: index) else { + return + } + self.select(type: type) + } + } + + private func select(type: ContainerType) { + let viewControllers = [investmentViewController, contactViewController] + removeChildVc(viewControllers[type.rawValue]) + addChildVc(viewControllers[type.rawValue]) + title = type.title + selectedType = type + managerNavigationButton(type: type) + } + + private func addChildVc(_ child: UIViewController) { + addChild(child) + view.insertSubview(child.view, belowSubview: containedSegmentedView) + child.view.snp.makeConstraints { (make) in + make.top.leading.trailing.equalToSuperview() + make.bottom.equalTo(containedSegmentedView.snp.top) + } + child.didMove(toParent: self) + } + + private func removeChildVc(_ child: UIViewController) { + child.willMove(toParent: self) + child.removeFromParent() + child.view.removeFromSuperview() + } + + private func managerNavigationButton(type: ContainerType) { + switch type { + case .funds: + navigationItem.setRightBarButtonItems([shareButton], animated: false) + case .contact: + navigationItem.setRightBarButtonItems([], animated: false) + } + } +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentInteractor.swift b/Santander/Santander/Scenes/Investment/InvestmentInteractor.swift new file mode 100644 index 00000000..18414cea --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentInteractor.swift @@ -0,0 +1,45 @@ +// +// InvestmentInteractor.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +protocol InvestmentBusinessLogic { + func getFunds() +} + +protocol InvestmentDataStore { + + //var name: String { get set } +} + +class InvestmentInteractor: InvestmentBusinessLogic, InvestmentDataStore { + + var presenter: InvestmentPresentationLogic? + var worker: InvestmentWorker = InvestmentWorker(investmentStore: InvestmentAPI()) + + // MARK: Do something + + // MARK: - Get Funds + func getFunds() { + worker.getFunds { [weak self] result in + guard let self = self, let presenter = self.presenter else { + return + } + switch result { + case .success(let response): + presenter.presentScreen(response: response) + case .failure(let error): + presenter.presentError(error) + } + } + } +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentModels.swift b/Santander/Santander/Scenes/Investment/InvestmentModels.swift new file mode 100644 index 00000000..8ae5bfa7 --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentModels.swift @@ -0,0 +1,31 @@ +// +// InvestmentModels.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +enum Investment { + + // MARK: Use cases + enum Funds { + struct Request { + + } + + struct Response: Decodable { + let screen: FundsScreen + } + + struct ViewModel { + let screen: FundsScreen + } + } +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentPresenter.swift b/Santander/Santander/Scenes/Investment/InvestmentPresenter.swift new file mode 100644 index 00000000..8ebdd119 --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentPresenter.swift @@ -0,0 +1,34 @@ +// +// InvestmentPresenter.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +protocol InvestmentPresentationLogic { + func presentScreen(response: Investment.Funds.Response) + func presentError(_ error: Error) +} + +class InvestmentPresenter: InvestmentPresentationLogic { + + weak var viewController: InvestmentDisplayLogic? + + // MARK: Present Screen + func presentScreen(response: Investment.Funds.Response) { + let viewModel = Investment.Funds.ViewModel(screen: response.screen) + viewController?.displayScreen(viewModel: viewModel) + } + + // MARK: Present Error + func presentError(_ error: Error) { + viewController?.displayError(error.localizedDescription) + } +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentRouter.swift b/Santander/Santander/Scenes/Investment/InvestmentRouter.swift new file mode 100644 index 00000000..4fb2ded4 --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentRouter.swift @@ -0,0 +1,58 @@ +// +// InvestmentRouter.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit + +@objc protocol InvestmentRoutingLogic { + //func routeToSomewhere(segue: UIStoryboardSegue?) +} + +protocol InvestmentDataPassing { + var dataStore: InvestmentDataStore? { get } +} + +class InvestmentRouter: NSObject, InvestmentRoutingLogic, InvestmentDataPassing { + + weak var viewController: InvestmentViewController? + var dataStore: InvestmentDataStore? + + // MARK: Routing + + //func routeToSomewhere(segue: UIStoryboardSegue?) + //{ + // if let segue = segue { + // let destinationVC = segue.destination as! SomewhereViewController + // var destinationDS = destinationVC.router!.dataStore! + // passDataToSomewhere(source: dataStore!, destination: &destinationDS) + // } else { + // let storyboard = UIStoryboard(name: "Main", bundle: nil) + // let destinationVC = storyboard.instantiateViewController(withIdentifier: "SomewhereViewController") as! SomewhereViewController + // var destinationDS = destinationVC.router!.dataStore! + // passDataToSomewhere(source: dataStore!, destination: &destinationDS) + // navigateToSomewhere(source: viewController!, destination: destinationVC) + // } + //} + + // MARK: Navigation + + //func navigateToSomewhere(source: InvestmentViewController, destination: SomewhereViewController) + //{ + // source.show(destination, sender: nil) + //} + + // MARK: Passing data + + //func passDataToSomewhere(source: InvestmentDataStore, destination: inout SomewhereDataStore) + //{ + // destination.name = source.name + //} +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentViewController.swift b/Santander/Santander/Scenes/Investment/InvestmentViewController.swift new file mode 100644 index 00000000..d402c9b1 --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentViewController.swift @@ -0,0 +1,263 @@ +// +// InvestmentViewController.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit +import Eureka +import JGProgressHUD +import SafariServices + +protocol InvestmentDisplayLogic: class { + func displayScreen(viewModel: Investment.Funds.ViewModel) + func displayError(_ error: String) +} + +class InvestmentViewController: SantanderBaseFormViewController, InvestmentDisplayLogic { + + private let progressHud: JGProgressHUD = { + let progressHud = JGProgressHUD(style: .light) + progressHud.textLabel.text = "Carregando..." + return progressHud + }() + + var interactor: InvestmentBusinessLogic? + var router: (NSObjectProtocol & InvestmentRoutingLogic & InvestmentDataPassing)? + + // MARK: Object lifecycle + override init() { + super.init() + setup() + } + + // MARK: Setup + private func setup() { + let viewController = self + let interactor = InvestmentInteractor() + let presenter = InvestmentPresenter() + let router = InvestmentRouter() + viewController.interactor = interactor + viewController.router = router + interactor.presenter = presenter + presenter.viewController = viewController + router.viewController = viewController + router.dataStore = interactor + } + + // MARK: View lifecycle + override func viewDidLoad() { + super.viewDidLoad() + getFunds() + } + + func getFunds() { + progressHud.show(in: view) + interactor?.getFunds() + } + + func displayScreen(viewModel: Investment.Funds.ViewModel) { + progressHud.dismiss() + form.removeAll() + var rows: [BaseRow] = [] + rows.append(makeFundNameRow(title: viewModel.screen.title, name: viewModel.screen.fundName)) + rows.append(makeSeparatorArrowLineRow()) + rows.append(makeWhatIsRow(title: viewModel.screen.whatIs, detail: viewModel.screen.definition)) + rows.append(makeRiskViewRow(title: viewModel.screen.riskTitle, risk: viewModel.screen.risk)) + rows.append(makeMoreInfoTitleRow(title: viewModel.screen.infoTitle)) + rows.append(makeMoreInfoPercentagesViewRow(moreInfo: viewModel.screen.moreInfo)) + rows.append(makeSeparatorLineRow()) + + viewModel.screen.info.forEach { info in + rows.append(makeMoreInfoRow(title: info.name, value: info.data)) + } + viewModel.screen.downInfo.forEach { downInfo in + rows.append(makeMoreInfoDownloadRow(title: downInfo.name)) + } + rows.append(makeInvestButtonRow()) + // Make the section + makeSection(rows: rows) + } + + func displayError(_ error: String) { + progressHud.dismiss() + showAlert(title: "Atenção", message: error) + } + +} + +// MARK: Factory +extension InvestmentViewController { + + private func makeFundNameRow(title: String, name: String) -> BaseRow { + let labelRow = TitleSubtitleRow() + labelRow.title = title + labelRow.value = name + + labelRow.cellUpdate { cell, row in + cell.titleLabel.font = UIFont.santander(type: .medium, with: 14.0) + cell.titleLabel.textAlignment = .center + cell.titleLabel.textColor = UIColor.Santander.gray + cell.titleLabel.numberOfLines = 0 + + cell.subtitleLabel.font = UIFont.santander(type: .medium, with: 28.0) + cell.subtitleLabel.textAlignment = .center + cell.subtitleLabel.textColor = UIColor.Santander.mineShaft + cell.subtitleLabel.numberOfLines = 0 + } + return labelRow + } + + private func makeSeparatorArrowLineRow() -> BaseRow { + let row = ViewRow() + let separatorLineImageView = UIImageView(image: UIImage(named: "separator-line-arrow-icon")) + + row.cellSetup { cell, row in + cell.view = separatorLineImageView + cell.viewTopMargin = 21.0 + cell.viewLeftMargin = 30.0 + cell.viewRightMargin = 30.0 + cell.viewBottomMargin = 14.0 + } + return row + } + + private func makeWhatIsRow(title: String, detail: String) -> BaseRow { + let labelRow = TitleSubtitleRow() + labelRow.title = title + labelRow.value = detail + + labelRow.cellUpdate { cell, row in + cell.titleLabel.font = UIFont.santander(type: .medium, with: 16.0) + cell.titleLabel.textAlignment = .center + cell.titleLabel.textColor = UIColor.Santander.gray + cell.titleLabel.numberOfLines = 0 + + cell.subtitleLabel.font = UIFont.santander(type: .light, with: 16.0) + cell.subtitleLabel.textAlignment = .center + cell.subtitleLabel.textColor = UIColor.Santander.gray + cell.subtitleLabel.numberOfLines = 0 + } + return labelRow + } + + private func makeRiskViewRow(title: String, risk: FundsScreen.Risk) -> BaseRow { + let row = ViewRow() + let fundRiskView = FundRiskView(text: title, risk: risk, frame: CGRect(x: 0, y: 0, width: view.frame.width - 38.0 - 38.0, height: 77.0)) + + row.cellSetup { cell, row in + cell.view = fundRiskView + cell.viewTopMargin = 35.0 + cell.viewLeftMargin = 38.0 + cell.viewRightMargin = 38.0 + cell.viewBottomMargin = 46.0 + } + return row + } + + private func makeMoreInfoTitleRow(title: String) -> BaseRow { + let labelRow = LabelRow() + labelRow.title = title + + labelRow.cellSetup { (cell, row) in + guard let textLabel = cell.textLabel else { + return + } + textLabel.snp.makeConstraints({ make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(34.0) + }) + } + + labelRow.cellUpdate { cell, row in + guard let textLabel = cell.textLabel else { + return + } + textLabel.font = UIFont.santander(type: .medium, with: 16.0) + textLabel.textAlignment = .center + textLabel.textColor = UIColor.Santander.gray + textLabel.numberOfLines = 0 + } + return labelRow + } + + private func makeMoreInfoPercentagesViewRow(moreInfo: FundsScreen.MoreInfo) -> BaseRow { + let row = ViewRow() + let moreInfoView = MoreInfoPercentagesView(moreInfo: moreInfo, frame: CGRect(x: 0, y: 0, width: view.frame.width - 30.0 - 30.0, height: 130.0)) + + row.cellSetup { cell, row in + cell.view = moreInfoView + cell.viewTopMargin = 19.0 + cell.viewLeftMargin = 30.0 + cell.viewRightMargin = 30.0 + cell.viewBottomMargin = 0.0 + } + return row + } + + private func makeSeparatorLineRow() -> BaseRow { + let row = ViewRow() + let separatorLineView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width - 30.0 - 30.0, height: 1.0)) + separatorLineView.backgroundColor = UIColor.Santander.cloudy.withAlphaComponent(0.2) + + row.cellSetup { cell, row in + cell.view = separatorLineView + cell.viewTopMargin = 21.0 + cell.viewLeftMargin = 30.0 + cell.viewRightMargin = 30.0 + cell.viewBottomMargin = 17.0 + } + return row + } + + private func makeMoreInfoRow(title: String, value: String) -> BaseRow { + let row = MoreInfoRow() + row.title = title + row.value = value + return row + } + + private func makeMoreInfoDownloadRow(title: String) -> BaseRow { + let row = DownloadInfoRow() + row.title = title + + row.cell.downloadButton.onTap { [weak self] in + guard let self = self, let url = URL(string: "https://www.google.com") else { + return + } + let safariViewController = SFSafariViewController(url: url) + // safariVC.delegate = self + self.present(safariViewController, animated: true, completion: nil) + } + + row.onDonwloadButtonTap { [weak self] in + guard let self = self, let url = URL(string: "https://www.google.com") else { + return + } + let safariViewController = SFSafariViewController(url: url) + self.present(safariViewController, animated: true, completion: nil) + } + + return row + } + + private func makeInvestButtonRow() -> BaseRow { + let row = ViewRow() + let sendButton = SantanderButton(title: "Investir", frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 50.0)) + + row.cellSetup { cell, row in + cell.view = sendButton + cell.viewTopMargin = 45.0 + cell.viewLeftMargin = 30.0 + cell.viewRightMargin = 30.0 + cell.viewBottomMargin = 40.0 + } + return row + } +} diff --git a/Santander/Santander/Scenes/Investment/InvestmentWorker.swift b/Santander/Santander/Scenes/Investment/InvestmentWorker.swift new file mode 100644 index 00000000..bb694458 --- /dev/null +++ b/Santander/Santander/Scenes/Investment/InvestmentWorker.swift @@ -0,0 +1,33 @@ +// +// InvestmentWorker.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// Copyright (c) 2019 ___ORGANIZATIONNAME___. All rights reserved. +// +// This file was generated by the Clean Swift Xcode Templates so +// you can apply clean architecture to your iOS and Mac projects, +// see http://clean-swift.com +// + +import UIKit +import Moya + +protocol InvestmentStoreProtocol { + func getFunds(result: @escaping (Result) -> Void) +} + +class InvestmentWorker { + + private let provider = MoyaProvider() + + var investmentStore: InvestmentStoreProtocol + + init(investmentStore: InvestmentStoreProtocol) { + self.investmentStore = investmentStore + } + + func getFunds(result: @escaping (Result) -> Void) { + investmentStore.getFunds(result: result) + } +} diff --git a/Santander/Santander/Scenes/SantanderBaseFormViewController.swift b/Santander/Santander/Scenes/SantanderBaseFormViewController.swift new file mode 100644 index 00000000..775a9ec3 --- /dev/null +++ b/Santander/Santander/Scenes/SantanderBaseFormViewController.swift @@ -0,0 +1,65 @@ +// +// SantanderBaseFormViewController.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Foundation +import Eureka + +class SantanderBaseFormViewController: FormViewController { + + init() { + super.init(style: .grouped) + } + + @available(*, unavailable) + override init(style: UITableView.Style) { + super.init(style: style) + } + + @available(*, unavailable) + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + private func setupView() { + view.backgroundColor = .white + tableView.backgroundColor = .white + tableView.separatorStyle = .none + } + + // MARK: - Fix animations + override func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .fade } + override func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .fade } + override func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation { return .fade } + override func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } + override func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } + override func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation { return .fade } +} + +extension SantanderBaseFormViewController { + + @discardableResult + func makeSection(with tag: String? = nil, header: SectionHeaderFooterRenderable? = nil, footer: SectionHeaderFooterRenderable? = nil, rows: [BaseRow]) -> Section { + let section = Section() + section.tag = tag + section.header = header?.viewForItem() + section.footer = footer?.viewForItem() + section.append(contentsOf: rows) + form.append(section) + return section + } +} diff --git a/Santander/Santander/Views/Buttons/CheckmarkButton.swift b/Santander/Santander/Views/Buttons/CheckmarkButton.swift new file mode 100644 index 00000000..cf82b55b --- /dev/null +++ b/Santander/Santander/Views/Buttons/CheckmarkButton.swift @@ -0,0 +1,129 @@ +// +// CheckmarkButton.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import Eureka + +class CheckmarkButton: UIView { + // MARK: - Dependencies + enum State { + case selected + case unselected + + var image: UIImage { + switch self { + case .selected: + return UIImage(named: "checkmark-selected-icon")! + case .unselected: + return UIImage(named: "checkmark-unselected-icon")! + } + } + } + + private(set) var state: State + + // MARK: - Views + private var checkmarkImage: UIImageView = { + var imageView = UIImageView() + return imageView + }() + + private var textLabel: UILabel = { + var label = UILabel() + label.font = UIFont.santander(type: .regular, with: 16.0) + label.textColor = UIColor.Santander.silverChalice + return label + }() + + // MARK: - Vars + typealias ButtonBlock = (State) -> Void + private var block: ButtonBlock? + + init(text: String, state: State = .unselected, frame: CGRect = .zero) { + self.state = state + super.init(frame: frame) + textLabel.text = text + setupView() + setupTapGesture() + update(to: state) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + addCheckmarkButton() + addTextLabel() + } + + private func addCheckmarkButton() { + addSubview(checkmarkImage) + checkmarkImage.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview() + make.height.width.equalTo(19.0) + } + } + + private func addTextLabel() { + addSubview(textLabel) + textLabel.snp.makeConstraints { [weak self] make in + guard let self = self else { + return + } + make.centerY.equalTo(self.checkmarkImage.snp.centerY) + make.leading.equalTo(self.checkmarkImage.snp.trailing).inset(-9.0) + make.trailing.equalToSuperview() + } + } + + // MARK: - Actions + + private func setupTapGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(self.onTapAction)) + addGestureRecognizer(tap) + } + + func onTap(_ block: @escaping ButtonBlock) { + self.block = block + } + + @objc private func onTapAction() { + let newState = state == State.unselected ? State.selected : State.unselected + update(to: newState) + block?(newState) + } + + private func update(to state: State) { + self.state = state + checkmarkImage.image = state.image + } +} + +extension CheckmarkButton: SectionHeaderFooterRenderable { + public func viewForItem() -> HeaderFooterViewRepresentable { + self.translatesAutoresizingMaskIntoConstraints = false + var footerView = HeaderFooterView(.class) + footerView.onSetupView = { [weak self] view, _ in + guard let self = self else { return } + view.addSubview(self) + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0), + self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0), + self.topAnchor.constraint(equalTo: view.topAnchor, constant: 47.0), + self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) + ]) + } + footerView.height = { + 47.0 + 21.0 + } + return footerView + } +} diff --git a/Santander/Santander/Views/Buttons/DownloadButton.swift b/Santander/Santander/Views/Buttons/DownloadButton.swift new file mode 100644 index 00000000..b6d8ca8d --- /dev/null +++ b/Santander/Santander/Views/Buttons/DownloadButton.swift @@ -0,0 +1,68 @@ +// +// DownloadButton.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +class DownloadButton: UIButton { + + // MARK: - Vars + typealias ButtonBlock = () -> Void + private var block: ButtonBlock? { + didSet { + self.addTarget(self, action: #selector(onTapAction), for: .touchUpInside) + } + } + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + setupView() + setupTapGesture() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + setupDownloadImageButton() + setupTitleLabel() + setupEdges() + } + + private func setupDownloadImageButton() { + setImage(UIImage(named: "download-icon"), for: .normal) + imageView?.contentMode = .scaleAspectFit + } + + private func setupTitleLabel() { + setTitle("Baixar", for: .normal) + setTitleColor(UIColor.Santander.monza, for: .normal) + titleLabel?.font = UIFont.santander(type: .regular, with: 14.0) + } + + private func setupEdges() { + let insetAmount: CGFloat = 8.0 / 2 + imageEdgeInsets = UIEdgeInsets(top: 0, left: -insetAmount, bottom: 0, right: insetAmount) + titleEdgeInsets = UIEdgeInsets(top: 0, left: insetAmount, bottom: 0, right: -insetAmount) + } + + // MARK: - Actions + private func setupTapGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(self.onTapAction)) + addGestureRecognizer(tap) + } + + func onTap(_ block: @escaping ButtonBlock) { + self.block = block + } + + @objc private func onTapAction() { + block?() + } +} diff --git a/Santander/Santander/Views/Buttons/SantanderButton.swift b/Santander/Santander/Views/Buttons/SantanderButton.swift new file mode 100644 index 00000000..f294f580 --- /dev/null +++ b/Santander/Santander/Views/Buttons/SantanderButton.swift @@ -0,0 +1,89 @@ +// +// SantanderButton.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import Eureka + +class SantanderButton: UIButton { + + typealias ButtonBlock = (SantanderButton) -> Void + + private var block: ButtonBlock? { + didSet { + self.addTarget(self, action: #selector(onTapAction(sender:)), for: .touchUpInside) + } + } + + var defaultBackgroundColor: UIColor = UIColor.Santander.monza + var selectedBackgroundColor: UIColor = UIColor.Santander.apricot + + init(title: String? = nil, frame: CGRect = .zero) { + super.init(frame: frame) + setTitle(title, for: .normal) + setupStyle() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupStyle() { + layer.cornerRadius = 25.0 + setTitleColor(UIColor.white, for: .normal) + backgroundColor = defaultBackgroundColor + guard let titleLabel = titleLabel else { return } + titleLabel.font = UIFont.santander(type: .medium, with: 16.0) + titleLabel.textAlignment = .center + } + + // MARK: - Highlighted + override var isHighlighted: Bool { + didSet { + invalidateHighlightedAppearance() + } + } + + private func invalidateHighlightedAppearance() { + UIView.animate(withDuration: 0.25, delay: 0.0, options: [.allowUserInteraction, .curveEaseOut], animations: { [weak self] in + guard let self = self else { return } + self.backgroundColor = self.isHighlighted ? self.selectedBackgroundColor : self.defaultBackgroundColor + self.transform = self.isHighlighted ? CGAffineTransform(scaleX: 0.9, y: 0.9) : .identity + }) + } + + // MARK: - Actions + func onTap(_ block: @escaping ButtonBlock) { + self.block = block + } + + @objc func onTapAction(sender: SantanderButton) { + self.block?(sender) + } +} + +extension SantanderButton: SectionHeaderFooterRenderable { + public func viewForItem() -> HeaderFooterViewRepresentable { + self.translatesAutoresizingMaskIntoConstraints = false + var footerView = HeaderFooterView(.class) + footerView.onSetupView = { [weak self] view, _ in + guard let self = self else { return } + view.addSubview(self) + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30.0), + self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30.0), + self.topAnchor.constraint(equalTo: view.topAnchor, constant: 45.0), + self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40.0) + ]) + } + footerView.height = { + 45.0 + 50.0 + 40.0 + } + return footerView + } +} diff --git a/Santander/Santander/Views/Custom/FloatLabelTextField.swift b/Santander/Santander/Views/Custom/FloatLabelTextField.swift new file mode 100644 index 00000000..e5f99c9b --- /dev/null +++ b/Santander/Santander/Views/Custom/FloatLabelTextField.swift @@ -0,0 +1,218 @@ +// +// FloatLabelTextField.swift +// FloatLabelFields +// +// Created by Fahim Farook on 28/11/14. +// Copyright (c) 2014 RookSoft Ltd. All rights reserved. +// +// Original Concept by Matt D. Smith +// http://dribbble.com/shots/1254439--GIF-Mobile-Form-Interaction?list=users +// +// Objective-C version by Jared Verdi +// https://github.com/jverdi/JVFloatLabeledTextField +// + +import UIKit + +@IBDesignable public class FloatLabelTextField: UITextField { + + let animationDuration = 0.3 + var title = UILabel() + let border = CALayer() + + // MARK: - Properties + override public var accessibilityLabel: String! { + get { + if text?.isEmpty ?? true { + return title.text + } else { + return text + } + } + set { + self.accessibilityLabel = newValue + } + } + + override public var placeholder: String? { + didSet { + title.text = placeholder + title.sizeToFit() + } + } + + override public var attributedPlaceholder: NSAttributedString? { + didSet { + title.text = attributedPlaceholder?.string + title.sizeToFit() + } + } + + var titleFont: UIFont = .systemFont(ofSize: 12.0) { + didSet { + title.font = titleFont + title.sizeToFit() + } + } + + @IBInspectable var hintYPadding: CGFloat = 0.0 + + @IBInspectable var titleYPadding: CGFloat = 0.0 { + didSet { + var r = title.frame + r.origin.y = titleYPadding + title.frame = r + } + } + + @IBInspectable var titleTextColour: UIColor = .gray { + didSet { + if !isFirstResponder { + title.textColor = titleTextColour + } + } + } + + @IBInspectable var titleActiveTextColour: UIColor! { + didSet { + if isFirstResponder { + title.textColor = titleActiveTextColour + } + } + } + + @IBInspectable var borderColor: UIColor? { + didSet { + border.borderColor = borderColor?.cgColor + } + } + + @IBInspectable var borderWidth: CGFloat = 0.5 { + didSet { + border.borderWidth = borderWidth + } + } + + // MARK: - Init + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + // MARK: - Overrides + override public func layoutSubviews() { + super.layoutSubviews() + setTitlePositionForTextAlignment() + let isResp = isFirstResponder + if isResp && !(text?.isEmpty ?? true) { + title.textColor = titleActiveTextColour + } else { + title.textColor = titleTextColour + } + // Should we show or hide the title label? + if text?.isEmpty ?? true { + // Hide + hideTitle(isResp) + } else { + // Show + showTitle(isResp) + } + border.frame = CGRect(x: 0, y: self.frame.size.height - borderWidth, width: self.frame.size.width, height: self.frame.size.height) + } + + override public func textRect(forBounds bounds: CGRect) -> CGRect { + var r = super.textRect(forBounds: bounds) + if !(text?.isEmpty ?? true) { + var top = ceil(title.font.lineHeight + hintYPadding) + top = min(top, maxTopInset()) + r = r.inset(by: UIEdgeInsets(top: top, left: 0.0, bottom: 0.0, right: 0.0)) + } + return r.integral + } + + override public func editingRect(forBounds bounds: CGRect) -> CGRect { + var r = super.editingRect(forBounds: bounds) + if !(text?.isEmpty ?? true) { + var top = ceil(title.font.lineHeight + hintYPadding) + top = min(top, maxTopInset()) + r = r.inset(by: UIEdgeInsets(top: top, left: 0.0, bottom: 0.0, right: 0.0)) + } + return r.integral + } + + override public func clearButtonRect(forBounds bounds: CGRect) -> CGRect { + var r = super.clearButtonRect(forBounds: bounds) + if !(text?.isEmpty ?? true) { + var top = ceil(title.font.lineHeight + hintYPadding) + top = min(top, maxTopInset()) + r = CGRect(x: r.origin.x, y: r.origin.y + (top * 0.5), width: r.size.width, height: r.size.height) + } + return r.integral + } + + // MARK: - Private Methods + private func setup() { + borderStyle = .none + titleActiveTextColour = tintColor + // Set up title label + title.alpha = 0.0 + title.font = titleFont + title.textColor = titleTextColour + if let str = placeholder, !str.isEmpty { + title.text = str + title.sizeToFit() + } + if let str = attributedPlaceholder, !str.string.isEmpty { + title.attributedText = str + title.sizeToFit() + } + self.addSubview(title) + + border.borderColor = borderColor?.cgColor + border.borderWidth = borderWidth + self.layer.addSublayer(border) + self.layer.masksToBounds = true + } + + private func maxTopInset() -> CGFloat { + return max(0, floor(bounds.size.height - (font?.lineHeight ?? 0) - 4.0)) + } + + private func setTitlePositionForTextAlignment() { + let r = textRect(forBounds: bounds) + var x = r.origin.x + if textAlignment == .center { + x = r.origin.x + (r.size.width * 0.5) - title.frame.size.width + } else if textAlignment == .right { + x = r.origin.x + r.size.width - title.frame.size.width + } + title.frame = CGRect(x: x, y: title.frame.origin.y, width: title.frame.size.width, height: title.frame.size.height) + } + + private func showTitle(_ animated: Bool) { + let dur = animated ? animationDuration : 0 + UIView.animate(withDuration: dur, delay: 0, options: [.beginFromCurrentState, .curveEaseOut], animations: { + // Animation + self.title.alpha = 1.0 + var r = self.title.frame + r.origin.y = self.titleYPadding + self.title.frame = r + }) + } + + private func hideTitle(_ animated: Bool) { + let dur = animated ? animationDuration : 0 + UIView.animate(withDuration: dur, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { + // Animation + self.title.alpha = 0.0 + var r = self.title.frame + r.origin.y = self.title.font.lineHeight + self.hintYPadding + self.title.frame = r + }) + } +} diff --git a/Santander/Santander/Views/Custom/FundRiskView.swift b/Santander/Santander/Views/Custom/FundRiskView.swift new file mode 100644 index 00000000..cb2ef123 --- /dev/null +++ b/Santander/Santander/Views/Custom/FundRiskView.swift @@ -0,0 +1,144 @@ +// +// FundRiskView.swift +// Santander +// +// Created by Orlando Amorim on 14/08/19. +// + +import UIKit +import SnapKit + +class FundRiskView: UIView { + + static let arrowWidth: CGFloat = 13.0 + + let risk: FundsScreen.Risk + + var textLabel: UILabel = { + let label = UILabel() + label.font = UIFont.santander(type: .medium, with: 16.0) + label.textColor = UIColor.Santander.gray + label.textAlignment = .center + return label + }() + + var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow-icon")) + imageView.contentMode = .scaleAspectFill + return imageView + }() + + var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .bottom + stackView.distribution = .fillEqually + stackView.spacing = 1 + return stackView + }() + + var arrowLeadingConstraint: Constraint? + + init(text: String, risk: FundsScreen.Risk, frame: CGRect = .zero) { + self.risk = risk + super.init(frame: frame) + textLabel.text = text + setupView() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + setupArrow() + } + + private func setupView() { + addTextLabel() + addStackView() + addArrowImageView() + setupArrow() + setupRiskLevel() + } + + private func addTextLabel() { + addSubview(textLabel) + textLabel.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(34.0) + } + } + + private func addArrowImageView() { + addSubview(arrowImageView) + arrowImageView.snp.makeConstraints { make in + make.top.equalTo(textLabel.snp.bottom).offset(15.5) + make.height.equalTo(8.0) + make.width.equalTo(FundRiskView.arrowWidth) + arrowLeadingConstraint = make.leading.equalToSuperview().inset(0.0).constraint + make.bottom.equalTo(stackView.snp.top).offset(-6.5) + } + } + + private func addStackView() { + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.height.equalTo(10.0) + make.bottom.leading.trailing.equalToSuperview() + } + } + + private func setupRiskLevel() { + stackView.removeAllArrangedSubviews() + FundsScreen.Risk.allCases.forEach { risk in + let riskView = makeRiskView(for: risk) + stackView.addArrangedSubview(riskView) + riskView.snp.makeConstraints { make in + if self.risk == risk { + make.top.equalToSuperview() + make.bottom.equalTo(self.snp.bottom) + } else { + make.bottom.equalTo(self.snp.bottom).inset(1.0) + make.height.equalTo(6.0) + } + } + } + } + + private func setupArrow() { + let arrowMiddleWidth = FundRiskView.arrowWidth / 2 + let riskLevelWidth = frame.size.width / CGFloat(FundsScreen.Risk.allCases.count) + let middleRiskLevelWidth = riskLevelWidth / CGFloat(2) + let arrowPosition = ((riskLevelWidth * CGFloat(risk.rawValue)) - middleRiskLevelWidth) - arrowMiddleWidth + arrowLeadingConstraint?.update(inset: arrowPosition) + } +} + +// MARK: - Factory +extension FundRiskView { + private func makeRiskView(for risk: FundsScreen.Risk) -> UIView { + let view = UIView() + view.tag = risk.rawValue + view.backgroundColor = risk.color + + let radius: CGFloat = risk == self.risk ? 5.0 : 3.0 + if risk == .one { + if #available(iOS 11.0, *) { + view.round(corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner], radius: radius) + } else { + view.round(corners: [.bottomLeft, .topLeft], radius: radius) + } + } + if risk == .five { + if #available(iOS 11.0, *) { + view.round(corners: [.layerMaxXMaxYCorner, .layerMaxXMinYCorner], radius: radius) + } else { + view.round(corners: [.topRight, .bottomRight], radius: radius) + } + } + return view + } +} diff --git a/Santander/Santander/Views/Custom/MoreInfoPercentagesView.swift b/Santander/Santander/Views/Custom/MoreInfoPercentagesView.swift new file mode 100644 index 00000000..97382eee --- /dev/null +++ b/Santander/Santander/Views/Custom/MoreInfoPercentagesView.swift @@ -0,0 +1,133 @@ +// +// MoreInfoPercentagesView.swift +// Santander +// +// Created by Orlando Amorim on 14/08/19. +// + +import UIKit +import SnapKit + +class MoreInfoPercentagesView: UIView { + + let moreInfo: FundsScreen.MoreInfo + + init(moreInfo: FundsScreen.MoreInfo, frame: CGRect = .zero) { + self.moreInfo = moreInfo + super.init(frame: frame) + setupView() + makeView() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + isUserInteractionEnabled = true + backgroundColor = .white + } +} + +// MARK: - Factory +extension MoreInfoPercentagesView { + + private func makeView() { + let header = makeHeader() + addSubview(header) + header.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(32.0) + } + + var lastPercentagesView: UIView? + let allCases = FundsScreen.MoreInfo.CodingKeys.allCases + allCases.forEach { key in + let percentagesView = makePercentagesView(with: key, percentages: moreInfo.value(for: key)) + addSubview(percentagesView) + percentagesView.snp.makeConstraints { make in + if let lastPercentagesView = lastPercentagesView { + make.top.equalTo(lastPercentagesView.snp.bottom) + } else { + make.top.equalTo(header.snp.bottom).inset(-2.0) + } + make.leading.trailing.equalToSuperview() + make.height.equalTo(32.0) + if let last = allCases.last, last == key { + make.bottom.equalToSuperview() + } + } + lastPercentagesView = percentagesView + } + } + + private func makeHeader() -> UIView { + let headerView = UIView() + + let cdiTitleLabel = makeTitleLabel(with: "CDI", textAlignment: .right) + headerView.addSubview(cdiTitleLabel) + cdiTitleLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(32.0) + } + + let fundTitleLabel = makeTitleLabel(with: "Fundo", textAlignment: .right) + headerView.addSubview(fundTitleLabel) + fundTitleLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(102.0) + make.centerY.equalToSuperview() + make.height.equalTo(32.0) + } + return headerView + } + + private func makePercentagesView(with key: FundsScreen.MoreInfo.CodingKeys, percentages: FundsScreen.MoreInfo.Percentages) -> UIView { + let percentageView = UIView() + + let titleLabel = makeTitleLabel(with: FundsScreen.MoreInfo.title(for: key), textAlignment: .left) + percentageView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(32.0) + } + + let cdiValueLabel = makeValueLabel(with: "\(percentages.cdi)%", textAlignment: .right) + percentageView.addSubview(cdiValueLabel) + cdiValueLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(32.0) + } + + let fundValueLabel = makeValueLabel(with: "\(percentages.fund)%", textAlignment: .right) + percentageView.addSubview(fundValueLabel) + fundValueLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(102.0) + make.centerY.equalToSuperview() + make.height.equalTo(32.0) + } + + return percentageView + } + + private func makeTitleLabel(with text: String, textAlignment: NSTextAlignment = .left) -> UILabel { + let label = UILabel() + label.text = text + label.font = UIFont.santander(type: .regular, with: 14.0) + label.textColor = UIColor.Santander.silverChalice + label.textAlignment = textAlignment + return label + } + + private func makeValueLabel(with text: String, textAlignment: NSTextAlignment = .right) -> UILabel { + let label = UILabel() + label.text = text + label.font = UIFont.santander(type: .regular, with: 14.0) + label.textColor = UIColor.Santander.mineShaft + label.textAlignment = textAlignment + return label + } +} diff --git a/Santander/Santander/Views/Custom/SegmentedControl.swift b/Santander/Santander/Views/Custom/SegmentedControl.swift new file mode 100644 index 00000000..76da09c0 --- /dev/null +++ b/Santander/Santander/Views/Custom/SegmentedControl.swift @@ -0,0 +1,142 @@ +// +// SegmentedControl.swift +// Santander +// +// Created by Orlando Amorim on 10/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import SnapKit + +class SegmentedControl: UIView { + + typealias Button = (title: String, isSelected: Bool) + typealias ButtonBlock = (_ selectedIndex: Int) -> Void + private var block: ButtonBlock? + + private var buttons: [UIButton] = [] + private var selectorView: UIView! + private var selectedIndex: Int = 0 + + var textColor: UIColor = .white + var selectedColor: UIColor = UIColor.Santander.guardsmanRed + var unselectedColor: UIColor = UIColor.Santander.monza + + var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.distribution = .fillEqually + return stackView + }() + + init() { + super.init(frame: .zero) + setupView() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + updateSelectorView() + } + + private func setupView() { + backgroundColor = .white + setupStackView() + setupSelectorView() + } + + func set(buttons: [Button]) { + updateView(with: buttons) + } + + func onTap(_ block: @escaping ButtonBlock) { + self.block = block + } + + @objc func onTapAction(sender: UIButton) { + guard let index = buttons.firstIndex(of: sender) else { + return + } + didSelected(index: index) + } + + func didSelected(index: Int) { + buttons.forEach({ $0.backgroundColor = unselectedColor }) + let selectorPosition = frame.width / CGFloat(self.buttons.count) * CGFloat(index) + UIView.animate(withDuration: 0.3) { [weak self] in + guard let self = self else { + return + } + self.block?(index) + self.selectedIndex = index + self.buttons[index].backgroundColor = self.selectedColor + self.selectorView.frame.origin.x = selectorPosition + } + } +} + +// MARK: - Factory +extension SegmentedControl { + + private func updateView(with buttons: [Button]) { + create(with: buttons) + updateStackView() + updateSelectorView() + } + + private func create(with buttons: [Button]) { + self.buttons.removeAll() + var selectedIndex = 0 + buttons.enumerated().forEach { [weak self] index, button in + if let self = self { + let newButton = self.createButton(with: button) + selectedIndex = button.isSelected ? index : selectedIndex + self.buttons.append(newButton) + } + } + didSelected(index: selectedIndex) + } + + private func createButton(with button: Button) -> UIButton { + let newButton = UIButton(type: .system) + newButton.setTitle(button.title, for: .normal) + newButton.addTarget(self, action: #selector(onTapAction(sender:)), for: .touchUpInside) + newButton.setTitleColor(textColor, for: .normal) + newButton.backgroundColor = button.isSelected ? selectedColor : unselectedColor + newButton.titleLabel?.font = UIFont.santander(type: .medium, with: 16.0) + return newButton + } + + private func updateSelectorView() { + let selectorPosition = frame.width / CGFloat(self.buttons.count) * CGFloat(selectedIndex) + let selectorWidth = frame.width / CGFloat(self.buttons.count) + selectorView.frame = CGRect(x: selectorPosition, y: 0.0, width: selectorWidth.isNaN ? 0.0 : selectorWidth, height: 2) + } + + private func setupSelectorView() { + let selectorWidth = frame.width / CGFloat(self.buttons.count) + selectorView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: selectorWidth.isNaN ? 0.0 : selectorWidth, height: 2)) + selectorView.backgroundColor = unselectedColor + addSubview(selectorView) + } + + private func updateStackView() { + stackView.removeAllArrangedSubviews() + buttons.forEach({ stackView.addArrangedSubview($0) }) + } + + private func setupStackView() { + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(2.0) + make.leading.trailing.bottom.equalToSuperview() + } + } +} diff --git a/Santander/Santander/Views/Custom/SuccessView.swift b/Santander/Santander/Views/Custom/SuccessView.swift new file mode 100644 index 00000000..56d93072 --- /dev/null +++ b/Santander/Santander/Views/Custom/SuccessView.swift @@ -0,0 +1,88 @@ +// +// SuccessView.swift +// Santander +// +// Created by Orlando Amorim on 13/08/19. +// + +import UIKit +import SnapKit + +class SuccessView: UIView { + + typealias ButtonBlock = () -> Void + private var block: ButtonBlock? { + didSet { + sendNewMessage.addTarget(self, action: #selector(onTapAction), for: .touchUpInside) + } + } + + private var infoLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + private var sendNewMessage: UIButton = { + let button = UIButton() + button.setTitle("Enviar nova mensagem", for: .normal) + button.setTitleColor(UIColor.Santander.monza, for: .normal) + button.titleLabel?.font = UIFont.santander(type: .medium, with: 16.0) + return button + }() + + init() { + super.init(frame: .zero) + setupView() + setupInfoLabel() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + isUserInteractionEnabled = true + addInfoLabel() + addSendNewButton() + backgroundColor = .white + } + + private func addInfoLabel() { + addSubview(infoLabel) + infoLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + private func addSendNewButton() { + addSubview(sendNewMessage) + sendNewMessage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().inset(87.0) + } + } + + private func setupInfoLabel() { + let firstAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.Santander.gray, + .font: UIFont.santander(type: .medium, with: 14.0)] + let secondAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.Santander.mineShaft, + .font: UIFont.santander(type: .medium, with: 28.0)] + + let infoText = NSMutableAttributedString(string: "Obrigado!\n", attributes: firstAttributes) + let secondString = NSAttributedString(string: "Mensagem enviada\ncom sucesso :)", attributes: secondAttributes) + infoText.append(secondString) + infoLabel.attributedText = infoText + } + + // MARK: - Actions + func onTap(_ block: @escaping ButtonBlock) { + self.block = block + } + + @objc private func onTapAction() { + self.block?() + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoCell.swift b/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoCell.swift new file mode 100644 index 00000000..802556eb --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoCell.swift @@ -0,0 +1,58 @@ +// +// DownloadInfoCell.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +public class DownloadInfoCell: Cell, CellType { + + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.santander(type: .regular, with: 14.0) + label.textAlignment = .left + label.textColor = UIColor.Santander.silverChalice + return label + }() + + var downloadButton: DownloadButton = { + var button = DownloadButton() + return button + }() + + // MARK: - Views + public override func update() { + titleLabel.text = row.title + } + + public override func setup() { + super.setup() + selectionStyle = .none + height = { 32.0 } + setupView() + } + + private func setupView() { + addTitleLabel() + addDownloadButton() + } + + private func addTitleLabel() { + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints({ make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().inset(30.0) + }) + } + + private func addDownloadButton() { + contentView.addSubview(downloadButton) + downloadButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(30.0) + } + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoRow.swift b/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoRow.swift new file mode 100644 index 00000000..de4e05d2 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/DownloadInfoRow/DownloadInfoRow.swift @@ -0,0 +1,22 @@ +// +// DownloadInfoRow.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +final class DownloadInfoRow: Row>, RowType { + + // MARK: - Actions + func onDonwloadButtonTap(_ block: @escaping DownloadButton.ButtonBlock) { + cell.downloadButton.onTap(block) + } + + required public init(tag: String?) { + super.init(tag: tag) + displayValueFor = nil + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/EmailFloatLabelRow.swift b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/EmailFloatLabelRow.swift new file mode 100644 index 00000000..10078eb5 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/EmailFloatLabelRow.swift @@ -0,0 +1,33 @@ +// +// EmailFloatLabelRow.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Eureka + +public class EmailFloatLabelCell: _FloatLabelCell, CellType { + + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func setup() { + super.setup() + textField?.autocorrectionType = .no + textField?.autocapitalizationType = .none + textField?.keyboardType = .emailAddress + } +} + +public final class EmailFloatLabelRow: FloatFieldRow, RowType { + public required init(tag: String?) { + super.init(tag: tag) + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/FloatFieldRow.swift b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/FloatFieldRow.swift new file mode 100644 index 00000000..dcd6f76f --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/FloatFieldRow.swift @@ -0,0 +1,267 @@ +// +// FloatFieldRow.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import UIKit +import Eureka +import TLCustomMask +import SnapKit + +// MARK: - FloatLabelCell + +public class _FloatLabelCell: Cell, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable { + + public var textField: UITextField! { return floatLabelTextField } + + public enum MaskType { + case phone + case none + } + + private lazy var clearButton: UIButton = { + let button = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + button.addTarget(self, action: #selector(toggleClearButton), for: .touchUpInside) + return button + }() + + private lazy var customMask = TLCustomMask() + + public var clearImage: (on: UIImage?, off: UIImage?) { + didSet { + setClearButtonImage() + } + } + + public var customMaskType: MaskType = .none { + didSet { + if customMaskType == .none { + customMask.formattingPattern = "" + } + } + } + + public var topSpacing: CGFloat = 0.0 { + didSet { + if let topConstraint = topConstraint { + topConstraint.update(inset: topSpacing) + } + } + } + private var topConstraint: Constraint? + + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + lazy public var floatLabelTextField: FloatLabelTextField = { [unowned self] in + let floatTextField = FloatLabelTextField() + floatTextField.translatesAutoresizingMaskIntoConstraints = false + floatTextField.titleFont = UIFont.santander(type: .regular, with: 11.0) + floatTextField.font = UIFont.santander(type: .medium, with: 18.0) + floatTextField.delegate = self + floatTextField.addTarget(self, action: #selector(_FloatLabelCell.textFieldDidChange(_:)), for: .editingChanged) + floatTextField.titleTextColour = UIColor.Santander.silverChalice + floatTextField.titleActiveTextColour = UIColor.Santander.silverChalice + floatTextField.borderWidth = 1 + floatTextField.borderColor = UIColor.Santander.gallery + floatTextField.tintColor = UIColor.Santander.havelockBlue + return floatTextField + }() + + open override func setup() { + super.setup() + height = { [weak self] in + guard let self = self else { + return 0.0 + } + return 47.0 + self.topSpacing + } + selectionStyle = .none + setupClearButton() + setupFloatLabelTextField() + } + + private func setupFloatLabelTextField() { + contentView.addSubview(floatLabelTextField) + floatLabelTextField.snp.makeConstraints { [weak self] make in + guard let self = self else { + return + } + self.topConstraint = make.top.equalToSuperview().inset(self.topSpacing).constraint + make.leading.trailing.equalToSuperview().inset(40.0) + make.bottom.equalToSuperview() + } + } + + open override func update() { + super.update() + textLabel?.text = nil + detailTextLabel?.text = nil + floatLabelTextField.attributedPlaceholder = NSAttributedString(string: row.title ?? "", attributes: [.foregroundColor: UIColor.Santander.silverChalice, + .font: UIFont.santander(type: .regular, with: 16.0)]) + floatLabelTextField.text = row.displayValueFor?(row.value) + floatLabelTextField.isEnabled = !row.isDisabled + floatLabelTextField.alpha = row.isDisabled ? 0.6 : 1 + setClearButtonImage() + } + + private func setupClearButton() { + clearImage = (on: UIImage(named: "clear-icon"), off: nil) + textField.clearButtonMode = .never + floatLabelTextField.rightViewMode = .always + floatLabelTextField.rightView = clearButton + } + + @objc + private func toggleClearButton() { + textField.text = customMask.formatString(string: "") + row.value = nil + row.updateCell() + setClearButtonImage() + } + + private func setClearButtonImage() { + let image = textField.text != nil ? (textField.text!.isEmpty ? clearImage.off : clearImage.on) : clearImage.off + clearButton.setImage(image, for: .normal) + clearButton.setImage(image, for: .highlighted) + } + + /// Returns the value withou mask + func cleanText() -> String { + return customMask.cleanText + } + + open override func cellCanBecomeFirstResponder() -> Bool { + return !row.isDisabled && floatLabelTextField.canBecomeFirstResponder + } + + open override func cellBecomeFirstResponder(withDirection direction: Direction) -> Bool { + return floatLabelTextField.becomeFirstResponder() + } + + open override func cellResignFirstResponder() -> Bool { + return floatLabelTextField.resignFirstResponder() + } + + @objc public func textFieldDidChange(_ textField: UITextField) { + guard let textValue = textField.text else { + row.value = nil + return + } + if let fieldRow = row as? FormatterConformance, let formatter = fieldRow.formatter { + if fieldRow.useFormatterDuringInput { + let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) + let errorDesc: AutoreleasingUnsafeMutablePointer? = nil + if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { + row.value = value.pointee as? T + if var selStartPos = textField.selectedTextRange?.start { + let oldVal = textField.text + textField.text = row.displayValueFor?(row.value) + if let f = formatter as? FormatterProtocol { + selStartPos = f.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) + } + textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos) + } + return + } + } else { + let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) + let errorDesc: AutoreleasingUnsafeMutablePointer? = nil + if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { + row.value = value.pointee as? T + } + return + } + } + guard !textValue.isEmpty else { + row.value = nil + return + } + guard let newValue = T.init(string: textValue) else { + row.value = nil + return + } + row.value = nil + row.value = newValue + row.updateCell() + } + + // MARK: - Helpers + + private func displayValue(useFormatter: Bool) -> String? { + guard let v = row.value else { return nil } + if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { + return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) + } + let text = String(describing: v) + if customMaskType != .none { + return customMask.formatString(string: text) + } else { + return text + } + } + + // MARK: - TextFieldDelegate + + public func textFieldDidBeginEditing(_ textField: UITextField) { + formViewController()?.beginEditing(of: self) + if let fieldRowConformance = row as? FormatterConformance, fieldRowConformance.formatter != nil, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput { + textField.text = displayValue(useFormatter: true) + } else { + textField.text = displayValue(useFormatter: false) + } + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + formViewController()?.endEditing(of: self) + formViewController()?.textInputDidEndEditing(textField, cell: self) + textFieldDidChange(textField) + textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + setClearButtonImage() + switch customMaskType { + case .phone: + return managePhoneMask(textField, shouldChangeCharactersIn: range, replacementString: string) + default: + return true + } + } +} + +extension _FloatLabelCell { + private func managePhoneMask(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard let text = textField.text as NSString? else { return true } + var newText = text.replacingCharacters(in: range, with: string) + if newText.count >= 15 { + customMask.formattingPattern = "($$) $$$$$-$$$$" + } else { + customMask.formattingPattern = "($$) $$$$-$$$$" + } + newText = customMask.formatString(string: newText) + guard let newValue = T.init(string: newText) else { + row.value = nil + return false + } + row.value = newValue + textField.text = newText + setClearButtonImage() + return false + } +} + +// MARK: - FloatLabelRow +open class FloatFieldRow: FormatteableRow where Cell: BaseCell, Cell: TextFieldCell { + public required init(tag: String?) { + super.init(tag: tag) + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/PhoneFloatLabelRow.swift b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/PhoneFloatLabelRow.swift new file mode 100644 index 00000000..7cf91105 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/PhoneFloatLabelRow.swift @@ -0,0 +1,32 @@ +// +// PhoneFloatLabelRow.swift +// Santander +// +// Created by Orlando Amorim on 13/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Eureka + +public class PhoneFloatLabelCell : _FloatLabelCell, CellType { + + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func setup() { + super.setup() + customMaskType = .phone + textField?.keyboardType = .phonePad + } +} + +public final class PhoneFloatLabelRow: FloatFieldRow, RowType { + public required init(tag: String?) { + super.init(tag: tag) + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/TextFloatLabelCell.swift b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/TextFloatLabelCell.swift new file mode 100644 index 00000000..61e42b60 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/FloatLabelRow/TextFloatLabelCell.swift @@ -0,0 +1,33 @@ +// +// TextFloatLabelCell.swift +// Santander +// +// Created by Orlando Amorim on 11/08/19. +// Copyright © 2019 Santander. All rights reserved. +// + +import Eureka + +public class TextFloatLabelCell: _FloatLabelCell, CellType { + + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func setup() { + super.setup() + textField?.autocorrectionType = .default + textField?.autocapitalizationType = .sentences + textField?.keyboardType = .default + } +} + +public final class TextFloatLabelRow: FloatFieldRow, RowType { + public required init(tag: String?) { + super.init(tag: tag) + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoCell.swift b/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoCell.swift new file mode 100644 index 00000000..d53fb1da --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoCell.swift @@ -0,0 +1,62 @@ +// +// MoreInfoCell.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +public class MoreInfoCell: Cell, CellType { + + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.santander(type: .regular, with: 14.0) + label.textAlignment = .left + label.textColor = UIColor.Santander.silverChalice + return label + }() + + let detailLabel: UILabel = { + let label = UILabel() + label.font = UIFont.santander(type: .regular, with: 14.0) + label.textAlignment = .right + label.textColor = UIColor.Santander.mineShaft + return label + }() + + // MARK: - Views + public override func update() { + titleLabel.text = row.title + detailLabel.text = row.value + } + + public override func setup() { + super.setup() + selectionStyle = .none + height = { 32.0 } + setupView() + } + + private func setupView() { + addTitleLabel() + addDetailLabel() + } + + private func addTitleLabel() { + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints({ make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().inset(30.0) + }) + } + + private func addDetailLabel() { + contentView.addSubview(detailLabel) + detailLabel.snp.makeConstraints({ make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(30.0) + }) + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoRow.swift b/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoRow.swift new file mode 100644 index 00000000..7e0b4355 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/MoreInfoRow/MoreInfoRow.swift @@ -0,0 +1,16 @@ +// +// MoreInfoRow.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +final class MoreInfoRow: Row, RowType { + required public init(tag: String?) { + super.init(tag: tag) + displayValueFor = nil + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleCell.swift b/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleCell.swift new file mode 100644 index 00000000..0f89987f --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleCell.swift @@ -0,0 +1,60 @@ +// +// TitleSubtitleCell.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +public class TitleSubtitleCell: Cell, CellType { + + let titleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + return label + }() + + let subtitleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + return label + }() + + // MARK: - Views + public override func update() { + titleLabel.text = row.title + subtitleLabel.text = row.value + } + + public override func setup() { + super.setup() + selectionStyle = .none + setupView() + } + + private func setupView() { + addTitleLabel() + addSubtitleLabel() + } + + private func addTitleLabel() { + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints({ make in + make.top.centerX.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(30.0) + }) + } + + private func addSubtitleLabel() { + contentView.addSubview(subtitleLabel) + subtitleLabel.snp.makeConstraints({ make in + make.centerX.equalToSuperview() + make.top.equalTo(titleLabel.snp.bottom).inset(-10.0) + make.leading.trailing.equalTo(titleLabel) + make.bottom.equalToSuperview() + }) + } +} + diff --git a/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleRow.swift b/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleRow.swift new file mode 100644 index 00000000..c50442c6 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/TitleValueRow/TitleSubtitleRow.swift @@ -0,0 +1,16 @@ +// +// TitleSubtitleRow.swift +// Santander +// +// Created by Orlando Amorim on 15/08/19. +// + +import UIKit +import Eureka + +final class TitleSubtitleRow: Row, RowType { + required public init(tag: String?) { + super.init(tag: tag) + displayValueFor = nil + } +} diff --git a/Santander/Santander/Views/Eureka Custom Rows/ViewRow.swift b/Santander/Santander/Views/Eureka Custom Rows/ViewRow.swift new file mode 100644 index 00000000..ea095194 --- /dev/null +++ b/Santander/Santander/Views/Eureka Custom Rows/ViewRow.swift @@ -0,0 +1,158 @@ +// +// ViewRow.swift +// CustomViewRow +// +// Created by Mark Alldritt on 2017-09-13. +// Copyright © 2017 Late Night Software Ltd. All rights reserved. +// +import UIKit +import Eureka + + +public class ViewCell : Cell, CellType { + + public var view : ViewType? + + public var viewRightMargin = CGFloat(15.0) + public var viewLeftMargin = CGFloat(15.0) + public var viewTopMargin = CGFloat(1.0) + public var viewBottomMargin = CGFloat(1.0) + + public var titleLeftMargin = CGFloat(15.0) + public var titleRightMargin = CGFloat(5.0) + public var titleTopMargin = CGFloat(12.0) + public var titleBottomMargin = CGFloat(4.0) + + public var titleLabel : UILabel? + + private var notificationObserver : NSObjectProtocol? + + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = UIColor.white + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let notificationObserver = notificationObserver { + NotificationCenter.default.removeObserver(notificationObserver) + } + } + + open override func setup() { + super.setup() + + titleLabel = UILabel(frame: CGRect.zero) + titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + contentView.addSubview(titleLabel!) + + // Provide a default row height calculation based on the height of the assigned view. + height = { + if self.titleLabel!.text == nil || self.titleLabel!.text == "" { + return ceil((self.view?.frame.height ?? 0) + self.viewTopMargin + self.viewBottomMargin) + } + else { + let titleHeight = ceil(self.titleLabel!.sizeThatFits(CGSize(width: self.contentView.frame.width - self.titleLeftMargin - self.titleRightMargin, height: 9999.0)).height) + + return ceil(titleHeight + self.titleTopMargin + self.titleBottomMargin + (self.view?.frame.height ?? 0.0) + self.viewTopMargin + self.viewBottomMargin) + } + } + + notificationObserver = NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, + object: nil, + queue: nil, + using: { [weak self] (note) in + self?.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + self?.setNeedsLayout() + }) + + selectionStyle = .none + } + + open override func didSelect() { + } + + + open override func layoutSubviews() { + super.layoutSubviews() + + // This could be done with autolayout, but this seems simpler... + let contentFrame = contentView.frame + + if titleLabel!.text == nil || titleLabel!.text == "" { + titleLabel!.frame = CGRect.zero + view?.frame = CGRect(x: viewLeftMargin, + y: viewTopMargin, + width: contentFrame.width - viewLeftMargin - viewRightMargin, + height: contentFrame.height - viewTopMargin - viewBottomMargin) + } + else { + let titleHeight = ceil(titleLabel!.sizeThatFits(CGSize(width: contentFrame.width - titleLeftMargin - titleRightMargin, height: 9999.0)).height) + let titleFrame = CGRect(x: titleLeftMargin, + y: titleTopMargin, + width: contentFrame.width - titleLeftMargin - titleRightMargin, + height: titleHeight) + let viewFrame = CGRect(x: viewLeftMargin, + y: titleFrame.maxY + titleBottomMargin + viewTopMargin, + width: contentFrame.width - viewLeftMargin - viewRightMargin, + height: ceil(contentFrame.height - titleFrame.maxY - titleBottomMargin - viewTopMargin - viewBottomMargin)) + + titleLabel!.frame = titleFrame + view?.frame = viewFrame + } + } + +} + +// MARK: ViewRow +open class _ViewRow: Row > { + + override open func updateCell() { + // NOTE: super.updateCell() deliberatly not called. + + // Deal with the case where the caller did not add their custom view to the containerView in a + // backwards compatible manner. + if let view = cell.view, + view.superview != cell.contentView { + view.removeFromSuperview() + cell.contentView.addSubview(view) + } + cell.titleLabel?.text = title + } + + required public init(tag: String?) { + super.init(tag: tag) + displayValueFor = nil + } +} + +// ViewRow class with value type specialization. When/if Swift allows default values for generics this can be folded +// into the ViewRow class. +public final class ViewRowGeneric: _ViewRow, RowType { + + public var view: ViewType? { // provide a convience accessor for the view + return cell.view + } + + required public init(tag: String?) { + super.init(tag: tag) + } + +} + +// legacy ViewRow class without value type specialization +public final class ViewRow : _ViewRow, RowType { + + public var view: ViewType? { // provide a convience accessor for the view + return cell.view + } + + required public init(tag: String?) { + super.init(tag: tag) + } + +} diff --git a/Santander/SantanderUnitTests/Info.plist b/Santander/SantanderUnitTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/Santander/SantanderUnitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Santander/SantanderUnitTests/Library/Eureka Rules/StringExtensionsTests.swift b/Santander/SantanderUnitTests/Library/Eureka Rules/StringExtensionsTests.swift new file mode 100644 index 00000000..901eeac7 --- /dev/null +++ b/Santander/SantanderUnitTests/Library/Eureka Rules/StringExtensionsTests.swift @@ -0,0 +1,74 @@ +// +// StringExtensionsTests.swift +// SantanderTests +// +// Created by Orlando Amorim on 17/08/19. +// + +import XCTest +@testable import Santander + +class StringExtensionsTests: XCTestCase { + + // Normally, it can be argued that force unwrapping (!) should + // be avoided, but in unit tests it can be a good idea for + // properties (only!) in order to avoid unnecessary boilerplate. + private var rule: RulePhoneNumber! + + override func setUp() { + super.setUp() + rule = RulePhoneNumber() + } + + override func tearDown() { + rule = nil + super.tearDown() + } + + // MARK: - Valid numbers allowing empty + func testRuleValidPhoneNumberAllowingEmpty() { + rule.allowsEmpty = true + + XCTAssertNil(rule.isValid(value: "(31) 4633-9632"), "Is valid number without digit 9") + XCTAssertNil(rule.isValid(value: "(31) 99633-9632"), "Is valid number with digit 9") + XCTAssertNil(rule.isValid(value: ""), "Is valid empty number") + } + + // MARK: - Inalid numbers allowing empty + func testRuleInvalidPhoneNumberAllowingEmpty() { + rule.allowsEmpty = true + + XCTAssertNotNil(rule.isValid(value: "(31) 9484-82080"), "Is invalid phone number with 5 digits after -") + XCTAssertNotNil(rule.isValid(value: "(11)9678-0750"), "Is invalid phone number without spacing after )") + XCTAssertNotNil(rule.isValid(value: "(021 12) 91212-2124"), "Is invalid phone number with carrier code") + XCTAssertNotNil(rule.isValid(value: "(12) 9 1212-1212"), "Is invalid phone number with spacing between the 9 digit and the rest of number") + XCTAssertNotNil(rule.isValid(value: "1212-1124"), "Is invalid phone number without the state code") + XCTAssertNotNil(rule.isValid(value: "319484-8208"), "Is invalid phone number without () between the state code") + XCTAssertNotNil(rule.isValid(value: "3194848208"), "Is invalid phone number without () between the state code and - between numbers") + XCTAssertNotNil(rule.isValid(value: " "), "Is invalid phone number with space") + } + + + // MARK: - Valid numbers not allowing empty + func testRuleValidPhoneNumberNotAllowingEmpty() { + rule.allowsEmpty = false + + XCTAssertNil(rule.isValid(value: "(31) 4633-9632"), "Is valid number without digit 9") + XCTAssertNil(rule.isValid(value: "(31) 99633-9632"), "Is valid number with digit 9") + } + + // MARK: - Invalid numbers not allowing empty + func testRuleInvalidPhoneNumberNotAllowingEmpty() { + rule.allowsEmpty = false + + XCTAssertNotNil(rule.isValid(value: "(31) 9484-82080"), "Is invalid phone number with 5 digits after -") + XCTAssertNotNil(rule.isValid(value: "(11)9678-0750"), "Is invalid phone number without spacing after )") + XCTAssertNotNil(rule.isValid(value: "(021 12) 91212-2124"), "Is invalid phone number with carrier code") + XCTAssertNotNil(rule.isValid(value: "(12) 9 1212-1212"), "Is invalid phone number with spacing between the 9 digit and the rest of number") + XCTAssertNotNil(rule.isValid(value: "1212-1124"), "Is invalid phone number without the state code") + XCTAssertNotNil(rule.isValid(value: "319484-8208"), "Is invalid phone number without () between the state code") + XCTAssertNotNil(rule.isValid(value: "3194848208"), "Is invalid phone number without () between the state code and - between numbers") + XCTAssertNotNil(rule.isValid(value: " "), "Is invalid phone number with space") + XCTAssertNotNil(rule.isValid(value: ""), "Is invalid empty number") + } +} diff --git a/Santander/SantanderUnitTests/Scenes/Contact/ContactInteractorTests.swift b/Santander/SantanderUnitTests/Scenes/Contact/ContactInteractorTests.swift new file mode 100644 index 00000000..ea01a0b4 --- /dev/null +++ b/Santander/SantanderUnitTests/Scenes/Contact/ContactInteractorTests.swift @@ -0,0 +1,126 @@ +// +// ContactInteractorTests.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 21/08/19. +// + +import XCTest +@testable import Santander + +class ContactInteractorTests: XCTestCase { + + // MARK: - Interactor + var interactor: ContactInteractor! + + // MARK: - Test lifecycle + override func setUp() { + super.setUp() + setupIntercator() + } + + override func tearDown() { + interactor = nil + super.tearDown() + } + + // MARK: - Test setup + func setupIntercator() { + interactor = ContactInteractor() + } + + class ContactPresentationLogicSpy: ContactPresentationLogic { + + // MARK: Method call expectations + var presentFormCalled = false + var presentErrorCalled = false + var presentSuccessCalled = false + + // MARK: Spied methods + func presentForm(_ form: ContactForm) { + presentFormCalled = true + } + + func presentError(_ error: Error) { + presentErrorCalled = true + } + + func presentSuccess() { + presentSuccessCalled = true + } + } + + class ContactWorkerSpy: ContactWorker { + + var shouldReturnSuccess: Bool = true + + // MARK: Method call expectations + var getFormCalled = false + + override func getForm(result: @escaping (Result) -> Void) { + getFormCalled = true + if shouldReturnSuccess { + let cells = [TestData.ContactForm.emailCell] + let contactForm = ContactForm(cells: cells) + result(.success(contactForm)) + } else { + result(.failure(TestError.contact)) + } + } + } + + func testFetchContactsShouldAskContactsWorkerToGetFormAndPresenterToFormatSuccessResult() { + // Given + let contactPresentationLogicSpy = ContactPresentationLogicSpy() + interactor.presenter = contactPresentationLogicSpy + let contactWorkerSpy = ContactWorkerSpy(contactStore: ContactAPI()) + interactor.worker = contactWorkerSpy + + // When + interactor.getForm() + + // Then + XCTAssert(contactWorkerSpy.getFormCalled, "ContactInteractor() should ask ContactWorker to fetch form") + XCTAssert(contactPresentationLogicSpy.presentFormCalled, "ContactInteractor() should ask presenter to present form") + } + + func testFetchContactsShouldAskContactsWorkerToGetFormAndPresenterToFormatErrorResult() { + // Given + let contactPresentationLogicSpy = ContactPresentationLogicSpy() + interactor.presenter = contactPresentationLogicSpy + let contactWorkerSpy = ContactWorkerSpy(contactStore: ContactAPI()) + contactWorkerSpy.shouldReturnSuccess = false + interactor.worker = contactWorkerSpy + + // When + interactor.getForm() + + // Then + XCTAssert(contactWorkerSpy.getFormCalled, "ContactInteractor() should ask ContactWorker to fetch form") + XCTAssert(contactPresentationLogicSpy.presentErrorCalled, "ContactInteractor() should ask presenter to present error") + } + + + func testSendContactsFormShouldCallPresenterToShowSuccess() { + // Given + let contactPresentationLogicSpy = ContactPresentationLogicSpy() + interactor.presenter = contactPresentationLogicSpy + let contactWorkerSpy = ContactWorkerSpy(contactStore: ContactAPI()) + interactor.worker = contactWorkerSpy + + // When + let contactFormDataRequest = ContactFormDataRequest(name: "Teste", email: "test@test.com", phone: "(41) 99734-2345") + let request = Contact.Form.Request(sendFormData: contactFormDataRequest) + interactor.sendForm(data: request) + + let expectation = XCTestExpectation(description: "The interactor will sendForm and wait 3 seconds to call presenter") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + + // Then + XCTAssert(contactPresentationLogicSpy.presentSuccessCalled, "ContactInteractor() should ask presenter to show success") + } +} diff --git a/Santander/SantanderUnitTests/Scenes/Contact/ContactPresenterTests.swift b/Santander/SantanderUnitTests/Scenes/Contact/ContactPresenterTests.swift new file mode 100644 index 00000000..df23fb12 --- /dev/null +++ b/Santander/SantanderUnitTests/Scenes/Contact/ContactPresenterTests.swift @@ -0,0 +1,112 @@ +// +// ContactPresenterTests.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 22/08/19. +// + +import XCTest +@testable import Santander + +class ContactPresenterTests: XCTestCase { + + // MARK: - Interactor + var presenter: ContactPresenter! + + // MARK: - Test lifecycle + override func setUp() { + super.setUp() + setupPresenter() + } + + override func tearDown() { + presenter = nil + super.tearDown() + } + + // MARK: - Test setup + func setupPresenter() { + presenter = ContactPresenter() + } + + class ContactDisplayLogicSpy: ContactDisplayLogic { + + // MARK: Method call expectations + var displayFormCalled = false + var displayErrorCalled = false + var displaySuccessCalled = false + + // MARK: Argument expectations + var form: ContactForm! + var error: String! + + // MARK: Spied methods + func displayForm(_ form: ContactForm) { + displayFormCalled = true + self.form = form + } + + func displayError(_ error: String) { + displayErrorCalled = true + self.error = error + } + + func displaySuccess() { + displaySuccessCalled = true + } + } + + // MARK: - Tests + + func testPresenterPresentFormShouldAskViewControllerToDisplayForm() { + // Given + let contactDisplayLogicSpy = ContactDisplayLogicSpy() + presenter.viewController = contactDisplayLogicSpy + + // When + let cells = [TestData.ContactForm.emailCell] + let contactForm = ContactForm(cells: cells) + presenter.presentForm(contactForm) + + // Then + XCTAssert(contactDisplayLogicSpy.displayFormCalled, "Presenting fetched contact form should ask view controller to display them") + } + + func testPresenterPresentErrorShouldAskViewControllerToDisplayError() { + // Given + let contactDisplayLogicSpy = ContactDisplayLogicSpy() + presenter.viewController = contactDisplayLogicSpy + + // When + let error = TestError.contact + presenter.presentError(error) + + // Then + XCTAssert(contactDisplayLogicSpy.displayErrorCalled, "Presenting present error should ask view controller to display them") + } + + func testPresenterPresentSuccessShouldAskViewControllerToDisplaySuccess() { + // Given + let contactDisplayLogicSpy = ContactDisplayLogicSpy() + presenter.viewController = contactDisplayLogicSpy + + // When + presenter.presentSuccess() + + // Then + XCTAssert(contactDisplayLogicSpy.displaySuccessCalled, "Presenting present success should ask view controller to display them") + } + + func testPresentPresentErrorShouldFormatErrorForDisplay() { + // Given + let contactDisplayLogicSpy = ContactDisplayLogicSpy() + presenter.viewController = contactDisplayLogicSpy + + // When + let error = TestError.contact + presenter.presentError(error) + + // Then + XCTAssertEqual(contactDisplayLogicSpy.error, error.localizedDescription, "Presenting should properly format error") + } +} diff --git a/Santander/SantanderUnitTests/Scenes/Investment/InvestmentInteractorTests.swift b/Santander/SantanderUnitTests/Scenes/Investment/InvestmentInteractorTests.swift new file mode 100644 index 00000000..60ce0234 --- /dev/null +++ b/Santander/SantanderUnitTests/Scenes/Investment/InvestmentInteractorTests.swift @@ -0,0 +1,97 @@ +// +// InvestmentInteractorTests.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 23/08/19. +// + +import XCTest +@testable import Santander + +class InvestmentInteractorTests: XCTestCase { + + // MARK: - Interactor + var interactor: InvestmentInteractor! + + // MARK: - Test lifecycle + override func setUp() { + super.setUp() + setupIntercator() + } + + override func tearDown() { + interactor = nil + super.tearDown() + } + + // MARK: - Test setup + func setupIntercator() { + interactor = InvestmentInteractor() + } + + class InvestmentPresentationLogicSpy: InvestmentPresentationLogic { + + // MARK: Method call expectations + var presentScreenCalled = false + var presentErrorCalled = false + + // MARK: Spied methods + func presentScreen(response: Investment.Funds.Response) { + presentScreenCalled = true + } + + func presentError(_ error: Error) { + presentErrorCalled = true + } + } + + class InvestmentWorkerSpy: InvestmentWorker { + + var shouldReturnSuccess: Bool = true + + // MARK: Method call expectations + var getFundsCalled = false + + override func getFunds(result: @escaping (Result) -> Void) { + getFundsCalled = true + if shouldReturnSuccess { + let screen = TestData.Investment.screen + let response = Investment.Funds.Response(screen: screen) + result(.success(response)) + } else { + result(.failure(TestError.investment)) + } + } + } + + func testFetchContactsShouldAskContactsWorkerToGetFormAndPresenterToFormatSuccessResult() { + // Given + let investmentPresentationLogicSpy = InvestmentPresentationLogicSpy() + interactor.presenter = investmentPresentationLogicSpy + let investmentWorkerSpy = InvestmentWorkerSpy(investmentStore: InvestmentAPI()) + interactor.worker = investmentWorkerSpy + + // When + interactor.getFunds() + + // Then + XCTAssert(investmentWorkerSpy.getFundsCalled, "InvestmentInteractor() should ask InvestmentWorker to fetch funds") + XCTAssert(investmentPresentationLogicSpy.presentScreenCalled, "InvestmentInteractor() should ask presenter to present screen") + } + + func testFetchContactsShouldAskContactsWorkerToGetFormAndPresenterToFormatErrorResult() { + // Given + let investmentPresentationLogicSpy = InvestmentPresentationLogicSpy() + interactor.presenter = investmentPresentationLogicSpy + let investmentWorkerSpy = InvestmentWorkerSpy(investmentStore: InvestmentAPI()) + investmentWorkerSpy.shouldReturnSuccess = false + interactor.worker = investmentWorkerSpy + + // When + interactor.getFunds() + + // Then + XCTAssert(investmentWorkerSpy.getFundsCalled, "InvestmentInteractor() should ask InvestmentWorker to fetch funds") + XCTAssert(investmentPresentationLogicSpy.presentErrorCalled, "InvestmentInteractor() should ask presenter to present error") + } +} diff --git a/Santander/SantanderUnitTests/Scenes/Investment/InvestmentPresenterTests.swift b/Santander/SantanderUnitTests/Scenes/Investment/InvestmentPresenterTests.swift new file mode 100644 index 00000000..022c1649 --- /dev/null +++ b/Santander/SantanderUnitTests/Scenes/Investment/InvestmentPresenterTests.swift @@ -0,0 +1,94 @@ +// +// InvestmentPresenterTests.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 23/08/19. +// + +import XCTest +@testable import Santander + +class InvestmentPresenterTests: XCTestCase { + + // MARK: - Interactor + var presenter: InvestmentPresenter! + + // MARK: - Test lifecycle + override func setUp() { + super.setUp() + setupPresenter() + } + + override func tearDown() { + presenter = nil + super.tearDown() + } + + // MARK: - Test setup + func setupPresenter() { + presenter = InvestmentPresenter() + } + + class InvestmentDisplayLogicSpy: InvestmentDisplayLogic { + + // MARK: Method call expectations + var displayScreenCalled = false + var displayErrorCalled = false + + // MARK: Argument expectations + var viewModel: Investment.Funds.ViewModel! + var error: String! + + // MARK: Spied methods + func displayScreen(viewModel: Investment.Funds.ViewModel) { + displayScreenCalled = true + self.viewModel = viewModel + } + + func displayError(_ error: String) { + displayErrorCalled = true + self.error = error + } + } + + // MARK: - Tests + + func testPresenterPresentFormShouldAskViewControllerToDisplayForm() { + // Given + let investmentDisplayLogicSpy = InvestmentDisplayLogicSpy() + presenter.viewController = investmentDisplayLogicSpy + + // When + let screen = TestData.Investment.screen + let response = Investment.Funds.Response(screen: screen) + presenter.presentScreen(response: response) + + // Then + XCTAssert(investmentDisplayLogicSpy.displayScreenCalled, "Presenting fetched screen should ask view controller to display them") + } + + func testPresenterPresentErrorShouldAskViewControllerToDisplayError() { + // Given + let investmentDisplayLogicSpy = InvestmentDisplayLogicSpy() + presenter.viewController = investmentDisplayLogicSpy + + // When + presenter.presentError(TestError.investment) + + // Then + XCTAssert(investmentDisplayLogicSpy.displayErrorCalled, "Presenting present error should ask view controller to display them") + } + + func testPresentPresentErrorShouldFormatErrorForDisplay() { + // Given + let investmentDisplayLogicSpy = InvestmentDisplayLogicSpy() + presenter.viewController = investmentDisplayLogicSpy + + // When + let error = TestError.investment + presenter.presentError(error) + + // Then + XCTAssertEqual(investmentDisplayLogicSpy.error, error.localizedDescription, "Presenting should properly format error") + } +} diff --git a/Santander/SantanderUnitTests/TestData/TestData.swift b/Santander/SantanderUnitTests/TestData/TestData.swift new file mode 100644 index 00000000..ccc4975b --- /dev/null +++ b/Santander/SantanderUnitTests/TestData/TestData.swift @@ -0,0 +1,46 @@ +// +// TestData.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 22/08/19. +// + +import Foundation +@testable import Santander + +struct TestData { + struct ContactForm { + static let emailCell = FormCell(id: 2, type: .field, message: "Email", fieldType: .email, isHidden: true, topSpacing: 35.0, fieldToPresent: nil, isRequired: true) + } + + struct Investment { + static let moreInfo = FundsScreen.MoreInfo(month: FundsScreen.MoreInfo.Percentages(fund: 0.3, cdi: 0.3), + year: FundsScreen.MoreInfo.Percentages(fund: 13.01, cdi: 12.08), + twelveMonths: FundsScreen.MoreInfo.Percentages(fund: 17.9, cdi: 17.6)) + + static let info = [FundsScreen.Info(name: "Taxa de administração", data: "0,50%"), + FundsScreen.Info(name: "Aplicação inicial", data: "R$ 10.000,00"), + FundsScreen.Info(name: "Movimentação mínima", data: "R$ 1.000,00"), + FundsScreen.Info(name: "Saldo mínimo", data: "R$ 5.000,00"), + FundsScreen.Info(name: "Resgate (valor bruto)", data: "D+0"), + FundsScreen.Info(name: "Cota (valor bruto)", data: "D+1"), + FundsScreen.Info(name: "Pagamento (valor bruto)", data: "D+2")] + + static let downInfo = [FundsScreen.DownInfo(name: "Essenciais", data: nil), + FundsScreen.DownInfo(name: "Desempenho", data: nil), + FundsScreen.DownInfo(name: "Complementares", data: nil), + FundsScreen.DownInfo(name: "Regulamento", data: nil), + FundsScreen.DownInfo(name: "Adesão", data: nil)] + + static let screen = FundsScreen(title: "Fundos de investimento", + fundName: "Vinci Valorem FI Multimercado", + whatIs: "O que é?", + definition: "O Fundo tem por objetivo proporcionar aos seus cotistas rentabilidade no longo prazo através de investimentos.", + riskTitle: "Grau de risco do investimento", + risk: .four, + infoTitle: "Mais informações sobre o investimento", + moreInfo: moreInfo, + info: info, + downInfo: downInfo) + } +} diff --git a/Santander/SantanderUnitTests/TestError/TestError.swift b/Santander/SantanderUnitTests/TestError/TestError.swift new file mode 100644 index 00000000..772c7748 --- /dev/null +++ b/Santander/SantanderUnitTests/TestError/TestError.swift @@ -0,0 +1,23 @@ +// +// TestError.swift +// SantanderUnitTests +// +// Created by Orlando Amorim on 23/08/19. +// + +import Foundation + +enum TestError: Error, LocalizedError { + + case contact + case investment + + public var errorDescription: String? { + switch self { + case .contact: + return NSLocalizedString("Contact Error", comment: "Contact Error") + case .investment: + return NSLocalizedString("Investment Error", comment: "Investment Error") + } + } +} diff --git a/Santander/bootstrap.sh b/Santander/bootstrap.sh new file mode 100644 index 00000000..842446e5 --- /dev/null +++ b/Santander/bootstrap.sh @@ -0,0 +1,11 @@ +if [ "$1" == "--install-bundler" ]; then + bundle install +fi + +if [ "$1" == "--force" ]; then + rm -rf "${HOME}/Library/Caches/CocoaPods" + rm -rf "`pwd`/Pods/" +fi + +xcodegen +bundle exec pod install \ No newline at end of file diff --git a/Santander/project.yml b/Santander/project.yml new file mode 100644 index 00000000..749fe611 --- /dev/null +++ b/Santander/project.yml @@ -0,0 +1,25 @@ +name: Santander +options: + bundleIdPrefix: br.com.santander +targets: + Santander: + type: application + platform: iOS + deploymentTarget: 9.0 + sources: Santander + scheme: + environmentVariables: + OS_ACTIVITY_MODE: disable + gatherCoverageData: true + testTargets: + - SantanderUnitTests + settings: + SWIFT_VERSION: 5.0 + TARGETED_DEVICE_FAMILY: 1 + SantanderUnitTests: + type: bundle.unit-test + platform: iOS + sources: SantanderUnitTests + settings: + SWIFT_VERSION: 5.0 + TEST_HOST: $(BUILT_PRODUCTS_DIR)/Santander.app/Santander \ No newline at end of file diff --git a/teste_app.sketch b/teste_app.sketch index 1047c7cb..4b9df99d 100644 Binary files a/teste_app.sketch and b/teste_app.sketch differ