├── .gitignore ├── README.md ├── apiary.apib └── iOS Bootstrap ├── .swiftlint.yml ├── Podfile ├── Podfile.lock ├── iOS Bootstrap Tests ├── DefaultTestCase.swift ├── Info.plist └── TestSomething.swift ├── iOS Bootstrap.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── iOS Bootstrap.xcscheme ├── iOS Bootstrap.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── iOS Bootstrap ├── Convenience Extensions ├── Alamofire+Promise.swift ├── NSDate+TimeAgo.swift ├── PromisesExtensios.swift ├── String+Localized.swift ├── UIRefreshControl+ProgramaticallyBeginRefresh.swift ├── UITableViewCell+Reusable.swift ├── UIViewController+HUD.swift ├── UIViewController+Storyboard.swift └── UIViewController+Toastr.swift ├── Convenience Protocols ├── NibFileOwnerLoadable.swift ├── OnlineOfflineProtocols.swift └── ViewModelProtocol.swift ├── Models ├── Article+CoreDataClass.swift ├── Article+CoreDataProperties.swift ├── Tag+CoreDataClass.swift ├── Tag+CoreDataProperties.swift └── iOS_Bootstrap.xcdatamodeld │ ├── .xccurrentversion │ └── CKL_iOS_Challenge.xcdatamodel │ └── contents ├── Resources ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── iTunesArtwork@2x.png │ ├── Contents.json │ ├── icon_filter.imageset │ │ ├── Contents.json │ │ ├── icon_filter_48.png │ │ └── icon_filter_72-1.png │ ├── img_placeholder.imageset │ │ ├── Contents.json │ │ └── img_placeholder.png │ └── lunch_icon.imageset │ │ ├── Contents.json │ │ └── launch_icon.png ├── Constants.swift ├── Error Handling │ ├── APIErrorHandling.swift │ ├── Logger.swift │ └── UIViewController+ErrorHandling.swift ├── Localizable.strings └── Theme │ ├── Fonts │ ├── Colfax │ │ ├── Colfax-Bold.ttf │ │ ├── Colfax-Medium.ttf │ │ └── Colfax-Regular.ttf │ ├── ProximaNova │ │ ├── ProximaNova-Black.ttf │ │ ├── ProximaNova-Bold-Italic.ttf │ │ ├── ProximaNova-Bold.ttf │ │ ├── ProximaNova-Extra-Bold.ttf │ │ ├── ProximaNova-Light-Italic.ttf │ │ ├── ProximaNova-Light.ttf │ │ ├── ProximaNova-Regular-Italic.ttf │ │ ├── ProximaNova-Regular.ttf │ │ ├── ProximaNova-Semibold-Italic.ttf │ │ └── ProximaNova-Semibold.ttf │ └── UIFont+Custom.swift │ └── UIColor+Custom.swift ├── Scenes ├── ApplicationCoordinator.swift ├── Auth │ ├── Auth Home │ │ ├── AuthHomeCoordinator.swift │ │ └── AuthHomeViewController.swift │ ├── Auth.storyboard │ ├── Forgot Password │ │ ├── ForgotPasswordCoordinator.swift │ │ ├── ForgotPasswordViewController.swift │ │ └── ForgotPasswordViewModel.swift │ ├── Login │ │ ├── LoginCoordinator.swift │ │ ├── LoginViewController.swift │ │ └── LoginViewModel.swift │ └── SignUp │ │ ├── SignUpCoordinator.swift │ │ ├── SignUpViewController.swift │ │ └── SignUpViewModel.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Coordinator.swift ├── News │ ├── Base.lproj │ │ └── Table.storyboard │ ├── News Collection │ │ ├── Cells (First Level) │ │ │ ├── Cells (Second Level) │ │ │ │ └── NewsSecondLevelCollectionViewCell.swift │ │ │ ├── InnerCollectionViewModel.swift │ │ │ └── NewsFirstLevelCollectionViewCell.swift │ │ ├── Collection.storyboard │ │ ├── NewsCollectionCoordinator.swift │ │ ├── NewsCollectionViewController.swift │ │ └── NewsCollectionViewModel.swift │ ├── News Detail │ │ ├── NewsDetailCoordinator.swift │ │ ├── NewsDetailViewController.swift │ │ └── NewsDetailViewModel.swift │ ├── News Table │ │ ├── Cells │ │ │ └── NewsTableViewCell.swift │ │ ├── NewsTableCoordinator.swift │ │ ├── NewsTableViewController.swift │ │ └── NewsTableViewModel.swift │ ├── NewsProtocols.swift │ └── en.lproj │ │ └── Table.strings ├── Views │ ├── Custom Views │ │ ├── NewsCardView.swift │ │ └── NewsCardView.xib │ ├── IBDesignable │ │ ├── DesignableButton.swift │ │ ├── DesignableLabel.swift │ │ ├── DesignableTextField.swift │ │ └── PaddingLabel.swift │ └── View Extensions │ │ └── UIView+Animation.swift └── Welcome │ ├── Welcome.storyboard │ ├── WelcomeCoordinator.swift │ ├── WelcomeTableViewCell.swift │ ├── WelcomeViewController.swift │ └── WelcomeViewModel.swift ├── Services └── APIService.swift └── Supporting Files ├── AppDelegate.swift ├── Info.plist └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2bdb7d141ace469dbe74e362ca150d68)](https://www.codacy.com/app/marcelosalloum/iOS-Bootstrap?utm_source=github.com&utm_medium=referral&utm_content=marcelosalloum/iOS-Bootstrap&utm_campaign=Badge_Grade) 2 | 3 | # iOS-Bootstrap 4 | 5 | This app contains a set of pre-build common structures so serve as a search base. In other words, the idea is to use this structure as a basis for my/your new features and project structure. 6 | 7 | ## Project Requirements 8 | 9 | * iOS Min Version: **10.0** 10 | * Swift version: **Swift 4.2** 11 | * Cocoapods Version: **1.5.3** 12 | 13 | ## Pods Versions 14 | 15 | | Pod | Version | Description | 16 | |:------ |:----------- |:----------- | 17 | | Alamofire | **4.7.3** | Deals with API requests | 18 | | Bagel | **1.3.2** | Helps debugging network requests | 19 | | Crashlytics | **3.10.8** | Crash reports | 20 | | [EZCoreData](https://github.com/CheesecakeLabs/EZCoreData) | **0.7.0** | Deals with Core Data and concurrent core data requests | 21 | | Fabric | **1.7.12** | Beta distribution | 22 | | Flurry-SDK/FlurrySDK | **9.1.0** | Analytics | 23 | | Kingfisher | **4.10.0** | Image caching | 24 | | SwiftMessages | **6.0.0** | Display Status Bar Messages | 25 | | SVProgressHUD | **2.2.5** |HUD: Head-Up Display | 26 | | PromiseKit | **6.8.1** |Promises impleemntation, used to improve callback readability | 27 | 28 | ## Project Structure 29 | 30 | I'm using a few things I consider vital to an iOS project. A more in-depth explanation can be found in the items below: 31 | 32 | ### Views 33 | 34 | * **Views Interface**: the interface was mainly built from storyboards. Since I currently hav more experience with code built interfaces, I decided to challenge myself with storyboard this time, to make sure I know well of both approaches. 35 | * **Reusable Views**: The project uses reusable Xibs that are actually drawn in the interface builder through `IBDesignable`. 36 | * **IBInspectable**: there are a few designable views that are not being highly used at te moment, but are very customizable 37 | 38 | ### API 39 | 40 | At the moment, the app is using a very simple API that is defined within a struct. It uses Alamofire and the async callbacks use clojures. A must-do in the short future is start using Promises, probably the [PromiseKit](https://github.com/mxcl/PromiseKit) library. 41 | 42 | The API is actually a mocked APIary, which doesn't have authentication in place. 43 | 44 | The API calls implement a layer on top of Alamofire, which is a good practice and better explained [here](https://mecid.github.io/2019/02/13/hiding-third-party-dependencies-with-protocols-and-extensions/). 45 | 46 | ### Persistence 47 | 48 | For persistence the project currently uses the cocapod [EZCoreData](https://github.com/CheesecakeLabs/EZCoreData), which is a pod authored by me in my persuit to better understand Core Data for iOS 10+. In the future I consider to migrate EZCoreData to use PromiseKit. 49 | 50 | As a rule of thumb, I firsly use CoreData to show the locally stored data before updating it from the backend. A step-by-step explanation: 51 | 52 | 1. A user opens a screen (a table view or a colection view for instance) 53 | 2. The app first shows any available data that is already stored in core data 54 | 3. Meanwhile, the app performs an API request to update the data with the API response 55 | 4. The retrieved data is then stored in the core data and any outdated data is removed 56 | 5. Finally, the app displays the most recently updated data from the backend to the user 57 | 58 | ### App Architecture: MVVM-C 59 | 60 | The project makes use of MVVM-C (Model–View–ViewModel Coordinator). It basically has all advantages of classic MVVM and adds a layer for dependency injection and flow coordination, which is the Coordinator. 61 | 62 | I'm particularly a big fan of raw MVVM and I'm finding great advantages of using the Coordinator as well, such as having simpler ViewControllers that con't know where they are in terms of application flow. This way it's easier to reuse ViewControllers throughout the application. 63 | 64 | If you don't use Cordinators, you're probabbly placing some of that kind of code in a custom UINavigationController or a base UIViewController. I find coordinators a more elegant solution though. 65 | 66 | There are a few posts about the subject, some [over complicated](https://medium.com/sudo-by-icalia-labs/ios-architecture-mvvm-c-introduction-1-6-815204248518) and others [over simplified](https://tech.trivago.com/2016/08/26/mvvm-c-a-simple-way-to-navigate/). It's definetly not a must-go rhitectures, but it's a nice way to split your application, in my opinion. 67 | 68 | ### Localization 69 | 70 | The structure for Localization is currently in place making use only of a base language (which is in english). It can be easily replicated to other languages, since the structure is already in place, like in the following piece of code: 71 | ```Swift 72 | self.title = "NEWS".localized 73 | ``` 74 | 75 | ### Constants 76 | 77 | All constants are placed in a dedicated struct to avoid using literals. You could call the database name constant by invoking: 78 | ```Swift 79 | let databaseName = Constants.databaseName 80 | ``` 81 | 82 | ### Custom Fonts 83 | 84 | There is a class declaring custom fonts using class/static methods, that instantiate the fonts nams using a string-struct instead of String literals. That is a very common setup to be used in any kind if project that uses custom fonts. You can call a cusstom font as easy as: 85 | ```Swift 86 | self.font = UIFont.proximaNovaRegular(size: 16) 87 | ``` 88 | 89 | ### Custom Colors 90 | 91 | There is a class declaring custom colors using computed properties (similar to `UIColor.blue`, for instance). This is too a very common setup to be used in any kind if project. Check out an example below: 92 | ```Swift 93 | self.backgroundColor = .gray5 94 | ``` 95 | 96 | ### Crash Tracking 97 | 98 | Crashlytics is configured in the project but the keyy is actually public, which is a bad practice. 99 | 100 | ### User Interaction Tracking 101 | 102 | Flurry is configured in the project but the keyy is actually public, which is a bad practice. 103 | 104 | ### Continuous Integration (CI) 105 | 106 | The only CI tool this project is currently using is Codacy's Code Quality. It's not meant for building, testing or delivering, just for measuring the overall code quality of the project. 107 | 108 | ### Unit Tests 109 | 110 | Currently, that part is still missing in this project but I've made a setup to make sure the AppDelegate won't be loaded for the Unit Tests, which makes your environment easier to control. 111 | 112 | If you'd like to check how I like o do Unit Tests, have a look on my Core Data lib: [EZCoreData](https://github.com/CheesecakeLabs/EZCoreData). 113 | 114 | ### TO-DO 115 | 116 | * Add a few Unit tests and UI tests. 117 | * Put on an implementation of `Codable` that makes sense for my current `NSManagedObject` models 118 | 119 | ## Interesting Links 120 | 121 | * Generate all icon sizes from one source image: https://appicon.co/ 122 | * [Unit Test]: [Faking App Delegate](https://marcosantadev.com/fake-appdelegate-unit-testing-swift/) 123 | * [Unit Test]: [Preparing InMemory Persistent Store](https://medium.com/flawless-app-stories/cracking-the-tests-for-core-data-15ef893a3fee) to avoid messing with production data 124 | * [Hiding third-party dependencies with protocols and extensions](https://mecid.github.io/2019/02/13/hiding-third-party-dependencies-with-protocols-and-extensions/) 125 | -------------------------------------------------------------------------------- /apiary.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://polls.apiblueprint.org/ 3 | 4 | # ckl-challenge 5 | 6 | This is a simple API allowing consumers to view articles. 7 | 8 | ## Questions Collection [/article] 9 | 10 | ### List All Articles [GET] 11 | 12 | + Response 200 (application/json) 13 | 14 | [ 15 | { 16 | "id": 1, 17 | "title":"Obama Offers Hopeful Vision While Noting Nation's Fears", 18 | "website":"MacStories", 19 | "authors":"Graham Spencer", 20 | "date":"05/26/2014", 21 | "content":"In his last State of the Union address, President Obama sought to paint a hopeful portrait. But he acknowledged that many Americans felt shut out of a political and economic system they view as rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. In his last State of the Union address, President Obama sought to paint a hopeful portrait. But he acknowledged that many Americans felt shut out of a political and economic system they view as rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. In his last State of the Union address, President Obama sought to paint a hopeful portrait. But he acknowledged that many Americans felt shut out of a political and economic system they view as rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. In his last State of the Union address, President Obama sought to paint a hopeful portrait. But he acknowledged that many Americans felt shut out of a political and economic system they view as rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. In his last State of the Union address, President Obama sought to paint a hopeful portrait. But he acknowledged that many Americans felt shut out of a political and economic system they view as rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged.rigged.rigged.rigged.rigged.rigged.rigged.rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged. rigged.", 22 | "tags":[ 23 | { 24 | "id":1, 25 | "label":"Politics" 26 | } 27 | ], 28 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488993901/challenge/news_01_illh01.jpg" 29 | }, 30 | { 31 | "id": 2, 32 | "title":"Didi Kuaidi, The Company Beating Uber In China, Opens Its API To Third Party Apps", 33 | "website":"Masslive", 34 | "authors":"Fran Bellamy", 35 | "date":"05/27/2014", 36 | "content":"One day after Uber updated its API to add 'content experiences' for passengers, the U.S. company’s biggest rival — Didi Kuaidi in China — has opened its own platform up by releasing an SDK for developers and third-parties.", 37 | "tags":[ 38 | { 39 | "id":2, 40 | "label":"Tech" 41 | } 42 | ], 43 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488993901/challenge/news_02_ulyqvw.jpg" 44 | }, 45 | { 46 | "id": 3, 47 | "title":"NASA Formalizes Efforts To Protect Earth From Asteroids", 48 | "website":"International Business Times - Australia", 49 | "authors":"Alexandre Henrique Shailesh Zeta-Jones", 50 | "date":"05/28/2014", 51 | "content":"Last week, NASA announced a new program called the Planetary Defense Coordination Office (PDCO) which will coordinate NASA’s efforts for detecting and tracking near-Earth objects (NEOs). If a large object comes hurtling toward our planet… ", 52 | "tags":[ 53 | { 54 | "id":3, 55 | "label":"Science" 56 | } 57 | ], 58 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488993901/challenge/news_03_ocz3gy.jpg" 59 | }, 60 | { 61 | "id": 4, 62 | "title":"For Some Atlanta Hawks, a Revved-Up Game of Uno Is Diversion No. 1", 63 | "website":"Into Mobile", 64 | "authors":"Dusan Belic", 65 | "date":"05/26/2014", 66 | "content":"The favored in-flight pastime of a group of players including Al Horford, Kent Bazemore and Dennis Schroder is a schoolchildren’s card game with some added twists.", 67 | "tags":[ 68 | { 69 | "id":4, 70 | "label":"Sports" 71 | } 72 | ], 73 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488994455/challenge/news_04.jpg" 74 | }, 75 | { 76 | "id": 5, 77 | "title":"Picking a Windows 10 Security Package", 78 | "website":"CNN", 79 | "authors":"Harmeet Shah Singh", 80 | "date":"04/02/2014", 81 | "content":"Oscar the Grouch has a recycling bin and Big Bird has moved to a tree as the children’s classic debuts on HBO, aiming at a generation that doesn’t distinguish between TV and mobile screens.", 82 | "tags":[ 83 | { 84 | "id":2, 85 | "label":"Tech" 86 | } 87 | ], 88 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488994454/challenge/news_05.jpg" 89 | }, 90 | { 91 | "id": 6, 92 | "title":"As U.S. Modernizes Nuclear Weapons, 'Smaller' Leaves Some Uneasy", 93 | "website":"iMore", 94 | "authors":"Rene Ritchie", 95 | "date":"05/26/2014", 96 | "content":"The Energy Department and the Pentagon have been readying a weapon with a build-it-smaller approach, setting off a philosophical clash in the world of nuclear arms.", 97 | "tags":[ 98 | { 99 | "id":3, 100 | "label":"Science" 101 | } 102 | ], 103 | "image_url":"https://res.cloudinary.com/cheesecakelabs/image/upload/v1488994454/challenge/news_06.png" 104 | } 105 | ] -------------------------------------------------------------------------------- /iOS Bootstrap/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: # paths to ignore during linting. Takes precedence over `included`. 2 | - Carthage 3 | - Pods 4 | 5 | disabled_rules: # rule identifiers to exclude from running 6 | - identifier_name 7 | - force_cast -------------------------------------------------------------------------------- /iOS Bootstrap/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | target 'iOS Bootstrap' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Makes REST requests: 9 | pod 'Alamofire', '4.7.3' # Deals with API requests 10 | pod 'Bagel', '~> 1.3.2', :configurations => ['Debug'] # Debugging network requests 11 | pod 'Crashlytics', '3.10.8' # Crash reports 12 | pod 'EZCoreData', '0.7.0' # Deals with Core Data 13 | pod 'Fabric', '1.7.12' # Beta distribution 14 | pod 'Flurry-iOS-SDK/FlurrySDK', '9.1.0' # Analytics 15 | pod 'Kingfisher', '4.10.0' # Image caching 16 | pod 'PromiseKit', '6.8.1' # Promises implementation, used to improve callback 17 | # readability and code decoupling 18 | pod 'SVProgressHUD', '2.2.5' # HUD: Head-Up Display 19 | end 20 | -------------------------------------------------------------------------------- /iOS Bootstrap/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.7.3) 3 | - Bagel (1.3.2): 4 | - CocoaAsyncSocket 5 | - CocoaAsyncSocket (7.6.3) 6 | - Crashlytics (3.10.8): 7 | - Fabric (~> 1.7.12) 8 | - EZCoreData (0.7.0) 9 | - Fabric (1.7.12) 10 | - Flurry-iOS-SDK/FlurrySDK (9.1.0) 11 | - Kingfisher (4.10.0) 12 | - PromiseKit (6.8.1): 13 | - PromiseKit/CorePromise (= 6.8.1) 14 | - PromiseKit/Foundation (= 6.8.1) 15 | - PromiseKit/UIKit (= 6.8.1) 16 | - PromiseKit/CorePromise (6.8.1) 17 | - PromiseKit/Foundation (6.8.1): 18 | - PromiseKit/CorePromise 19 | - PromiseKit/UIKit (6.8.1): 20 | - PromiseKit/CorePromise 21 | - SVProgressHUD (2.2.5) 22 | 23 | DEPENDENCIES: 24 | - Alamofire (= 4.7.3) 25 | - Bagel (~> 1.3.2) 26 | - Crashlytics (= 3.10.8) 27 | - EZCoreData (= 0.7.0) 28 | - Fabric (= 1.7.12) 29 | - Flurry-iOS-SDK/FlurrySDK (= 9.1.0) 30 | - Kingfisher (= 4.10.0) 31 | - PromiseKit (= 6.8.1) 32 | - SVProgressHUD (= 2.2.5) 33 | 34 | SPEC REPOS: 35 | https://github.com/cocoapods/specs.git: 36 | - Alamofire 37 | - Bagel 38 | - CocoaAsyncSocket 39 | - Crashlytics 40 | - EZCoreData 41 | - Fabric 42 | - Flurry-iOS-SDK 43 | - Kingfisher 44 | - PromiseKit 45 | - SVProgressHUD 46 | 47 | SPEC CHECKSUMS: 48 | Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 49 | Bagel: 199e0105ac694d6eee9db54dd006c940da6ce81b 50 | CocoaAsyncSocket: eafaa68a7e0ec99ead0a7b35015e0bf25d2c8987 51 | Crashlytics: 058dc1ba579f6c100bc60a6878703779bff383c4 52 | EZCoreData: 4bcb839fdf3efd9edb191b5a1a555d84a4cf434d 53 | Fabric: d7387db9a31aadff63b147a4b91d982c53afaa4f 54 | Flurry-iOS-SDK: 5f321ecc8282cf10b051b64bfdc4657f6b280118 55 | Kingfisher: 43c4b802d8b5256cf1f4379e9cd10b329be6d3e2 56 | PromiseKit: cd5df8e6553887c93cc009d1260cdf6f7efbcbbd 57 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 58 | 59 | PODFILE CHECKSUM: c92b13b1252b1129611a6050be7ce64510b0af31 60 | 61 | COCOAPODS: 1.5.3 62 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap Tests/DefaultTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOS_Bootstrap_Tests.swift 3 | // iOS Bootstrap Tests 4 | // 5 | // Created by Marcelo Salloum dos Santos on 24/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | @testable import iOS_Bootstrap 12 | @testable import EZCoreData 13 | 14 | // MARK: - Mocking Core Data: 15 | class DefaultTestCase: XCTestCase { 16 | 17 | override func setUp() { 18 | EZCoreData.shared.setupInMemoryPersistence("Model") 19 | // eraseAllArticles() 20 | // importAllArticles() 21 | } 22 | 23 | var context: NSManagedObjectContext { 24 | return EZCoreData.mainThreadContext 25 | } 26 | 27 | var backgroundContext: NSManagedObjectContext { 28 | return EZCoreData.privateThreadContext 29 | } 30 | 31 | // public func eraseAllArticles() { 32 | // try? Article.deleteAll(context: context) 33 | // let countZero = try? Article.count(context: context) 34 | // XCTAssertEqual(countZero, 0) 35 | // } 36 | // 37 | // public func importAllArticles() { 38 | // _ = try? Article.importList(mockArticleListResponseJSON, idKey: "id", shouldSave: true, context: context) 39 | // let countSix = try? Article.count(context: context) 40 | // XCTAssertEqual(countSix, mockArticleListResponseJSON.count) 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap Tests/TestSomething.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestSomething.swift 3 | // iOS Bootstrap Tests 4 | // 5 | // Created by Marcelo Salloum dos Santos on 18/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class TestSomething: DefaultTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap.xcodeproj/xcshareddata/xcschemes/iOS Bootstrap.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/Alamofire+Promise.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alamofire+Promise.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 18/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | @_exported import Alamofire 10 | import Foundation 11 | #if !PMKCocoaPods 12 | import PromiseKit 13 | #endif 14 | 15 | /** 16 | To import the `Alamofire` category: 17 | 18 | use_frameworks! 19 | pod "PromiseKit/Alamofire" 20 | 21 | And then in your sources: 22 | 23 | import PromiseKit 24 | */ 25 | extension Alamofire.DataRequest { 26 | /// Adds a handler to be called once the request has finished. 27 | public func response(_: PMKNamespacer, queue: DispatchQueue? = nil) 28 | -> Promise<(URLRequest, HTTPURLResponse, Data)> { 29 | return Promise { seal in 30 | response(queue: queue) { rsp in 31 | if let error = rsp.error { 32 | seal.reject(error) 33 | } else if let a = rsp.request, let b = rsp.response, let c = rsp.data { 34 | seal.fulfill((a, b, c)) 35 | } else { 36 | seal.reject(PMKError.invalidCallingConvention) 37 | } 38 | } 39 | } 40 | } 41 | 42 | /// Adds a handler to be called once the request has finished. 43 | public func responseData(queue: DispatchQueue? = nil) -> Promise<(data: Data, response: PMKAlamofireDataResponse)> { 44 | return Promise { seal in 45 | responseData(queue: queue) { response in 46 | switch response.result { 47 | case .success(let value): 48 | seal.fulfill((value, PMKAlamofireDataResponse(response))) 49 | case .failure(let error): 50 | seal.reject(error) 51 | } 52 | } 53 | } 54 | } 55 | 56 | /// Adds a handler to be called once the request has finished. 57 | public func responseString(queue: DispatchQueue? = nil) -> 58 | Promise<(string: String, response: PMKAlamofireDataResponse)> { 59 | return Promise { seal in 60 | responseString(queue: queue) { response in 61 | switch response.result { 62 | case .success(let value): 63 | seal.fulfill((value, PMKAlamofireDataResponse(response))) 64 | case .failure(let error): 65 | seal.reject(error) 66 | } 67 | } 68 | } 69 | } 70 | 71 | /// Adds a handler to be called once the request has finished. 72 | public func responseJSON(queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments) 73 | -> Promise<(json: Any, response: PMKAlamofireDataResponse)> { 74 | return Promise { seal in 75 | responseJSON(queue: queue, options: options) { response in 76 | switch response.result { 77 | case .success(let value): 78 | seal.fulfill((value, PMKAlamofireDataResponse(response))) 79 | case .failure(let error): 80 | seal.reject(error) 81 | } 82 | } 83 | } 84 | } 85 | 86 | /// Adds a handler to be called once the request has finished. 87 | public func responsePropertyList(queue: DispatchQueue? = nil, 88 | options: PropertyListSerialization.ReadOptions 89 | = PropertyListSerialization.ReadOptions()) 90 | -> Promise<(plist: Any, response: PMKAlamofireDataResponse)> { 91 | return Promise { seal in 92 | responsePropertyList(queue: queue, options: options) { response in 93 | switch response.result { 94 | case .success(let value): 95 | seal.fulfill((value, PMKAlamofireDataResponse(response))) 96 | case .failure(let error): 97 | seal.reject(error) 98 | } 99 | } 100 | } 101 | } 102 | 103 | #if swift(>=3.2) 104 | /** 105 | Returns a Promise for a Decodable 106 | Adds a handler to be called once the request has finished. 107 | 108 | - Parameter queue: DispatchQueue, by default nil 109 | - Parameter decoder: JSONDecoder, by default JSONDecoder() 110 | */ 111 | public func responseDecodable(queue: DispatchQueue? = nil, 112 | decoder: JSONDecoder = JSONDecoder()) -> Promise { 113 | return Promise { seal in 114 | responseData(queue: queue) { response in 115 | switch response.result { 116 | case .success(let value): 117 | do { 118 | seal.fulfill(try decoder.decode(T.self, from: value)) 119 | } catch { 120 | seal.reject(error) 121 | } 122 | case .failure(let error): 123 | seal.reject(error) 124 | } 125 | } 126 | } 127 | } 128 | 129 | /** 130 | Returns a Promise for a Decodable 131 | Adds a handler to be called once the request has finished. 132 | 133 | - Parameter queue: DispatchQueue, by default nil 134 | - Parameter decoder: JSONDecoder, by default JSONDecoder() 135 | */ 136 | public func responseDecodable(_ type: T.Type, 137 | queue: DispatchQueue? = nil, 138 | decoder: JSONDecoder = JSONDecoder()) -> Promise { 139 | return Promise { seal in 140 | responseData(queue: queue) { response in 141 | switch response.result { 142 | case .success(let value): 143 | do { 144 | seal.fulfill(try decoder.decode(type, from: value)) 145 | } catch { 146 | seal.reject(error) 147 | } 148 | case .failure(let error): 149 | seal.reject(error) 150 | } 151 | } 152 | } 153 | } 154 | #endif 155 | } 156 | 157 | extension Alamofire.DownloadRequest { 158 | public func response(_: PMKNamespacer, queue: DispatchQueue? = nil) -> Promise { 159 | return Promise { seal in 160 | response(queue: queue) { response in 161 | if let error = response.error { 162 | seal.reject(error) 163 | } else { 164 | seal.fulfill(response) 165 | } 166 | } 167 | } 168 | } 169 | 170 | /// Adds a handler to be called once the request has finished. 171 | public func responseData(queue: DispatchQueue? = nil) -> Promise> { 172 | return Promise { seal in 173 | responseData(queue: queue) { response in 174 | switch response.result { 175 | case .success: 176 | seal.fulfill(response) 177 | case .failure(let error): 178 | seal.reject(error) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | /// Alamofire.DataResponse, but without the `result`, since the Promise represents the `Result` 186 | public struct PMKAlamofireDataResponse { 187 | public init(_ rawrsp: Alamofire.DataResponse) { 188 | request = rawrsp.request 189 | response = rawrsp.response 190 | data = rawrsp.data 191 | timeline = rawrsp.timeline 192 | } 193 | 194 | /// The URL request sent to the server. 195 | public let request: URLRequest? 196 | 197 | /// The server's response to the URL request. 198 | public let response: HTTPURLResponse? 199 | 200 | /// The data returned by the server. 201 | public let data: Data? 202 | 203 | /// The timeline of the complete lifecycle of the request. 204 | public let timeline: Timeline 205 | } 206 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/NSDate+TimeAgo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+TimeAgo.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 09/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSDate { 12 | 13 | static public func timeAgoSince(_ date: NSDate?, shortPattern: Bool = false) -> String? { 14 | guard let date = date as Date? else { return nil } 15 | return timeAgoSince(date, shortPattern: shortPattern) 16 | } 17 | 18 | static public func timeAgoSince(_ date: Date?, shortPattern: Bool = false) -> String? { 19 | 20 | guard let date = date else { return nil } 21 | let calendar = Calendar.current 22 | let now = Date() 23 | let unitFlags: NSCalendar.Unit = [.second, .minute, .hour, .day, .weekOfYear, .month, .year] 24 | let components = (calendar as NSCalendar).components(unitFlags, from: date, to: now, options: []) 25 | 26 | if let year = components.year { 27 | return timeAgoText(shortPattern, amount: year, shortText: "y", plural: "years", singular: "year") 28 | } 29 | 30 | if let month = components.month { 31 | return timeAgoText(shortPattern, amount: month, shortText: "m", plural: "months", singular: "month") 32 | } 33 | 34 | if let week = components.weekOfYear { 35 | return timeAgoText(shortPattern, amount: week, shortText: "w", plural: "weeks", singular: "week") 36 | } 37 | 38 | if let day = components.day { 39 | return timeAgoText(shortPattern, amount: day, shortText: "d", plural: "days", singular: "day") 40 | } 41 | 42 | if let hour = components.hour { 43 | return timeAgoText(shortPattern, amount: hour, shortText: "h", plural: "hours", singular: "hour") 44 | } 45 | 46 | if let minute = components.minute { 47 | return timeAgoText(shortPattern, amount: minute, shortText: "min", plural: "minutes", singular: "minute") 48 | } 49 | 50 | if let second = components.second { 51 | return timeAgoText(shortPattern, amount: second, shortText: "s", plural: "seconds", singular: "second") 52 | } 53 | 54 | return "Just now" 55 | } 56 | 57 | fileprivate static func timeAgoText(_ shortPattern: Bool, 58 | amount: Int, 59 | shortText: String, 60 | plural: String, 61 | singular: String) -> String { 62 | if shortPattern { 63 | return "\(amount) \(shortText)" 64 | } else if amount >= 2 { 65 | return "\(amount) \(plural) ago" 66 | } 67 | return "Last \(singular)" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/PromisesExtensios.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromisesExtensios.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 18/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import EZCoreData 11 | import PromiseKit 12 | 13 | extension NSFetchRequestResult where Self: NSManagedObject { 14 | static public func deleteAll(except toKeep: [Self]? = nil, backgroundContext: NSManagedObjectContext = 15 | EZCoreData.privateThreadContext) -> Promise<[Self]?> { 16 | return Promise<[Self]?> { resolver in 17 | deleteAll(except: toKeep, backgroundContext: backgroundContext) { result in 18 | switch result { 19 | case .success: 20 | resolver.fulfill(toKeep) 21 | case .failure(let error): 22 | resolver.reject(error) 23 | } 24 | } 25 | } 26 | } 27 | 28 | public static func importList(_ jsonArray: [[String: Any]]?, 29 | idKey: String = "id", 30 | backgroundContext: NSManagedObjectContext = EZCoreData.privateThreadContext) 31 | -> Promise<[Self]?> { 32 | return Promise<[Self]?> { resolver in 33 | importList(jsonArray, 34 | idKey: idKey, 35 | backgroundContext: backgroundContext) { result in 36 | switch result { 37 | case .success(let result): 38 | resolver.fulfill(result) 39 | case .failure(let error): 40 | resolver.reject(error) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/String+Localized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Localized.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 14/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | public var localized: String { 13 | return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") 14 | } 15 | 16 | public func localized(value: String = "", comment: String = "") -> String { 17 | return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: value, comment: comment) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/UIRefreshControl+ProgramaticallyBeginRefresh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIRefreshControl+ProgramaticallyBeginRefresh.swift .swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIRefreshControl { 12 | func programaticallyBeginRefreshing(in tableView: UITableView) { 13 | beginRefreshing() 14 | let offsetPoint = CGPoint.init(x: 0, y: -frame.size.height) 15 | tableView.setContentOffset(offsetPoint, animated: true) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/UITableViewCell+Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Getter.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ReusableObject { } 12 | 13 | extension ReusableObject { 14 | static var reuseIdentifier: String { 15 | return String(describing: self) 16 | } 17 | } 18 | 19 | extension UITableViewCell: ReusableObject { } 20 | 21 | extension UIViewController: ReusableObject { } 22 | 23 | extension ReusableObject where Self: UITableViewCell { 24 | static func dequeuedReusableCell(_ tableView: UITableView, indexPath: IndexPath) -> Self { 25 | return tableView.dequeueReusableCell(withIdentifier: self.reuseIdentifier, for: indexPath) as! Self 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/UIViewController+HUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+HUD.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 14/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SVProgressHUD 11 | 12 | /// This allows a ViewController to call a HUD (Head-Up Display) anytime. 13 | /// It's encapsulated within this protocol, so a HUD lib can be easily replaced 14 | protocol HUD: class { 15 | /// Shows a success message HUD 16 | func showSuccessHUD(message: String?, delay: TimeInterval) 17 | /// Shows a error message HUD 18 | func showErrorHUD(message: String?, delay: TimeInterval) 19 | /// Shows an info message HUD 20 | func showInfoHUD(message: String?, delay: TimeInterval) 21 | /// Shows a progress HUD 22 | func showProgressHUD(progress: Float, statusMessage: String?) 23 | /// Shows a loading HUD with an optional `message` 24 | func showLoadingHUD(message: String?) 25 | /// Dismisses a HUD 26 | func dismissHUD(delay: TimeInterval) 27 | } 28 | 29 | extension HUD where Self: UIViewController { 30 | 31 | /// Configures the HUD appearance 32 | fileprivate func configHud() { 33 | SVProgressHUD.setBorderColor(.lightGray) 34 | SVProgressHUD.setBorderWidth(0.5) 35 | SVProgressHUD.setBackgroundColor(UIColor(255, 255, 255, 0.3)) 36 | SVProgressHUD.setDefaultMaskType(.black) 37 | } 38 | 39 | /// Shows a success message HUD 40 | func showSuccessHUD(message: String?, delay: TimeInterval = 3) { 41 | configHud() 42 | SVProgressHUD.showSuccess(withStatus: message) 43 | SVProgressHUD.dismiss(withDelay: delay) 44 | } 45 | 46 | /// Shows a error message HUD 47 | func showErrorHUD(message: String?, delay: TimeInterval = 3) { 48 | configHud() 49 | SVProgressHUD.showError(withStatus: message) 50 | SVProgressHUD.dismiss(withDelay: delay) 51 | } 52 | 53 | /// Shows an info message HUD 54 | func showInfoHUD(message: String?, delay: TimeInterval = 3) { 55 | configHud() 56 | SVProgressHUD.showInfo(withStatus: message) 57 | SVProgressHUD.dismiss(withDelay: delay) 58 | } 59 | 60 | /// Shows a progress HUD 61 | func showProgressHUD(progress: Float, statusMessage: String? = nil) { 62 | configHud() 63 | SVProgressHUD.showProgress(progress, status: statusMessage) 64 | } 65 | 66 | /// Shows a loading HUD with an optional `message` 67 | func showLoadingHUD(message: String?) { 68 | configHud() 69 | SVProgressHUD.show(withStatus: message) 70 | SVProgressHUD.dismiss(withDelay: 10) 71 | } 72 | 73 | /// Dismisses a HUD 74 | func dismissHUD(delay: TimeInterval = 0) { 75 | SVProgressHUD.dismiss(withDelay: delay) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/UIViewController+Storyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Storyboard.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension ReusableObject where Self: UIViewController { 12 | static func fromStoryboard(_ storyboardName: StoryboardName) -> Self? { 13 | let storyboard = UIStoryboard(name: storyboardName.rawValue, bundle: Bundle.main) 14 | let viewControllerID = self.reuseIdentifier 15 | let viewController = storyboard.instantiateViewController(withIdentifier: viewControllerID) 16 | guard let newViewController = viewController as? Self else { return nil } 17 | return newViewController 18 | } 19 | } 20 | 21 | enum StoryboardName: String { 22 | case welcome = "Welcome" 23 | case auth = "Auth" 24 | case table = "Table" 25 | case collection = "Collection" 26 | } 27 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Extensions/UIViewController+Toastr.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Toastr.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 04/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | /// Displays a simple toastr in the ViewController 14 | func toastr(_ message: String, delay: Double = 2.0) { 15 | let alert = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) 16 | self.present(alert, animated: true) 17 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) { 18 | alert.dismiss(animated: true) 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Protocols/NibFileOwnerLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NibFileOwnerLoadable.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 13/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol NibFileOwnerLoadable: class { 12 | static var nib: UINib { get } 13 | } 14 | 15 | public extension NibFileOwnerLoadable where Self: UIView { 16 | 17 | static var nib: UINib { 18 | return UINib(nibName: String(describing: self), bundle: Bundle(for: self)) 19 | } 20 | 21 | func instantiateFromNib() -> UIView? { 22 | let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView 23 | return view 24 | } 25 | 26 | func loadNibContent() { 27 | guard let view = instantiateFromNib() else { 28 | fatalError("Failed to instantiate nib \(Self.nib)") 29 | } 30 | view.translatesAutoresizingMaskIntoConstraints = false 31 | self.addSubview(view) 32 | let views = ["view": view] 33 | let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]-0-|", 34 | options: .alignAllLastBaseline, 35 | metrics: nil, 36 | views: views) 37 | let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]-0-|", 38 | options: .alignAllLastBaseline, 39 | metrics: nil, 40 | views: views) 41 | addConstraints(verticalConstraints + horizontalConstraints) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Protocols/OnlineOfflineProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObserveOfflineProtocol.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 12/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | // MARK: - Reachability Notifications 13 | struct ReachabilityNotifications { 14 | static let PhoneIsOffline = Notification.Name("PhoneIsOffline") 15 | static let PhoneIsOnline = Notification.Name("PhoneIsOnline") 16 | } 17 | 18 | // MARK: - 19 | /// **Enable Reachability** 20 | /// When you implement this module, remember to instantiate your `reachabilityManager` 21 | /// This will make yourr class throw app Notifications when your phone's switches between 22 | /// online/offline modes. 23 | protocol EnableReachabilityProtocol { 24 | static var reachabilityManager: NetworkReachabilityManager? { get } 25 | static func setupReachability() 26 | } 27 | 28 | extension EnableReachabilityProtocol { 29 | static func setupReachability() { 30 | reachabilityManager?.startListening() 31 | reachabilityManager?.listener = { _ in 32 | if let isNetworkReachable = self.reachabilityManager?.isReachable, isNetworkReachable == true { 33 | NotificationCenter.default.post(name: ReachabilityNotifications.PhoneIsOnline, object: nil) 34 | } else { 35 | NotificationCenter.default.post(name: ReachabilityNotifications.PhoneIsOffline, object: nil) 36 | } 37 | } 38 | } 39 | } 40 | 41 | // MARK: - Should be implemented in ViewControllers that deal/care with offline mode transition 42 | /// This method should be implemented in ViewControllers that deal/care with offline mode transition 43 | /// Method `handleOfflineSituation` needs to be implemented to deal with offline transition 44 | protocol ObserveOfflineProtocol: class { 45 | func startWatchingOfflineMode() 46 | func stopWatchingOfflineMode() 47 | func handleOfflineSituation() 48 | } 49 | 50 | extension ObserveOfflineProtocol { 51 | /// Calls method `handleOfflineSituation` when phone goes offline 52 | /// Don't forget too call `stopWatchingOfflineMode` from `dinit`, if you have started observing it 53 | func startWatchingOfflineMode() { 54 | // Observes offline mode 55 | NotificationCenter.default.addObserver(self, 56 | selector: Selector(("handleOfflineSituation")), 57 | name: ReachabilityNotifications.PhoneIsOffline, 58 | object: nil) 59 | } 60 | 61 | /// Should be called in the deinit method 62 | func stopWatchingOfflineMode() { 63 | // Deallocs observers 64 | NotificationCenter.default.removeObserver(self, name: ReachabilityNotifications.PhoneIsOffline, object: nil) 65 | } 66 | } 67 | 68 | // MARK: - Should be implemented in ViewControllers that deal/care with online mode transition 69 | /// This method should be implemented in ViewControllers that deal/care with online mode transition 70 | /// Method `handleOnlineSituation` needs to be implemented to deal with online transition 71 | protocol ObserveOnlineProtocol: class { 72 | func startWatchingOnlineMode() 73 | func stopWatchingOnlineMode() 74 | func handleOnlineSituation() 75 | } 76 | 77 | extension ObserveOnlineProtocol { 78 | /// Calls method `handleOnlineSituation` when phone goes online 79 | /// Don't forget too call `stopWatchingOnlineMode` from `dinit`, if you have started observing it 80 | func startWatchingOnlineMode() { 81 | // Observes offline mode 82 | NotificationCenter.default.addObserver(self, 83 | selector: Selector(("handleOnlineSituation")), 84 | name: ReachabilityNotifications.PhoneIsOnline, 85 | object: nil) 86 | } 87 | 88 | /// Should be called in the deinit method 89 | func stopWatchingOnlineMode() { 90 | // Deallocs observers 91 | NotificationCenter.default.removeObserver(self, name: ReachabilityNotifications.PhoneIsOnline, object: nil) 92 | } 93 | } 94 | 95 | /// A protocol that can watch both Offline and Online transitions to handle them 96 | /// 97 | /// You should implement methods `handleOnlineSituation` and `handleOfflineSituation` accordingly 98 | typealias ObserveConectivityProtocol = ObserveOfflineProtocol & ObserveOnlineProtocol 99 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Convenience Protocols/ViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelProtocol.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Should be implemented when you have a collectionView or a tableView and want to: 12 | /// 1. Get an object given an indexPath, using `getObject` method 13 | /// 2. Handle a user cliick, using `userDidSelect` method 14 | protocol ListViewModelProtocol { 15 | /// Returns an object from a given array that corresponds to the indexPath 16 | static func getObject(from objectsList: [T], with indexPath: IndexPath) -> T 17 | 18 | /// User Selected index path 19 | func userDidSelect(indexPath: IndexPath) 20 | } 21 | 22 | extension ListViewModelProtocol { 23 | static func getObject(from objectsList: [T], with indexPath: IndexPath) -> T { 24 | // Retrieve the correspondant article and set-up the cell 25 | if indexPath.row >= objectsList.count { 26 | fatalError("EMPTY_OBJECT_LIST".localized) 27 | } 28 | let article = objectsList[indexPath.row] 29 | return article 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/Article+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article+CoreDataClass.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 10/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | // 9 | 10 | import CoreData 11 | import EZCoreData 12 | 13 | // MARK: - NSManagedObject Declaration 14 | public class Article: NSManagedObject { 15 | } 16 | 17 | // MARK: - Convenience methods 18 | extension Article { 19 | /// Transforms the list of tags into a comma-separated string 20 | func tagsToString() -> String { 21 | 22 | guard let tags = self.tags else { return "" } 23 | if tags.count == 0 { return "" } 24 | guard let tagsArray = tags.allObjects as? [Tag] else { return "" } 25 | 26 | var stringfiedTags = "" 27 | for tag in tagsArray { 28 | guard let label = tag.label else { continue } 29 | if stringfiedTags.count > 0 { 30 | stringfiedTags += ", " 31 | } 32 | stringfiedTags += label 33 | } 34 | 35 | return stringfiedTags 36 | } 37 | } 38 | 39 | // MARK: - EZCoreData 40 | extension Article { 41 | /// Populates Article objects from JSON 42 | override open func populateFromJSON(_ json: [String: Any], context: NSManagedObjectContext) { 43 | guard let rawId = json["id"], let id = Int16("\(rawId)") else { return } 44 | self.id = id 45 | self.authors = json["authors"] as? String 46 | self.content = json["content"] as? String 47 | self.imageUrl = json["image_url"] as? String 48 | self.title = json["title"] as? String 49 | self.website = json["website"] as? String 50 | 51 | if let dateString = json["date"] as? String { 52 | let dateFormatter = DateFormatter() 53 | dateFormatter.dateFormat = "MM-dd-yyyy" 54 | 55 | if let date = dateFormatter.date(from: dateString) as NSDate? { 56 | self.date = date 57 | } 58 | } 59 | 60 | if let tags = json["tags"] as? [[String: Any]] { 61 | do { 62 | guard let tagObjects = try Tag.importList(tags, 63 | idKey: "id", 64 | context: context) else { return } 65 | let tagsSet = NSSet(array: tagObjects) 66 | self.addToTags(tagsSet) 67 | } catch let error { 68 | print(error.localizedDescription) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/Article+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article+CoreDataProperties.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 10/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | extension Article { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest
{ 16 | return NSFetchRequest
(entityName: "Article") 17 | } 18 | 19 | @NSManaged public var authors: String? 20 | @NSManaged public var content: String? 21 | @NSManaged public var date: NSDate? 22 | @NSManaged public var id: Int16 23 | @NSManaged public var imageUrl: String? 24 | @NSManaged public var title: String? 25 | @NSManaged public var website: String? 26 | @NSManaged public var wasRead: Bool 27 | @NSManaged public var tags: NSSet? 28 | 29 | } 30 | 31 | // MARK: Generated accessors for tags 32 | extension Article { 33 | 34 | @objc(addTagsObject:) 35 | @NSManaged public func addToTags(_ value: Tag) 36 | 37 | @objc(removeTagsObject:) 38 | @NSManaged public func removeFromTags(_ value: Tag) 39 | 40 | @objc(addTags:) 41 | @NSManaged public func addToTags(_ values: NSSet) 42 | 43 | @objc(removeTags:) 44 | @NSManaged public func removeFromTags(_ values: NSSet) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/Tag+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag+CoreDataClass.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 10/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | // 9 | 10 | import CoreData 11 | import EZCoreData 12 | 13 | // MARK: - NSManagedObject Declaration 14 | public class Tag: NSManagedObject { 15 | } 16 | 17 | // MARK: - EZCoreData 18 | extension Tag { 19 | /// Populates Tag objects from JSON 20 | override open func populateFromJSON(_ json: [String: Any], context: NSManagedObjectContext) { 21 | guard let id = json["id"] as? Int16 else { return } 22 | self.id = id 23 | self.label = json["label"] as? String 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/Tag+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag+CoreDataProperties.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 10/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | extension Tag { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Tag") 17 | } 18 | 19 | @NSManaged public var id: Int16 20 | @NSManaged public var label: String? 21 | @NSManaged public var articles: NSSet? 22 | 23 | } 24 | 25 | // MARK: Generated accessors for articles 26 | extension Tag { 27 | 28 | @objc(addArticlesObject:) 29 | @NSManaged public func addToArticles(_ value: Article) 30 | 31 | @objc(removeArticlesObject:) 32 | @NSManaged public func removeFromArticles(_ value: Article) 33 | 34 | @objc(addArticles:) 35 | @NSManaged public func addToArticles(_ values: NSSet) 36 | 37 | @objc(removeArticles:) 38 | @NSManaged public func removeFromArticles(_ values: NSSet) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/iOS_Bootstrap.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | CKL_iOS_Challenge.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Models/iOS_Bootstrap.xcdatamodeld/CKL_iOS_Challenge.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-App-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-40x40@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "iTunesArtwork@2x.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/icon_filter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_filter_48.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_filter_72-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/icon_filter.imageset/icon_filter_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/icon_filter.imageset/icon_filter_48.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/icon_filter.imageset/icon_filter_72-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/icon_filter.imageset/icon_filter_72-1.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/img_placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "img_placeholder.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/img_placeholder.imageset/img_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/img_placeholder.imageset/img_placeholder.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/lunch_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "launch_icon.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/lunch_icon.imageset/launch_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Assets.xcassets/lunch_icon.imageset/launch_icon.png -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // States.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Constants { 12 | static let flurryAPIKey: String = "WMFS8685YKR8J5Z4XQ5V" 13 | static let databaseName: String = "iOS_Bootstrap" 14 | static let idKey: String = "id" 15 | } 16 | 17 | struct ArticleState { 18 | static let markRead: String = "Mark as read" 19 | static let markUnread: String = "Mark as unread" 20 | static func getText(initialReadStatus wasRead: Bool) -> String { 21 | if wasRead { 22 | return ArticleState.markUnread 23 | } 24 | return ArticleState.markRead 25 | } 26 | } 27 | 28 | enum DefaultError: Error { 29 | case unknownError 30 | // case connectionError 31 | // case invalidCredentials 32 | // case invalidRequest 33 | // case notFound 34 | // case invalidResponse 35 | // case serverError 36 | // case serverUnavailable 37 | // case timeOut 38 | // case unsuppotedURL 39 | } 40 | 41 | extension DefaultError: LocalizedError { 42 | public var errorDescription: String? { 43 | switch self { 44 | case .unknownError: 45 | return NSLocalizedString("Oops, something went wrong", comment: "DefaultError") 46 | // default: 47 | // return NSLocalizedString("Oops, something went wrong", comment: "DefaultError") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Error Handling/APIErrorHandling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIErrorHandling.swift 3 | // Glovo iOS Challenge 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/03/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Supported error status codes 12 | enum ErrorStatusCode: Int { 13 | case silentError = 0 14 | case customError = 182 15 | case badRequestError = 400 16 | case unauthorizedError = 401 17 | case forbiddenError = 403 18 | case notFoundError = 404 19 | case timeoutError = 408 20 | case conflictError = 409 21 | case serverError = 500 22 | } 23 | 24 | /// This class is used to handle errors more easily 25 | class CustomError: Error { 26 | 27 | var code = ErrorStatusCode.customError 28 | var localizedDescription: String = "Oops, something \n went wrong" 29 | var isCancelled = false 30 | 31 | required init(code: ErrorStatusCode = .customError, error: Error? = nil, localizedDescription: String? = nil) { 32 | self.code = code 33 | 34 | if let error = error { 35 | self.isCancelled = error.isCancelled 36 | self.code = error.isCancelled ? .silentError : code 37 | self.localizedDescription = error.localizedDescription 38 | } 39 | 40 | if let localizedDescription = localizedDescription { 41 | self.localizedDescription = localizedDescription 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Error Handling/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Glovo iOS Challenge 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/03/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Logging Handling 12 | /// Printing Level 13 | public enum LogLevel: Int { 14 | /// Prints nothing 15 | case none = 0 16 | 17 | /// Prints only errors 18 | case error = 1 19 | 20 | /// Prints only errors and warnings 21 | case warning = 2 22 | 23 | /// Prints everything 24 | case info = 3 25 | } 26 | 27 | /// A struct used to handle logs and change the log level easily 28 | public struct Logger { 29 | 30 | fileprivate static let prefixName = "[Glovo iOS]" 31 | 32 | /// Authorized Logging level. Can be any of the following: [none, error, warning, info] 33 | static var allowedVerbose = LogLevel.info 34 | 35 | /// Prints `logText` if `verboseLevel > authorizedVerbose` 36 | static func log(_ logText: Any?, verboseLevel: LogLevel = .info) { 37 | guard let text = logText else { return } 38 | if verboseLevel.rawValue > self.allowedVerbose.rawValue { return } 39 | print("\(prefixName) \(String(describing: verboseLevel).uppercased()): \(text)") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Error Handling/UIViewController+ErrorHandling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ErrorHandling.swift 3 | // Glovo iOS Challenge 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/03/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A ViewController that calls method `onDeinit` when being deallocated. 12 | /// This will ensure the coordinator is being deallocated as well 13 | extension UIViewController: ErrorHandler, HUD { 14 | 15 | /// Handles the error of type ErrorStatus 16 | func handleError(_ error: Error) { 17 | if let err = error as? CustomError { 18 | if err.code == .silentError { return } 19 | showErrorHUD(message: err.localizedDescription) 20 | } else if !error.isCancelled { 21 | showErrorHUD(message: error.localizedDescription) 22 | } else { 23 | dismissHUD() 24 | } 25 | } 26 | } 27 | 28 | /// Handles the error of type ErrorStatus 29 | protocol ErrorHandler: class { 30 | /// Handles the error of type ErrorStatus 31 | func handleError(_ error: Error) 32 | } 33 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iOS Bootstrap 4 | 5 | Created by Marcelo Salloum dos Santos on 14/01/19. 6 | Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | */ 8 | 9 | "NEWS" = "News"; 10 | "COLLECTION" = "Collection"; 11 | "NO_INTERNET_CONNECTION" = "No Internet Connection"; 12 | "SEARCH_NEWS_PLACEHOLDER" = "Search the news..."; 13 | 14 | // Error Messages : 15 | "EMPTY_OBJECT_LIST" = "Object list was not populated"; 16 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Bold.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Medium.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/Colfax/Colfax-Regular.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Black.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Bold-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Bold-Italic.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Bold.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Extra-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Extra-Bold.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Light-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Light-Italic.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Light.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Regular-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Regular-Italic.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Regular.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Semibold-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Semibold-Italic.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelosalloum/iOS-Bootstrap/f1ed34303ec08fc090ef1da94ee1ec89f2ee5c28/iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/ProximaNova/ProximaNova-Semibold.ttf -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/Fonts/UIFont+Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fonts+Custom.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 14/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Struct used to call custom fonts without using String literals 13 | private struct Fonts { 14 | struct ProximaNova { 15 | static let Black: String = "ProximaNova-Black" 16 | static let Bold: String = "ProximaNova-Bold" 17 | static let BoldItalic: String = "ProximaNova-Bold-Italic" 18 | static let ExtraBold: String = "ProximaNova-Extra-Bold" 19 | static let Light: String = "ProximaNova-Light" 20 | static let LightItalic: String = "ProximaNova-Light-Italic" 21 | static let Regular: String = "ProximaNova-Regular" 22 | static let RegularItalic: String = "ProximaNova-Regular-Italic" 23 | static let Semibold: String = "ProximaNova-Semibold" 24 | static let SemiboldItalic: String = "ProximaNova-Semibold-Italic" 25 | } 26 | 27 | struct HelveticaNeue { 28 | static let Bold: String = "HelveticaNeue-Bold" 29 | } 30 | 31 | struct Colfax { 32 | static let Bold: String = "Colfax-Bold" 33 | static let Medium: String = "Colfax-Medium" 34 | static let Regular: String = "Colfax-Regular" 35 | } 36 | } 37 | 38 | // MARK: - Custom Fonts static methods 39 | extension UIFont { 40 | // MARK: - ProximaNova 41 | /// ProximaNova-Black 42 | class func proximaNovaBlack(size: CGFloat) -> UIFont? { 43 | return UIFont(name: Fonts.ProximaNova.Black, size: size) 44 | } 45 | 46 | /// ProximaNova-Bold 47 | class func proximaNovaBold(withSize size: CGFloat) -> UIFont? { 48 | return UIFont(name: Fonts.ProximaNova.Bold, size: size) 49 | } 50 | 51 | /// ProximaNova-Bold-Italic 52 | class func proximaNovaBoldItalic(withSize size: CGFloat) -> UIFont? { 53 | return UIFont(name: Fonts.ProximaNova.BoldItalic, size: size) 54 | } 55 | 56 | /// ProximaNova-Bold-Italic 57 | class func proximaNovaExtraBold(withSize size: CGFloat) -> UIFont? { 58 | return UIFont(name: Fonts.ProximaNova.ExtraBold, size: size) 59 | } 60 | 61 | /// ProximaNova-Light 62 | class func proximaNovaLight(size: CGFloat) -> UIFont? { 63 | return UIFont(name: Fonts.ProximaNova.Light, size: size) 64 | } 65 | 66 | /// ProximaNova-Light-Italic 67 | class func proximaNovaLightItalic(size: CGFloat) -> UIFont? { 68 | return UIFont(name: Fonts.ProximaNova.LightItalic, size: size) 69 | } 70 | 71 | /// ProximaNova-Regular 72 | class func proximaNovaRegular(size: CGFloat) -> UIFont? { 73 | return UIFont(name: Fonts.ProximaNova.Regular, size: size) 74 | } 75 | 76 | /// ProximaNova-Regular-Italic 77 | class func proximaNovaRegularItalic(size: CGFloat) -> UIFont? { 78 | return UIFont(name: Fonts.ProximaNova.RegularItalic, size: size) 79 | } 80 | 81 | /// ProximaNova-Semibold 82 | class func proximaNovaSemibold(size: CGFloat) -> UIFont? { 83 | return UIFont(name: Fonts.ProximaNova.Semibold, size: size) 84 | } 85 | 86 | /// ProximaNova-Semibold-Italic 87 | class func proximaNovaSemiboldItalic(size: CGFloat) -> UIFont? { 88 | return UIFont(name: Fonts.ProximaNova.SemiboldItalic, size: size) 89 | } 90 | 91 | // MARK: - HelveticaNeue 92 | /// HelveticaNeue-Bold 93 | class func helveticaNeueBold(size: CGFloat) -> UIFont? { 94 | return UIFont(name: Fonts.HelveticaNeue.Bold, size: size) 95 | } 96 | 97 | // MARK: - Colfax 98 | /// Colfax-Bold 99 | class func colfaxBold(size: CGFloat) -> UIFont? { 100 | return UIFont(name: Fonts.Colfax.Bold, size: size) 101 | } 102 | 103 | /// Colfax-Medium 104 | class func colfaxMedium(size: CGFloat) -> UIFont? { 105 | return UIFont(name: Fonts.Colfax.Medium, size: size) 106 | } 107 | 108 | /// Colfax-Regular 109 | class func colfaxRegular(size: CGFloat) -> UIFont? { 110 | return UIFont(name: Fonts.Colfax.Regular, size: size) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Resources/Theme/UIColor+Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Custom.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 14/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // MARK: - Convenience Methods 13 | extension UIColor { 14 | /// Init UIColor with values between 0 and 255 (instead of 0...1) 15 | convenience init(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ alpha: CGFloat = 1) { 16 | self.init(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: alpha) 17 | } 18 | 19 | /// Generates a Random Color 20 | class var random: UIColor { 21 | return UIColor(CGFloat.random(in: 0...255), CGFloat.random(in: 0...255), 22 | CGFloat.random(in: 0...255), CGFloat.random(in: 0...1)) 23 | } 24 | } 25 | 26 | // MARK: - Custom Colors 27 | extension UIColor { 28 | 29 | class var mainColor: UIColor { 30 | return UIColor(59.0, 141.0, 238.0) 31 | } 32 | 33 | class var mainColorHighlighted: UIColor { 34 | return UIColor(18.0, 102.0, 203.0) 35 | } 36 | 37 | class var whiteColorHighlighted: UIColor { 38 | return UIColor(white: 217.0 / 255.0, alpha: 1.0) 39 | } 40 | 41 | class var warningRedColor: UIColor { 42 | return UIColor(255.0, 59.0, 48.0) 43 | } 44 | 45 | class var gray80: UIColor { 46 | return UIColor(77.0, 80.0, 90.0) 47 | } 48 | 49 | class var gray50: UIColor { 50 | return UIColor(106.0, 110.0, 124.0) 51 | } 52 | 53 | class var gray30: UIColor { 54 | return UIColor(142.0, 142.0, 147.0) 55 | } 56 | 57 | class var gray20: UIColor { 58 | return UIColor(200.0, 204.0, 215.0) 59 | } 60 | 61 | class var gray5: UIColor { 62 | return UIColor(240.0, 243.0, 247.0) 63 | } 64 | 65 | class var navBarColor: UIColor { 66 | return UIColor(246.0, 247.0, 247.0) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/ApplicationCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | import CoreData 12 | 13 | class ApplicationCoordinator: Coordinator { 14 | let window: UIWindow 15 | var ezCoreData: EZCoreData 16 | let rootViewController: UINavigationController 17 | var welcomeCoordinator: WelcomeCoordinator? 18 | 19 | init(window: UIWindow) { 20 | // Init Values 21 | self.window = window 22 | rootViewController = UINavigationController() 23 | ezCoreData = EZCoreData() 24 | 25 | // Offline Handling 26 | APIService.setupReachability() 27 | 28 | super.init() 29 | // Init Core Data 30 | ezCoreData.setupPersistence(Constants.databaseName) // Initialize Core Data 31 | 32 | // Configures RootVC 33 | rootViewController.navigationBar.prefersLargeTitles = true 34 | 35 | // SetUp Welcome Coordinator 36 | setupWelcomeCoordinator() 37 | // let isUserLogged = false 38 | // if !isUserLogged { 39 | // } else { 40 | // } 41 | } 42 | 43 | override func start() { 44 | window.rootViewController = rootViewController 45 | window.makeKeyAndVisible() 46 | } 47 | 48 | fileprivate func setupWelcomeCoordinator() { 49 | let welcomeCoordinator = WelcomeCoordinator(presenter: rootViewController, ezCoreData: ezCoreData) 50 | welcomeCoordinator.start() 51 | welcomeCoordinator.stop = { 52 | self.welcomeCoordinator = nil 53 | } 54 | self.welcomeCoordinator = welcomeCoordinator 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Auth Home/AuthHomeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthHomeCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | 12 | protocol AuthHomeViewControllerDelegate: class { 13 | func userDidClickLogin() 14 | func userDidClickSignUp() 15 | } 16 | 17 | class AuthHomeCoordinator: Coordinator { 18 | private let presenter: UINavigationController 19 | private var ezCoreData: EZCoreData! 20 | 21 | private weak var authHomeViewController: AuthHomeViewController? 22 | 23 | init(presenter: UINavigationController, ezCoreData: EZCoreData) { 24 | self.presenter = presenter 25 | self.ezCoreData = ezCoreData 26 | } 27 | 28 | override func start() { 29 | // View Controller: 30 | guard let authHomeViewController = AuthHomeViewController.fromStoryboard(.auth) else { return } 31 | authHomeViewController.coordinator = self 32 | 33 | // Present View Controller: 34 | presenter.pushViewController(authHomeViewController, animated: true) 35 | setDeallocallable(with: authHomeViewController) 36 | self.authHomeViewController = authHomeViewController 37 | } 38 | } 39 | 40 | extension AuthHomeCoordinator: AuthHomeViewControllerDelegate { 41 | func userDidClickLogin() { 42 | let loginCoordinator = LoginCoordinator(presenter: presenter) 43 | startCoordinator(loginCoordinator) 44 | } 45 | 46 | func userDidClickSignUp() { 47 | let signUpCoordinator = SignUpCoordinator(presenter: presenter) 48 | startCoordinator(signUpCoordinator) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Auth Home/AuthHomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthHomeViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AuthHomeViewController: CoordinatedViewController { 12 | 13 | weak var coordinator: AuthHomeViewControllerDelegate? 14 | 15 | @IBAction func loginButtonClicked(_ sender: UIButton) { 16 | sender.animateTouchDown().done { 17 | self.coordinator?.userDidClickLogin() 18 | } 19 | } 20 | 21 | @IBAction func signUpButtonClicked(_ sender: UIButton) { 22 | sender.animateTouchDown().done { 23 | self.coordinator?.userDidClickSignUp() 24 | } 25 | } 26 | // override func viewWillAppear(_ animated: Bool) { 27 | // navigationController?.setNavigationBarHidden(true, animated: false) 28 | // super.viewWillAppear(animated) 29 | // } 30 | } 31 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Forgot Password/ForgotPasswordCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ForgotPasswordViewControllerDelegate: class { 12 | func userDidClickForgotPassword() 13 | } 14 | 15 | class ForgotPasswordCoordinator: Coordinator { 16 | private let presenter: UINavigationController 17 | private weak var forgotPasswordViewController: ForgotPasswordViewController? 18 | 19 | init(presenter: UINavigationController) { 20 | self.presenter = presenter 21 | } 22 | 23 | override func start() { 24 | // View Model 25 | let viewModel = ForgotPasswordViewModel() 26 | 27 | // View Controller: 28 | guard let forgotPasswordViewController = ForgotPasswordViewController.fromStoryboard(.auth) else { return } 29 | forgotPasswordViewController.viewModel = viewModel 30 | 31 | // Present View Controller: 32 | presenter.pushViewController(forgotPasswordViewController, animated: true) 33 | setDeallocallable(with: forgotPasswordViewController) 34 | self.forgotPasswordViewController = forgotPasswordViewController 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Forgot Password/ForgotPasswordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ForgotPasswordViewController: CoordinatedViewController { 12 | @IBOutlet weak var emailTextField: DesignableTextField! 13 | 14 | var viewModel: ForgotPasswordViewModel! 15 | 16 | @IBAction func forgotPasswordClicked(_ sender: UIButton) { 17 | sender.animateTouchDown().done { 18 | self.viewModel.forgotPassword(email: self.emailTextField.text) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Forgot Password/ForgotPasswordViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ForgotPasswordViewModel: NSObject { 12 | 13 | func forgotPassword(email: String?) { 14 | print(email ?? "") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Login/LoginCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol LoginViewControllerDelegate: class { 12 | func userDidClickForgotPassword() 13 | } 14 | 15 | class LoginCoordinator: Coordinator { 16 | private let presenter: UINavigationController 17 | 18 | private weak var loginViewController: LoginViewController? 19 | 20 | init(presenter: UINavigationController) { 21 | self.presenter = presenter 22 | } 23 | 24 | override func start() { 25 | // View Model 26 | let viewModel = LoginViewModel() 27 | viewModel.coordinator = self 28 | 29 | // View Controller: 30 | guard let loginViewController = LoginViewController.fromStoryboard(.auth) else { return } 31 | loginViewController.viewModel = viewModel 32 | 33 | // Present View Controller: 34 | presenter.pushViewController(loginViewController, animated: true) 35 | setDeallocallable(with: loginViewController) 36 | self.loginViewController = loginViewController 37 | } 38 | } 39 | 40 | extension LoginCoordinator: LoginViewControllerDelegate { 41 | func userDidClickForgotPassword() { 42 | let forgotPasswordCoordinator = ForgotPasswordCoordinator(presenter: presenter) 43 | startCoordinator(forgotPasswordCoordinator) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Login/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginViewController: CoordinatedViewController { 12 | 13 | var viewModel: LoginViewModel! 14 | @IBOutlet weak var emailField: UITextField! 15 | @IBOutlet weak var passwordFied: UITextField! 16 | 17 | @IBAction func loginButtonClicked(_ sender: UIButton) { 18 | sender.animateTouchDown().done { 19 | self.toastr("loginButtonClicked with -mail: \(self.emailField.text!), password: \(self.passwordFied.text!)") 20 | self.viewModel.login(email: self.emailField.text, password: self.passwordFied.text) 21 | } 22 | } 23 | 24 | @IBAction func forgotPasswordClicked(_ sender: UIButton) { 25 | sender.animateTouchDown().done { 26 | self.viewModel.userDidClickForgotPassword() 27 | } 28 | } 29 | 30 | override func viewWillAppear(_ animated: Bool) { 31 | navigationController?.navigationBar.prefersLargeTitles = false 32 | navigationController?.setNavigationBarHidden(false, animated: true) 33 | super.viewWillAppear(animated) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/Login/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginViewModel: NSObject { 12 | 13 | weak var coordinator: LoginViewControllerDelegate? 14 | 15 | func login(email: String?, password: String?) { 16 | print(email ?? "", password ?? "") 17 | } 18 | 19 | func userDidClickForgotPassword() { 20 | coordinator?.userDidClickForgotPassword() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/SignUp/SignUpCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol SignUpViewControllerDelegate: class { 12 | func userDidClickForgotPassword() 13 | } 14 | 15 | class SignUpCoordinator: Coordinator { 16 | private let presenter: UINavigationController 17 | private weak var signUpViewController: SignUpViewController? 18 | 19 | init(presenter: UINavigationController) { 20 | self.presenter = presenter 21 | } 22 | 23 | override func start() { 24 | // View Model 25 | let viewModel = SignUpViewModel() 26 | viewModel.coordinator = self 27 | 28 | // View Controller: 29 | guard let signUpViewController = SignUpViewController.fromStoryboard(.auth) else { return } 30 | signUpViewController.viewModel = viewModel 31 | // viewModel.delegate = signUpViewController 32 | 33 | // Present View Controller: 34 | presenter.pushViewController(signUpViewController, animated: true) 35 | setDeallocallable(with: signUpViewController) 36 | self.signUpViewController = signUpViewController 37 | } 38 | } 39 | 40 | extension SignUpCoordinator: SignUpViewControllerDelegate { 41 | func userDidClickForgotPassword() { 42 | signUpViewController?.toastr("userDidClickForgotPassword") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/SignUp/SignUpViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SignUpViewController: CoordinatedViewController { 12 | 13 | @IBOutlet weak var emailTextField: DesignableTextField! 14 | @IBOutlet weak var passwordTextField: DesignableTextField! 15 | @IBOutlet weak var passwordConfirmationTextField: DesignableTextField! 16 | 17 | var viewModel: SignUpViewModel! 18 | 19 | @IBAction func signUpButtonClicked(_ sender: UIButton) { 20 | sender.animateTouchDown().done { 21 | let emailText = self.emailTextField.text 22 | let passText = self.passwordTextField.text 23 | let confirmPassText = self.passwordConfirmationTextField.text 24 | self.toastr("signUpButtonClicked with e-mail: \(emailText!), password: \(passText!)") 25 | self.viewModel.signUp(email: emailText, 26 | password: passText, 27 | passwordConfirmation: confirmPassText) 28 | } 29 | } 30 | 31 | override func viewWillAppear(_ animated: Bool) { 32 | navigationController?.setNavigationBarHidden(false, animated: true) 33 | super.viewWillAppear(animated) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Auth/SignUp/SignUpViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SignUpViewModel: NSObject { 12 | 13 | weak var coordinator: SignUpViewControllerDelegate? 14 | 15 | func signUp(email: String?, password: String?, passwordConfirmation: String?) { 16 | print(email ?? "", password ?? "", passwordConfirmation ?? "") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 01/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Deintiable Coordinator 12 | /// When a ViewController is removed from the stack, it's Coordinator is atomatically dealocated 13 | protocol DeInitCallable: AnyObject { 14 | var onDeinit: (() -> Void)? { get set } 15 | } 16 | 17 | /// A ViewController that calls method `onDeinit` when being deallocated. 18 | /// This will ensure the coordinator is being deallocated as well 19 | class CoordinatedViewController: UIViewController, DeInitCallable { 20 | 21 | var onDeinit: (() -> Void)? 22 | deinit { 23 | onDeinit?() 24 | } 25 | } 26 | 27 | /// A Protocol adopted by the coordinator class, containing it's basic methods 28 | protocol CoordinatorProtocol: AnyObject { 29 | // Start and stopdefault functions 30 | func start() 31 | var stop: (() -> Void)? { get set } 32 | // method and variable used to make coordinators easy to deallocate 33 | func setDeallocallable(with object: DeInitCallable) 34 | var deallocallable: DeInitCallable? { get set } 35 | 36 | /// The array containing any child Coordinators 37 | var childCoordinators: [Coordinator] { get set } 38 | } 39 | 40 | extension CoordinatorProtocol { 41 | /// Sets the key Deallocable object for a coordinator 42 | /// This enables dealloacation of the coordinator once the object gets deallocated via onDeinit closure. 43 | func setDeallocallable(with object: DeInitCallable) { 44 | deallocallable?.onDeinit = nil 45 | object.onDeinit = { [weak self] in 46 | self?.stop?() 47 | } 48 | deallocallable = object 49 | } 50 | 51 | /// Add a child coordinator to the parent 52 | func addChildCoordinator(childCoordinator: Coordinator) { 53 | self.childCoordinators.append(childCoordinator) 54 | } 55 | 56 | /// Remove a child coordinator from the parent 57 | func removeChildCoordinator(childCoordinator: Coordinator) { 58 | self.childCoordinators = self.childCoordinators.filter { $0 !== childCoordinator } 59 | } 60 | 61 | } 62 | 63 | // MARK: - Cordinator base class 64 | /// Cordinator base class 65 | class Coordinator: NSObject, CoordinatorProtocol { 66 | 67 | // MARK: Properties 68 | func start() { } 69 | var stop: (() -> Void)? 70 | weak var deallocallable: DeInitCallable? 71 | 72 | var childCoordinators = [Coordinator]() 73 | 74 | func startCoordinator(_ childCoordinator: Coordinator) { 75 | childCoordinator.start() 76 | childCoordinator.stop = { [weak self, weak childCoordinator] in 77 | guard let strongSelf = self else { return } 78 | guard let childCoordinator = childCoordinator else { return } 79 | strongSelf.removeChildCoordinator(childCoordinator: childCoordinator) 80 | } 81 | self.addChildCoordinator(childCoordinator: childCoordinator) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/Cells (First Level)/Cells (Second Level)/NewsSecondLevelCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsFirstLevelCollectionViewCell.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 07/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsSecondLevelCollectionViewCell: UICollectionViewCell { 12 | 13 | // MARK: - Injected Dependencies (Interface Builder included) 14 | @IBOutlet weak var newsCardView: NewsCardView! 15 | 16 | var article: Article! { 17 | didSet { 18 | setupSubviews() 19 | } 20 | } 21 | } 22 | 23 | // MARK: - Interface Setup & Customization 24 | extension NewsSecondLevelCollectionViewCell { 25 | func setupSubviews() { 26 | // Set-up the cell content 27 | newsCardView.titleLabel.text = article.title 28 | newsCardView.timeLabel.text = NSDate.timeAgoSince(article.date, shortPattern: true) 29 | newsCardView.authorsLabel.text = article.authors 30 | updateWasReadStatus(article.wasRead) 31 | 32 | // Setup the cell image 33 | guard let imageURL = article.imageUrl else { return } 34 | guard let url = URL(string: imageURL) else { return } 35 | // Image Caching 36 | newsCardView.imageView.kf.indicatorType = .activity 37 | newsCardView.imageView.kf.setImage(with: url) 38 | } 39 | 40 | func updateWasReadStatus(_ wasRead: Bool) { 41 | newsCardView.backgroundColor = wasRead ? UIColor.gray5 : UIColor.gray20 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/Cells (First Level)/InnerCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class InnerCollectionViewModel: NSObject { 12 | 13 | // MARK: - Injected Dependencies 14 | weak var coordinator: NewsInteractionProtocol? 15 | 16 | var articlesTag: Tag! { 17 | didSet { 18 | guard let articles = articlesTag.articles?.allObjects as? [Article] else { return } 19 | self.articles = articles 20 | } 21 | } 22 | 23 | var articles: [Article] = [] 24 | } 25 | 26 | // MARK: - User Selected index path 27 | extension InnerCollectionViewModel: ListViewModelProtocol { 28 | func userDidSelect(indexPath: IndexPath) { 29 | let article = type(of: self).getObject(from: articles, with: indexPath) 30 | coordinator?.userDidSelectArticle(article) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/Cells (First Level)/NewsFirstLevelCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsFirstLevelCollectionViewCell.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 07/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsFirstLevelCollectionViewCell: UICollectionViewCell { 12 | 13 | // MARK: - Injected Dependencies 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var innerCollectionView: UICollectionView! 16 | 17 | var viewModel: InnerCollectionViewModel! { 18 | didSet { 19 | setupSubviews() 20 | } 21 | } 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | 26 | // Collection View 27 | innerCollectionView.delegate = self 28 | innerCollectionView.dataSource = self 29 | } 30 | } 31 | 32 | // MARK: - Interface Setup & Customization 33 | extension NewsFirstLevelCollectionViewCell { 34 | /// Setup Subview(s) 35 | func setupSubviews() { 36 | // titleLabel 37 | titleLabel.text = viewModel.articlesTag.label 38 | innerCollectionView.reloadData() 39 | } 40 | } 41 | 42 | // MARK: - Data Source 43 | extension NewsFirstLevelCollectionViewCell: UICollectionViewDataSource { 44 | func collectionView(_ collectionView: UICollectionView, 45 | numberOfItemsInSection section: Int) -> Int { 46 | return viewModel.articles.count 47 | } 48 | 49 | func collectionView(_ collectionView: UICollectionView, 50 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 51 | let cellId = String(describing: NewsSecondLevelCollectionViewCell.self) 52 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, 53 | for: indexPath) as! NewsSecondLevelCollectionViewCell 54 | cell.article = InnerCollectionViewModel.getObject(from: viewModel.articles, with: indexPath) 55 | 56 | return cell 57 | } 58 | } 59 | 60 | // MARK: - Delegate 61 | extension NewsFirstLevelCollectionViewCell: UICollectionViewDelegateFlowLayout { 62 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 63 | guard let cell = collectionView.cellForItem(at: indexPath) as? NewsSecondLevelCollectionViewCell else { return } 64 | cell.animateTouchDown().done { 65 | self.viewModel.userDidSelect(indexPath: indexPath) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/Collection.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ProximaNova-Regular 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/NewsCollectionCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsCollectionCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | 12 | // MARK: - Coordinator Mandatory Implementation 13 | class NewsCollectionCoordinator: Coordinator { 14 | // MARK: - Init 15 | private let presenter: UINavigationController 16 | private var ezCoreData: EZCoreData 17 | 18 | init(presenter: UINavigationController, ezCoreData: EZCoreData) { 19 | self.presenter = presenter 20 | self.ezCoreData = ezCoreData 21 | } 22 | 23 | // MARK: - Start 24 | private weak var newsCollectionViewController: NewsCollectionViewController? 25 | override func start() { 26 | // View Model 27 | let viewModel = NewsCollectionViewModel() 28 | viewModel.ezCoreData = ezCoreData 29 | viewModel.coordinator = self 30 | 31 | // View Controller: 32 | guard let newsCollectionViewController = 33 | NewsCollectionViewController.fromStoryboard(.collection) else { return } 34 | newsCollectionViewController.viewModel = viewModel 35 | viewModel.delegate = newsCollectionViewController 36 | 37 | // Present View Controller: 38 | presenter.pushViewController(newsCollectionViewController, animated: true) 39 | setDeallocallable(with: newsCollectionViewController) 40 | self.newsCollectionViewController = newsCollectionViewController 41 | } 42 | } 43 | 44 | // MARK: - NewsInteractionProtocol 45 | extension NewsCollectionCoordinator: NewsInteractionProtocol { 46 | func userDidSelectArticle(_ selectedArticle: Article) { 47 | let newsDetailCoordinator = NewsDetailCoordinator(presenter: presenter, article: selectedArticle) 48 | startCoordinator(newsDetailCoordinator) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/NewsCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsCollectionViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsCollectionViewController: CoordinatedViewController { 12 | 13 | // MARK: - Injected Dependencies (Interface Builder included) 14 | @IBOutlet weak var collectionView: UICollectionView! 15 | var viewModel: NewsCollectionViewModel! 16 | 17 | // MARK: - Lifecycle 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | // Collection View 22 | collectionView.dataSource = self 23 | collectionView.delegate = self 24 | 25 | // Nav Bar 26 | self.title = "COLLECTION".localized 27 | } 28 | 29 | override func viewWillAppear(_ animated: Bool) { 30 | // Nav Bar 31 | navigationController?.setNavigationBarHidden(false, animated: false) 32 | 33 | // View Model 34 | viewModel.updateDataSource() 35 | 36 | super.viewWillAppear(animated) 37 | } 38 | } 39 | 40 | // MARK: - Data Source 41 | extension NewsCollectionViewController: UICollectionViewDataSource { 42 | func collectionView(_ collectionView: UICollectionView, 43 | numberOfItemsInSection section: Int) -> Int { 44 | return viewModel.tagsViewModels.count 45 | } 46 | 47 | func collectionView(_ collectionView: UICollectionView, 48 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 49 | // Get Cell 50 | let cellId = String(describing: NewsFirstLevelCollectionViewCell.self) 51 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, 52 | for: indexPath) as! NewsFirstLevelCollectionViewCell 53 | // Configure Cell 54 | let cellViewModel = NewsCollectionViewModel.getObject(from: viewModel.tagsViewModels, with: indexPath) 55 | cell.viewModel = cellViewModel 56 | return cell 57 | } 58 | } 59 | 60 | // MARK: - Delegate 61 | extension NewsCollectionViewController: UICollectionViewDelegateFlowLayout { 62 | func collectionView(_ collectionView: UICollectionView, 63 | layout collectionViewLayout: UICollectionViewLayout, 64 | sizeForItemAt indexPath: IndexPath) -> CGSize { 65 | return CGSize(width: collectionView.frame.width, height: 150) 66 | } 67 | } 68 | 69 | // MARK: - View Model: NewsCollectionViewDelegate 70 | extension NewsCollectionViewController: NewsCollectionViewDelegate { 71 | func reloadData(endRefreshing: Bool) { 72 | self.collectionView.reloadData() 73 | } 74 | 75 | func displayError(error: Error, endRefreshing: Bool) { 76 | self.showErrorHUD(message: error.localizedDescription) 77 | } 78 | 79 | func displayMessage(_ message: String) { 80 | self.toastr(message) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Collection/NewsCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import EZCoreData 11 | import PromiseKit 12 | 13 | class NewsCollectionViewModel: NSObject { 14 | // MARK: - Initial Set-up 15 | private var tags: [Tag] = [] { 16 | didSet { 17 | var newTagsViewModels = [InnerCollectionViewModel]() 18 | for tag in tags { 19 | let newChildModel = InnerCollectionViewModel() 20 | newChildModel.coordinator = coordinator 21 | newChildModel.articlesTag = tag 22 | newTagsViewModels.append(newChildModel) 23 | } 24 | self.tagsViewModels = newTagsViewModels 25 | } 26 | } 27 | var tagsViewModels: [InnerCollectionViewModel] = [] 28 | 29 | var ezCoreData: EZCoreData! 30 | 31 | /// Delegate to ViewController 32 | weak var delegate: NewsCollectionViewDelegate? 33 | 34 | /// Coordinator delegate to Coordinator 35 | weak var coordinator: NewsInteractionProtocol? 36 | 37 | // MARK: - Observe for Offline mode 38 | override init() { 39 | super.init() 40 | startWatchingOfflineMode() 41 | } 42 | 43 | deinit { 44 | stopWatchingOfflineMode() 45 | } 46 | } 47 | 48 | // MARK: - Offline mode 49 | extension NewsCollectionViewModel: ObserveOfflineProtocol { 50 | @objc func handleOfflineSituation() { 51 | delegate?.displayMessage("NO_INTERNET_CONNECTION".localized) 52 | } 53 | } 54 | 55 | // MARK: - User Selected index path 56 | extension NewsCollectionViewModel: ListViewModelProtocol { 57 | func userDidSelect(indexPath: IndexPath) { } 58 | } 59 | 60 | // MARK: - Core Data Service: GET Tags (and Articles) 61 | extension NewsCollectionViewModel { 62 | func updateDataSource() { 63 | do { 64 | tags = try Tag.readAll(context: ezCoreData.mainThreadContext) 65 | DispatchQueue.main.async { 66 | self.delegate?.reloadData(endRefreshing: true) 67 | } 68 | } catch let error as NSError { 69 | print("ERROR: \(error.localizedDescription)") 70 | } 71 | } 72 | } 73 | 74 | // MARK: - API Service: GET Articles (and Tags) 75 | extension NewsCollectionViewModel { 76 | func fetchAPIData() { 77 | firstly { () -> Promise<[[String: Any]]> in 78 | APIService.getArticlesList(ezCoreData.privateThreadContext) 79 | }.then { json -> Promise<[Article]?> in 80 | Article.importList(json, idKey: Constants.idKey, backgroundContext: self.ezCoreData.privateThreadContext) 81 | }.then { importedArticles -> Promise<[Article]?> in 82 | Article.deleteAll(except: importedArticles, backgroundContext: self.ezCoreData.privateThreadContext) 83 | }.asVoid().done { 84 | self.updateDataSource() 85 | }.catch { error in 86 | self.delegate?.displayError(error: error, endRefreshing: true) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Detail/NewsDetailCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | 12 | // MARK: - Coordinator Mandatory Implementation 13 | class NewsDetailCoordinator: Coordinator { 14 | // MARK: - Init 15 | private let presenter: UINavigationController 16 | private var article: Article 17 | 18 | init(presenter: UINavigationController, article: Article) { 19 | self.presenter = presenter 20 | self.article = article 21 | } 22 | 23 | // MARK: - Start 24 | private weak var newsDetailViewController: NewsDetailViewController? 25 | override func start() { 26 | // View Controller: 27 | guard let newsDetailViewController = NewsDetailViewController.fromStoryboard(.table) else { return } 28 | newsDetailViewController.title = "📚" 29 | // View Model: 30 | let viewModel = NewsDetailViewModel() 31 | viewModel.article = article 32 | viewModel.delegate = newsDetailViewController 33 | newsDetailViewController.viewModel = viewModel 34 | // Present View Controller: 35 | presenter.pushViewController(newsDetailViewController, animated: true) 36 | setDeallocallable(with: newsDetailViewController) 37 | self.newsDetailViewController = newsDetailViewController 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Detail/NewsDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsDetailViewController: CoordinatedViewController { 12 | 13 | // MARK: - Injected Dependencies 14 | @IBOutlet weak var imageView: UIImageView! 15 | @IBOutlet weak var titleLabel: UILabel! 16 | @IBOutlet weak var timeLabel: UILabel! 17 | @IBOutlet weak var authorLabel: UILabel! 18 | @IBOutlet weak var tagsLabel: UILabel! 19 | @IBOutlet weak var contentLabel: UILabel! 20 | @IBOutlet weak var rightBarButtonItem: UIBarButtonItem! 21 | 22 | var viewModel: NewsDetailViewModel! 23 | 24 | // MARK: - User Action 25 | @IBAction func didSelectRightBarButtonItem(_ sender: UIBarButtonItem) { 26 | self.viewModel.userSwitchedReadStatus() 27 | } 28 | 29 | // MARK: - Lifecycle 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | setupView() 33 | } 34 | 35 | override func viewWillAppear(_ animated: Bool) { 36 | super.viewWillAppear(animated) 37 | navigationController?.navigationBar.prefersLargeTitles = false 38 | } 39 | 40 | } 41 | 42 | // MARK: - Interface Customization 43 | extension NewsDetailViewController: NewsDetailProtocol { 44 | 45 | /// Setups the view appearance 46 | fileprivate func setupView() { 47 | // Image 48 | guard let imageUrl = viewModel.article.imageUrl else { return } 49 | guard let url = URL(string: imageUrl) else { return } 50 | imageView.kf.indicatorType = .activity 51 | imageView.kf.setImage(with: url) 52 | 53 | // Labels 54 | titleLabel?.text = viewModel.article.title 55 | authorLabel?.text = viewModel.article.authors 56 | contentLabel?.text = viewModel.article.content 57 | timeLabel?.text = NSDate.timeAgoSince(viewModel.article.date) 58 | tagsLabel?.text = viewModel.article.tagsToString() 59 | 60 | // Navbar 61 | self.title = "NEWS".localized 62 | resetRightBarButtonItem() 63 | } 64 | 65 | /// Sets up the right bar button in the navbar 66 | func resetRightBarButtonItem(withText buttonText: String = ArticleState.markUnread) { 67 | self.navigationItem.rightBarButtonItem = UIBarButtonItem( 68 | title: buttonText, 69 | style: .plain, 70 | target: self, 71 | action: #selector(NewsDetailViewController.didSelectRightBarButtonItem(_:))) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Detail/NewsDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 10/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol NewsDetailProtocol: class { 12 | func resetRightBarButtonItem(withText buttonText: String) 13 | } 14 | 15 | class NewsDetailViewModel: NSObject { 16 | 17 | // MARK: - Properties 18 | weak var delegate: NewsDetailProtocol? 19 | 20 | var article: Article! { 21 | didSet { 22 | updateReadStatus(true) 23 | } 24 | } 25 | 26 | // MARK: - From the VC: 27 | func userSwitchedReadStatus() { 28 | updateReadStatus(!article.wasRead) 29 | } 30 | 31 | fileprivate func updateReadStatus(_ finalReadState: Bool) { 32 | article.wasRead = finalReadState 33 | article.managedObjectContext?.saveContextToStore() // Sync task because the user is waiting for the result 34 | let buttonText = ArticleState.getText(initialReadStatus: article.wasRead) 35 | self.delegate?.resetRightBarButtonItem(withText: buttonText) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Table/Cells/NewsTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewCell.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 28/12/18. 6 | // Copyright © 2018 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsTableViewCell: UITableViewCell { 12 | 13 | // MARK: - Injected Dependencies 14 | @IBOutlet weak var newsContentView: NewsCardView! 15 | 16 | var article: Article! { 17 | didSet { 18 | setupSubviews() 19 | } 20 | } 21 | } 22 | 23 | // MARK: - Interface Setup & Customization 24 | extension NewsTableViewCell { 25 | func setupSubviews() { 26 | // Set-up the cell content 27 | newsContentView.titleLabel.text = article.title 28 | newsContentView.timeLabel.text = NSDate.timeAgoSince(article.date, shortPattern: true) 29 | newsContentView.authorsLabel.text = article.authors 30 | updateWasReadStatus(article.wasRead) 31 | 32 | // Setup the cell image 33 | guard let imageURL = article.imageUrl else { return } 34 | guard let url = URL(string: imageURL) else { return } 35 | // Image Caching 36 | newsContentView.imageView.kf.indicatorType = .activity 37 | newsContentView.imageView.kf.setImage(with: url) 38 | } 39 | 40 | func updateWasReadStatus(_ wasRead: Bool) { 41 | newsContentView.backgroundColor = wasRead ? UIColor.gray5 : UIColor.gray20 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Table/NewsTableCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 30/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | 12 | // MARK: - Coordinator Mandatory Implementation 13 | class NewsTableCoordinator: Coordinator { 14 | // MARK: - Init 15 | private let presenter: UINavigationController 16 | private var ezCoreData: EZCoreData! 17 | 18 | init(presenter: UINavigationController, ezCoreData: EZCoreData) { 19 | self.presenter = presenter 20 | self.ezCoreData = ezCoreData 21 | } 22 | 23 | // MARK: - Start 24 | private weak var newsTableViewController: NewsTableViewController? 25 | override func start() { 26 | // View Controller: 27 | guard let newsTableViewController = NewsTableViewController.fromStoryboard(.table) else { return } 28 | setDeallocallable(with: newsTableViewController) 29 | 30 | // View Model: 31 | let viewModel = NewsTableViewModel() 32 | newsTableViewController.viewModel = viewModel 33 | viewModel.coordinator = self 34 | viewModel.delegate = newsTableViewController 35 | viewModel.ezCoreData = ezCoreData 36 | 37 | // Present View Controller: 38 | presenter.pushViewController(newsTableViewController, animated: true) 39 | self.newsTableViewController = newsTableViewController 40 | } 41 | } 42 | 43 | // MARK: - Article Interaction Protocol 44 | extension NewsTableCoordinator: NewsInteractionProtocol { 45 | func userDidSelectArticle(_ selectedArticle: Article) { 46 | let newsDetailCoordinator = NewsDetailCoordinator(presenter: presenter, article: selectedArticle) 47 | startCoordinator(newsDetailCoordinator) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Table/NewsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 28/12/18. 6 | // Copyright © 2018 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | class NewsTableViewController: CoordinatedViewController { 13 | 14 | // MARK: - Injected Dependencies 15 | // bottomView and it's constraints: 16 | @IBOutlet weak var bottomView: UIView! 17 | @IBOutlet weak var bottomViewBottomConstraint: NSLayoutConstraint! 18 | @IBOutlet weak var bottomViewHeightConstraint: NSLayoutConstraint! 19 | 20 | @IBOutlet weak var tableView: UITableView! 21 | 22 | var viewModel: NewsTableViewModel! 23 | 24 | /// `UIRefreshControl` used for pullToRefresh 25 | var refreshControl: UIRefreshControl! 26 | 27 | /// `UISearchController` used to search throughout the news 28 | var searchController: UISearchController! 29 | 30 | // MARK: - Lifecycle 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | // TableView 35 | tableView.delegate = self 36 | tableView.dataSource = self 37 | 38 | // Refresh Controller 39 | setupRefreshControl() 40 | 41 | // Search Controller 42 | setupSearchController() 43 | 44 | // ViewModel 45 | viewModel.searchTerm = "" // Runs first Search by setting this value 46 | 47 | // BottomView 48 | bottomViewBottomConstraint.constant = -100 49 | 50 | // Navbar 51 | self.title = "NEWS".localized 52 | navigationController?.navigationBar.prefersLargeTitles = true 53 | navigationController?.setNavigationBarHidden(false, animated: false) 54 | } 55 | 56 | override func viewDidAppear(_ animated: Bool) { 57 | bottomViewHeightConstraint.constant = 44 + self.view.safeAreaInsets.bottom 58 | self.pullToRefresh(refreshControl) 59 | super.viewDidAppear(animated) 60 | } 61 | } 62 | 63 | // MARK: - Pull to Refresh 64 | extension NewsTableViewController { 65 | func setupRefreshControl() { 66 | refreshControl = UIRefreshControl() 67 | refreshControl.addTarget(self, 68 | action: #selector(NewsTableViewController.pullToRefresh(_:)), 69 | for: UIControl.Event.valueChanged) 70 | // Refresh Controller 71 | tableView.addSubview(refreshControl) 72 | } 73 | 74 | @IBAction func pullToRefresh(_ sender: UIRefreshControl) { 75 | viewModel.fetchAPIData() 76 | } 77 | } 78 | 79 | // MARK: - Search Controller 80 | extension NewsTableViewController: UISearchResultsUpdating { 81 | func setupSearchController() { 82 | // SearchViewController 83 | searchController = UISearchController(searchResultsController: nil) 84 | searchController.searchResultsUpdater = self 85 | searchController.obscuresBackgroundDuringPresentation = false 86 | searchController.searchBar.placeholder = "SEARCH_NEWS_PLACEHOLDER".localized 87 | definesPresentationContext = true 88 | navigationItem.searchController = searchController 89 | } 90 | 91 | // UISearchResultsUpdating 92 | func updateSearchResults(for searchController: UISearchController) { 93 | if let searchText = searchController.searchBar.text { 94 | viewModel.searchTerm = searchText 95 | } 96 | } 97 | } 98 | 99 | // MARK: - Data Source 100 | extension NewsTableViewController: UITableViewDataSource { 101 | func numberOfSections(in tableView: UITableView) -> Int { 102 | return 1 103 | } 104 | 105 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 106 | return viewModel.articles.count 107 | } 108 | 109 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 110 | let articleCell = NewsTableViewCell.dequeuedReusableCell(tableView, indexPath: indexPath) 111 | articleCell.article = NewsTableViewModel.getObject(from: viewModel.articles, with: indexPath) 112 | return articleCell 113 | } 114 | 115 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 116 | return true 117 | } 118 | } 119 | 120 | // MARK: - Delegate 121 | extension NewsTableViewController: UITableViewDelegate { 122 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 123 | guard let cell = tableView.cellForRow(at: indexPath) as? NewsTableViewCell else { return } 124 | cell.animateTouchDown().done { 125 | self.viewModel.userDidSelect(indexPath: indexPath) 126 | } 127 | } 128 | 129 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 130 | // Get Article: 131 | let article = NewsTableViewModel.getObject(from: viewModel.articles, with: indexPath) 132 | let initialReadStatus = article.wasRead 133 | let finalReadStatusText = ArticleState.getText(initialReadStatus: initialReadStatus) 134 | 135 | // Setups Button Text 136 | let readStatus = UITableViewRowAction(style: .normal, title: finalReadStatusText) { _, indexPath in 137 | self.viewModel.toggleReadStatus(article: article) 138 | let articleCell = NewsTableViewCell.dequeuedReusableCell(tableView, indexPath: indexPath) 139 | articleCell.updateWasReadStatus(initialReadStatus) 140 | tableView.reloadRows(at: [indexPath], with: .none) 141 | } 142 | readStatus.backgroundColor = Color.mainColor 143 | 144 | return [readStatus] 145 | } 146 | 147 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 148 | return 120 149 | } 150 | } 151 | 152 | // MARK: - View Model NewsCollectionViewDelegate 153 | extension NewsTableViewController: NewsCollectionViewDelegate { 154 | func reloadData(endRefreshing: Bool) { 155 | if endRefreshing { 156 | self.refreshControl.endRefreshing() 157 | } 158 | self.tableView.reloadData() 159 | } 160 | 161 | func displayError(error: Error, endRefreshing: Bool) { 162 | if endRefreshing { 163 | self.refreshControl.endRefreshing() 164 | } 165 | self.showErrorHUD(message: error.localizedDescription) 166 | } 167 | 168 | func displayMessage(_ message: String) { 169 | self.toastr(message) 170 | } 171 | } 172 | 173 | // MARK: - Filter Button and BottomView animation 174 | extension NewsTableViewController { 175 | 176 | @IBAction func filterButtonClicked(_ sender: UIBarButtonItem) { 177 | bottomViewBottomConstraint.constant = viewModel.toggledContraintForFilterView(self.bottomView.frame.height) 178 | UIView.animate(.promise, 179 | duration: 0.5, 180 | delay: 0, 181 | usingSpringWithDamping: 0.8, 182 | initialSpringVelocity: 5, 183 | options: .curveEaseOut) { 184 | self.bottomView.superview?.layoutIfNeeded() 185 | } 186 | } 187 | 188 | @IBAction func titleFilterClicked(_ sender: UIButton) { 189 | sender.animateTouchDown().done { 190 | self.viewModel.articlesOrder = .title 191 | } 192 | } 193 | 194 | @IBAction func authorsFilterClicked(_ sender: UIButton) { 195 | sender.animateTouchDown().done { 196 | self.viewModel.articlesOrder = .authors 197 | } 198 | } 199 | 200 | @IBAction func defaultFilterClicked(_ sender: UIButton) { 201 | sender.animateTouchDown().done { 202 | self.viewModel.articlesOrder = .id 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/News Table/NewsTableViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import EZCoreData 11 | import PromiseKit 12 | 13 | // MARK: - Used to order the Articles 14 | enum ArticlesOrder: String { 15 | case id 16 | case authors 17 | case title 18 | } 19 | 20 | class NewsTableViewModel: NSObject { 21 | 22 | // MARK: - Data source 23 | var articles: [Article] = [] 24 | 25 | // MARK: - Injected properties: 26 | /// Core Data service 27 | var ezCoreData: EZCoreData! 28 | 29 | /// Delegate (to ViewController) 30 | weak var delegate: NewsCollectionViewDelegate? 31 | 32 | /// Coordinator delegate (to Coordinator) 33 | weak var coordinator: NewsInteractionProtocol? 34 | 35 | // MARK: - Handling Offline mode with message to the user 36 | override init() { 37 | super.init() 38 | startWatchingOfflineMode() 39 | } 40 | 41 | deinit { 42 | stopWatchingOfflineMode() 43 | } 44 | 45 | // MARK: - Search on the database when the user seearches for a term or chaanges the list ordering 46 | var articlesOrder: ArticlesOrder = .id { 47 | didSet { 48 | searchArticles(searchTerm, orderBy: articlesOrder, ascending: true) 49 | } 50 | } 51 | 52 | var searchTerm: String = "" { 53 | didSet { 54 | searchArticles(searchTerm, orderBy: articlesOrder, ascending: true) 55 | } 56 | } 57 | 58 | /// Bottom Bar Showing/Hidding current stare 59 | fileprivate var isShowingBottomView: Bool = false 60 | } 61 | 62 | // MARK: - Bottom Bar Show/Hide 63 | extension NewsTableViewModel { 64 | func toggledContraintForFilterView(_ height: CGFloat) -> CGFloat { 65 | isShowingBottomView = !isShowingBottomView 66 | return isShowingBottomView ? 0 : -height 67 | } 68 | } 69 | 70 | // MARK: - Offline Handler 71 | extension NewsTableViewModel: ObserveOfflineProtocol { 72 | @objc func handleOfflineSituation() { 73 | delegate?.displayMessage("NO_INTERNET_CONNECTION".localized) 74 | } 75 | } 76 | 77 | // MARK: - User Selected index path 78 | extension NewsTableViewModel: ListViewModelProtocol { 79 | func userDidSelect(indexPath: IndexPath) { 80 | let article = NewsTableViewModel.getObject(from: articles, with: indexPath) 81 | coordinator?.userDidSelectArticle(article) 82 | } 83 | } 84 | 85 | // MARK: - API Service: GET Articles 86 | extension NewsTableViewModel { 87 | func fetchAPIData() { 88 | firstly { () -> Promise<[[String: Any]]> in 89 | APIService.getArticlesList(ezCoreData.privateThreadContext) 90 | }.then { json -> Promise<[Article]?> in 91 | Article.importList(json, idKey: Constants.idKey, backgroundContext: self.ezCoreData.privateThreadContext) 92 | }.then { importedArticles -> Promise<[Article]?> in 93 | Article.deleteAll(except: importedArticles, backgroundContext: self.ezCoreData.privateThreadContext) 94 | }.asVoid().done { 95 | self.searchArticles(self.searchTerm, orderBy: self.articlesOrder, ascending: true) 96 | }.catch { error in 97 | self.delegate?.displayError(error: error, endRefreshing: true) 98 | } 99 | } 100 | } 101 | 102 | // MARK: - Core Data Service 103 | extension NewsTableViewModel { 104 | fileprivate func searchArticles(_ searchTerm: String = "", orderBy: ArticlesOrder = .id, ascending: Bool = true) { 105 | // Build NSPredicate 106 | var predicate: NSPredicate? 107 | if searchTerm.count > 0 { 108 | predicate = NSPredicate(format: "title CONTAINS[c] '\(searchTerm)' or authors CONTAINS[c] '\(searchTerm)'") 109 | } 110 | 111 | // Build NSSortDescriptor 112 | let sortDescriptor = NSSortDescriptor(key: articlesOrder.rawValue, ascending: true) 113 | 114 | // Read from the database 115 | do { 116 | articles = try Article.readAll(predicate: predicate, 117 | context: ezCoreData.mainThreadContext, 118 | sortDescriptors: [sortDescriptor]) 119 | DispatchQueue.main.async { 120 | self.delegate?.reloadData(endRefreshing: true) 121 | } 122 | } catch let e as NSError { 123 | print("ERROR: \(e.localizedDescription)") 124 | } 125 | } 126 | } 127 | 128 | // MARK: - Read Status (upon swipe to action) 129 | extension NewsTableViewModel { 130 | /// Updates the read status in the CoreData 131 | func toggleReadStatus(article: Article) { 132 | article.wasRead = !article.wasRead 133 | article.managedObjectContext?.saveContextToStore() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/NewsProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsProtocols.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 12/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Protocol to comunicate from ViewModel o Coordinator (MVVM-C) 12 | /// Protocol to comunicate from ViewModel o Coordinator (MVVM-C) 13 | protocol NewsInteractionProtocol: class { 14 | func userDidSelectArticle(_ selectedArticle: Article) 15 | } 16 | 17 | // MARK: - Protocol to comunicate from ViewModel o ViewController (MVVM-C) 18 | /// Protocol to comunicate from ViewModel o ViewController (MVVM-C) 19 | protocol NewsCollectionViewDelegate: class { 20 | func reloadData(endRefreshing: Bool) 21 | func displayError(error: Error, endRefreshing: Bool) 22 | func displayMessage(_ message: String) 23 | } 24 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/News/en.lproj/Table.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UILabel"; text = "Authors"; ObjectID = "0DG-J2-fNQ"; */ 3 | "0DG-J2-fNQ.text" = "Authors"; 4 | 5 | /* Class = "UILabel"; text = "Title"; ObjectID = "6Kc-xZ-Ucl"; */ 6 | "6Kc-xZ-Ucl.text" = "Title"; 7 | 8 | /* Class = "UINavigationItem"; title = "News"; ObjectID = "FFA-kA-ueH"; */ 9 | "FFA-kA-ueH.title" = "News"; 10 | 11 | /* Class = "UILabel"; text = "Tags"; ObjectID = "HKR-dG-26V"; */ 12 | "HKR-dG-26V.text" = "Tags"; 13 | 14 | /* Class = "UILabel"; text = "Content"; ObjectID = "IYV-hl-0oR"; */ 15 | "IYV-hl-0oR.text" = "Content"; 16 | 17 | /* Class = "UIButton"; normalTitle = "Authors"; ObjectID = "Iii-Oe-ysP"; */ 18 | "Iii-Oe-ysP.normalTitle" = "Authors"; 19 | 20 | /* Class = "UILabel"; text = "Time"; ObjectID = "L6G-J6-L0O"; */ 21 | "L6G-J6-L0O.text" = "Time"; 22 | 23 | /* Class = "UIBarButtonItem"; title = "Mark as read"; ObjectID = "LR6-wb-SSJ"; */ 24 | "LR6-wb-SSJ.title" = "Mark as read"; 25 | 26 | /* Class = "UILabel"; text = "Authors"; ObjectID = "Zq2-bg-1nD"; */ 27 | "Zq2-bg-1nD.text" = "Authors"; 28 | 29 | /* Class = "UIButton"; normalTitle = "Title"; ObjectID = "gdF-6U-Y92"; */ 30 | "gdF-6U-Y92.normalTitle" = "Title"; 31 | 32 | /* Class = "UILabel"; text = "Time"; ObjectID = "hGd-QF-n9M"; */ 33 | "hGd-QF-n9M.text" = "Time"; 34 | 35 | /* Class = "UILabel"; text = "Time"; ObjectID = "n0j-0D-Wnn"; */ 36 | "n0j-0D-Wnn.text" = "Time"; 37 | 38 | /* Class = "UIButton"; normalTitle = "Default"; ObjectID = "oGR-zU-eDK"; */ 39 | "oGR-zU-eDK.normalTitle" = "Default"; 40 | 41 | /* Class = "UIBarButtonItem"; title = "Item"; ObjectID = "pmL-Zl-Gvk"; */ 42 | "pmL-Zl-Gvk.title" = "Item"; 43 | 44 | /* Class = "UILabel"; text = "Author"; ObjectID = "rEW-Pr-Jxt"; */ 45 | "rEW-Pr-Jxt.text" = "Author"; 46 | 47 | /* Class = "UILabel"; text = "Title"; ObjectID = "xOJ-E0-QFQ"; */ 48 | "xOJ-E0-QFQ.text" = "Title"; 49 | 50 | /* Class = "UILabel"; text = "Title"; ObjectID = "y7R-FZ-AY2"; */ 51 | "y7R-FZ-AY2.text" = "Title"; 52 | 53 | /* Class = "UIBarButtonItem"; title = "Item"; ObjectID = "yU6-Zd-kSm"; */ 54 | "yU6-Zd-kSm.title" = "Item"; 55 | 56 | /* Class = "UINavigationItem"; title = "Articles"; ObjectID = "yW4-L6-Wfw"; */ 57 | "yW4-L6-Wfw.title" = "Articles"; 58 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/Custom Views/NewsCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyCustomView.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 13/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | open class NewsCardView: UIView, NibFileOwnerLoadable { 13 | 14 | @IBOutlet weak var imageView: UIImageView! 15 | @IBOutlet weak var titleLabel: UILabel! 16 | @IBOutlet weak var timeLabel: UILabel! 17 | @IBOutlet weak var authorsLabel: UILabel! 18 | 19 | // MARK: - Custom Init 20 | required public init?(coder aDecoder: NSCoder) { 21 | super.init(coder: aDecoder) 22 | loadNibContent() 23 | commonInit() 24 | } 25 | 26 | public override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | loadNibContent() 29 | commonInit() 30 | } 31 | 32 | open override func prepareForInterfaceBuilder() { // Used for updating the Interface Builder 33 | super.prepareForInterfaceBuilder() 34 | loadNibContent() 35 | commonInit() 36 | } 37 | 38 | fileprivate func commonInit() { 39 | // Do your view code-customization here 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/Custom Views/NewsCardView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Colfax-Bold 15 | 16 | 17 | Colfax-Regular 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/IBDesignable/DesignableButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersonView.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | open class DesignableButton: UIButton { 13 | 14 | // MARK: - Custom Init 15 | required public init?(coder aDecoder: NSCoder) { 16 | super.init(coder: aDecoder) 17 | commonInit() 18 | } 19 | 20 | public override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | commonInit() 23 | } 24 | 25 | open override func prepareForInterfaceBuilder() { // Used for updating the Interface Builder 26 | super.prepareForInterfaceBuilder() 27 | commonInit() 28 | } 29 | 30 | fileprivate func commonInit() { 31 | self.titleLabel?.font = UIFont.proximaNovaRegular(size: 18) 32 | self.setTitleColor(.white, for: .normal) 33 | self.backgroundColor = UIColor.mainColor 34 | 35 | layer.cornerRadius = 30 36 | layer.borderWidth = 1 37 | layer.borderColor = UIColor.clear.cgColor 38 | } 39 | 40 | // UIView Customizable from Interface Builder 41 | @IBInspectable open var borderColor: UIColor = UIColor.clear { 42 | didSet { 43 | layer.borderColor = borderColor.cgColor 44 | } 45 | } 46 | 47 | @IBInspectable open var borderWidth: CGFloat = 3 { 48 | didSet { 49 | layer.borderWidth = borderWidth 50 | } 51 | } 52 | 53 | @IBInspectable open var cornerRadius: CGFloat = 10 { 54 | didSet { 55 | layer.cornerRadius = cornerRadius 56 | layer.masksToBounds = true 57 | } 58 | } 59 | 60 | // UIButton Customizable from Interface Builder 61 | @IBInspectable open var spacingHorizontal: CGFloat = 0 { 62 | didSet { 63 | self.imageEdgeInsets = UIEdgeInsets(top: spacingVertical, left: 0, bottom: 0, right: spacingHorizontal) 64 | self.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacingHorizontal, bottom: spacingVertical, right: 0) 65 | } 66 | } 67 | 68 | @IBInspectable open var spacingVertical: CGFloat = 0 { 69 | didSet { 70 | self.imageEdgeInsets = UIEdgeInsets(top: spacingVertical, left: 0, bottom: 0, right: spacingHorizontal) 71 | self.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacingHorizontal, bottom: spacingVertical, right: 0) 72 | } 73 | } 74 | 75 | @IBInspectable open var spacing: CGFloat = 0 { 76 | didSet { 77 | let attributes = [NSAttributedString.Key.kern: spacing] as [NSAttributedString.Key: Any] 78 | self.titleLabel?.attributedText = NSAttributedString(string: (self.titleLabel?.text ?? "")!, 79 | attributes: attributes) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/IBDesignable/DesignableLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignableLabel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | public class DesignableLabel: UILabel { 13 | 14 | // MARK: - Custom Init 15 | required public init?(coder aDecoder: NSCoder) { 16 | super.init(coder: aDecoder) 17 | commonInit() 18 | } 19 | 20 | public override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | commonInit() 23 | } 24 | 25 | open override func prepareForInterfaceBuilder() { // Used for updating the Interface Builder 26 | super.prepareForInterfaceBuilder() 27 | commonInit() 28 | } 29 | 30 | fileprivate func commonInit() { 31 | self.font = UIFont.proximaNovaRegular(size: 16) 32 | self.tintColor = UIColor.mainColor 33 | } 34 | 35 | // MARK: - Formatting 36 | @IBInspectable public var lineHeight: CGFloat = 0 { 37 | didSet { 38 | let attributedString = NSMutableAttributedString(string: self.text!) 39 | let paragraphStyle = NSMutableParagraphStyle() 40 | paragraphStyle.lineSpacing = lineHeight - self.font.pointSize 41 | paragraphStyle.alignment = self.textAlignment 42 | var count = 0 43 | if let text = self.text { 44 | count = text.count 45 | } 46 | attributedString.addAttribute(NSAttributedString.Key(rawValue: "NSParagraphStyleAttributeName"), 47 | value: paragraphStyle, 48 | range: NSRange(0...count)) 49 | self.attributedText = attributedString 50 | } 51 | } 52 | 53 | @IBInspectable public var spacing: CGFloat = 0 { 54 | didSet { 55 | let attributes = [NSAttributedString.Key.kern: spacing] as [NSAttributedString.Key: Any] 56 | self.attributedText = NSAttributedString(string: self.text!, attributes: attributes) 57 | } 58 | } 59 | 60 | // MARK: - Shapes 61 | 62 | @IBInspectable open var borderColor: UIColor = UIColor.clear { 63 | didSet { 64 | layer.borderColor = borderColor.cgColor 65 | } 66 | } 67 | 68 | @IBInspectable open var borderWidth: CGFloat = 0 { 69 | didSet { 70 | layer.borderWidth = borderWidth 71 | } 72 | } 73 | 74 | @IBInspectable open var cornerRadius: CGFloat = 0 { 75 | didSet { 76 | layer.cornerRadius = cornerRadius 77 | layer.masksToBounds = true 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/IBDesignable/DesignableTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignableTextField.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 05/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol DesignableTextFieldDelegate: class { 12 | func deleteKeyPressed(sender: DesignableTextField) 13 | } 14 | 15 | @IBDesignable 16 | open class DesignableTextField: UITextField { 17 | 18 | // MARK: - Custom Init 19 | required public init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | commonInit() 22 | } 23 | 24 | public override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | commonInit() 27 | } 28 | 29 | open override func prepareForInterfaceBuilder() { // Used for updating the Interface Builder 30 | super.prepareForInterfaceBuilder() 31 | commonInit() 32 | } 33 | 34 | fileprivate func commonInit() { 35 | self.backgroundColor = .gray5 36 | self.font = UIFont.proximaNovaRegular(size: 15) 37 | self.borderStyle = .none 38 | } 39 | 40 | weak var designableDelegate: DesignableTextFieldDelegate? 41 | 42 | // MARK: - Shapes 43 | @IBInspectable open var borderColor: UIColor = UIColor.clear { 44 | didSet { 45 | layer.borderColor = borderColor.cgColor 46 | } 47 | } 48 | 49 | @IBInspectable open var borderWidth: CGFloat = 10 { 50 | didSet { 51 | layer.borderWidth = borderWidth 52 | } 53 | } 54 | 55 | @IBInspectable var cornerRadius: CGFloat = 0 { 56 | didSet { 57 | layer.cornerRadius = cornerRadius 58 | layer.masksToBounds = true 59 | } 60 | } 61 | 62 | // MARK: - TextField 63 | 64 | @IBInspectable var insetX: CGFloat = 0 65 | @IBInspectable var insetY: CGFloat = 0 66 | 67 | @IBInspectable var placeholderColor: UIColor = UIColor.white { 68 | didSet { 69 | if let placeholder = placeholder { 70 | let attributes = [NSAttributedString.Key.foregroundColor: placeholderColor] 71 | attributedPlaceholder = NSAttributedString(string: placeholder, 72 | attributes: attributes) 73 | } 74 | } 75 | } 76 | 77 | @IBInspectable var placeholderSpacing: CGFloat = 0 { 78 | didSet { 79 | if let placeholder = placeholder { 80 | let attributes = [NSAttributedString.Key.kern: placeholderSpacing] 81 | attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) 82 | } 83 | } 84 | } 85 | 86 | // Detect delete key 87 | override open func deleteBackward() { 88 | super.deleteBackward() 89 | self.designableDelegate?.deleteKeyPressed(sender: self) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/IBDesignable/PaddingLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaddingLabel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 08/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable class PaddingLabel: UILabel { 12 | 13 | @IBInspectable var topInset: CGFloat = 5.0 14 | @IBInspectable var bottomInset: CGFloat = 5.0 15 | @IBInspectable var leftInset: CGFloat = 7.0 16 | @IBInspectable var rightInset: CGFloat = 7.0 17 | 18 | override func drawText(in rect: CGRect) { 19 | let insets = UIEdgeInsets.init(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) 20 | super.drawText(in: rect.inset(by: insets)) 21 | } 22 | 23 | override var intrinsicContentSize: CGSize { 24 | let size = super.intrinsicContentSize 25 | return CGSize(width: size.width + leftInset + rightInset, 26 | height: size.height + topInset + bottomInset) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Views/View Extensions/UIView+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Animation.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 15/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PromiseKit 11 | 12 | extension UIView { 13 | public func animateTouchDown() -> Guarantee { 14 | return UIView.animate(.promise, duration: 0.09) { 15 | self.alpha = 0.89 16 | self.transform = CGAffineTransform(scaleX: 0.89, y: 0.89) 17 | }.then { _ -> Guarantee in 18 | UIView.animate(.promise, duration: 0.09) { 19 | self.alpha = 1 20 | self.transform = CGAffineTransform(scaleX: 1, y: 1) 21 | } 22 | }.asVoid() 23 | } 24 | 25 | public func animateColorChange(toColor: UIColor) -> Guarantee { 26 | return UIView.animate(.promise, duration: 0.3) { 27 | self.backgroundColor = toColor 28 | }.asVoid() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Welcome/Welcome.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Welcome/WelcomeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeCoordinator.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EZCoreData 11 | 12 | protocol WelcomeViewControllerDelegate: class { 13 | func userDidSelectStoryboard(_ storyboardName: StoryboardName) 14 | } 15 | 16 | class WelcomeCoordinator: Coordinator { 17 | private let presenter: UINavigationController 18 | private var ezCoreData: EZCoreData 19 | 20 | private weak var welcomeViewController: WelcomeViewController? 21 | 22 | init(presenter: UINavigationController, ezCoreData: EZCoreData) { 23 | self.presenter = presenter 24 | self.ezCoreData = ezCoreData 25 | } 26 | 27 | override func start() { 28 | // View Model 29 | let viewModel = WelcomeViewModel() 30 | viewModel.coordinator = self 31 | 32 | // View Controller: 33 | guard let welcomeViewController = WelcomeViewController.fromStoryboard(.welcome) else { return } 34 | welcomeViewController.viewModel = viewModel 35 | 36 | // Present View Controller: 37 | presenter.pushViewController(welcomeViewController, animated: true) 38 | setDeallocallable(with: welcomeViewController) 39 | self.welcomeViewController = welcomeViewController 40 | } 41 | } 42 | 43 | extension WelcomeCoordinator: WelcomeViewControllerDelegate { 44 | func userDidSelectStoryboard(_ storyboardName: StoryboardName) { 45 | switch storyboardName { 46 | case .table: 47 | setupArticleListScreen() 48 | case .auth: 49 | setupAuthHomeCoordinator() 50 | case .collection: 51 | setupNewsCollectionScreen() 52 | default: 53 | break 54 | } 55 | 56 | } 57 | 58 | fileprivate func setupNewsCollectionScreen() { 59 | // Setups NewsCollectionCoordinator 60 | let newsCollectionCoordinator = NewsCollectionCoordinator(presenter: presenter, 61 | ezCoreData: ezCoreData) 62 | startCoordinator(newsCollectionCoordinator) 63 | } 64 | 65 | fileprivate func setupArticleListScreen() { 66 | // Setups NewsTableCoordinator 67 | let newsTableCoordinator = NewsTableCoordinator(presenter: presenter, ezCoreData: ezCoreData) 68 | startCoordinator(newsTableCoordinator) 69 | } 70 | 71 | fileprivate func setupAuthHomeCoordinator() { 72 | // Setups NewsTableCoordinator 73 | let authHomeCoordinator = AuthHomeCoordinator(presenter: presenter, ezCoreData: ezCoreData) 74 | startCoordinator(authHomeCoordinator) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Welcome/WelcomeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeTableViewCell.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WelcomeTableViewCell: UITableViewCell { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | } 16 | 17 | override func setSelected(_ selected: Bool, animated: Bool) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Welcome/WelcomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeViewController.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WelcomeViewController: CoordinatedViewController { 12 | 13 | @IBOutlet weak var tableView: UITableView! 14 | 15 | var viewModel: WelcomeViewModel! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Inits Table View 21 | tableView.dataSource = self 22 | tableView.delegate = self 23 | } 24 | } 25 | 26 | extension WelcomeViewController: UITableViewDataSource { 27 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 28 | return viewModel.objects.count 29 | } 30 | 31 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 32 | let cell = WelcomeTableViewCell.dequeuedReusableCell(tableView, indexPath: indexPath) 33 | let storyboardName = WelcomeViewModel.getObject(from: viewModel.objects, with: indexPath) 34 | cell.textLabel?.text = storyboardName.rawValue 35 | return cell 36 | } 37 | } 38 | 39 | extension WelcomeViewController: UITableViewDelegate { 40 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 41 | viewModel.userDidSelect(indexPath: indexPath) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Scenes/Welcome/WelcomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeViewModel.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 11/02/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WelcomeViewModel: NSObject { 12 | // MARK: - Injected Dependencies 13 | weak var coordinator: WelcomeViewControllerDelegate? 14 | 15 | // MARK: - Properties 16 | public let objects: [StoryboardName] = [.auth, .table, .collection] 17 | } 18 | 19 | // MARK: - User Selected index path 20 | extension WelcomeViewModel: ListViewModelProtocol { 21 | func userDidSelect(indexPath: IndexPath) { 22 | let storyboardName = WelcomeViewModel.getObject(from: objects, with: indexPath) 23 | coordinator?.userDidSelectStoryboard(storyboardName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Services/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 28/12/18. 6 | // Copyright © 2018 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Alamofire 11 | import CoreData 12 | import EZCoreData 13 | import PromiseKit 14 | 15 | // MARK: - Result Handling 16 | /// Handles any kind of results 17 | enum APIResult { 18 | /// Handles success results 19 | case success(_ result: Value?) 20 | 21 | /// Handles failure results 22 | case failure(_ error: CustomError) 23 | } 24 | 25 | // MARK: - String Endpoints 26 | /// List of Endpoints 27 | struct APIPath { 28 | static let rootUrl: String = "https://private-0d75e8-cklchallenge.apiary-mock.com" 29 | static let articleURL: String = "\(rootUrl)/article" 30 | } 31 | 32 | // MARK: - The supported requests 33 | /// Used to manage all API requests in an async way, handling their responses and returning a APIResult enum 34 | struct APIService { 35 | 36 | static func getArticlesList(_ context: NSManagedObjectContext) -> Promise<[[String: Any]]> { 37 | return Alamofire.request(APIPath.articleURL).validate().responseJSON() 38 | .map { (json, _) -> [[String: Any]] in 39 | 40 | guard let jsonDict = json as? [[String: Any]] else { 41 | throw DefaultError.unknownError 42 | } 43 | 44 | return jsonDict 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Reachability 50 | extension APIService: EnableReachabilityProtocol { 51 | static let reachabilityManager = NetworkReachabilityManager() 52 | } 53 | 54 | // MARK: - Helper methods 55 | extension APIService { 56 | /// Handles response JSON, dealing with the common errors 57 | private static func handleResponseJSON(result resultData: Alamofire.Result, 58 | inputType: U.Type, 59 | outputType: T.Type, 60 | completion: @escaping (APIResult) -> Void, 61 | _ code: @escaping (U) throws -> Void) { 62 | DispatchQueue.global(qos: .userInitiated).async { 63 | switch resultData { 64 | 65 | case .success(let value): 66 | if let jsonArray = value as? U { 67 | do { 68 | try code(jsonArray) 69 | } catch let error { 70 | self.modelInconsistencyError(error, completion) 71 | } 72 | } else { 73 | modelInconsistencyError(nil, completion) 74 | } 75 | 76 | case .failure(let error): 77 | Logger.log(error, verboseLevel: .error) 78 | completion(.failure(CustomError(error: error))) 79 | } 80 | } 81 | } 82 | 83 | /// Regular model inconsistency error 84 | fileprivate static func modelInconsistencyError(_ error: Error?, _ completion: (APIResult) -> Void) { 85 | let errorStatus = CustomError(code: .customError, error: error) 86 | errorStatus.localizedDescription = "Model inconsistency found when parsing data from API." 87 | Logger.log(errorStatus, verboseLevel: .error) 88 | completion(.failure(errorStatus)) 89 | } 90 | } 91 | 92 | // MARK: - Request Cancellation 93 | extension APIService { 94 | /// Cancels all Alamofire requests (dataTasks, downloadTasks, uploadTasks) 95 | static func cancelAllRequests(with url: String? = nil) { 96 | let sessionManager = Alamofire.SessionManager.default 97 | sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in 98 | dataTasks.forEach { 99 | if self.compare(url: url, on: $0) { $0.cancel() } 100 | } 101 | uploadTasks.forEach { 102 | if self.compare(url: url, on: $0) { $0.cancel() } 103 | } 104 | downloadTasks.forEach { 105 | if self.compare(url: url, on: $0) { $0.cancel() } 106 | } 107 | } 108 | } 109 | 110 | /// Checks if `url` is the one used in the `sessionTask`. If the url is nil, returns true! 111 | private static func compare(url: String?, on sessionTask: URLSessionTask) -> Bool { 112 | guard let url = url else { return true } 113 | guard let taskUrl = sessionTask.originalRequest?.url?.absoluteString else { return false } 114 | return url == taskUrl 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Supporting Files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 27/12/18. 6 | // Copyright © 2018 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | import Fabric 12 | import Crashlytics 13 | import Flurry_iOS_SDK 14 | #if DEBUG 15 | import Bagel 16 | #endif 17 | 18 | class AppDelegate: UIResponder, UIApplicationDelegate { 19 | 20 | var window: UIWindow? 21 | private var applicationCoordinator: ApplicationCoordinator? 22 | 23 | func application(_ application: UIApplication, 24 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 25 | 26 | //import Bagel 27 | #if DEBUG 28 | Bagel.start() 29 | #endif 30 | 31 | // Init Fabric and Crashlytics 32 | Fabric.with([Crashlytics.self]) 33 | 34 | // Init Flurry 35 | Flurry.startSession(Constants.flurryAPIKey, with: FlurrySessionBuilder 36 | .init() 37 | .withCrashReporting(true) 38 | .withLogLevel(FlurryLogLevelNone)) 39 | 40 | // Setup Window and Application Coordinator 41 | window = UIWindow(frame: UIScreen.main.bounds) 42 | applicationCoordinator = ApplicationCoordinator(window: window!) 43 | applicationCoordinator?.start() 44 | 45 | return true 46 | } 47 | 48 | func applicationWillResignActive(_ application: UIApplication) { 49 | // Sent when the application is about to move from active to inactive state. 50 | // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) 51 | // or when the user quits the application and it begins the transition to the background state. 52 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. 53 | // Games should use this method to pause the game. 54 | } 55 | 56 | func applicationDidEnterBackground(_ application: UIApplication) { 57 | // Use this method to release shared resources, save user data, invalidate timers, and store 58 | // enough application state information to restore your application to its current state 59 | // in case it is terminated later. 60 | // If your application supports background execution, this method is called 61 | // instead of applicationWillTerminate: when the user quits. 62 | } 63 | 64 | func applicationWillEnterForeground(_ application: UIApplication) { 65 | // Called as part of the transition from the background to the active state; 66 | // Here you can undo many of the changes made on entering the background. 67 | } 68 | 69 | func applicationDidBecomeActive(_ application: UIApplication) { 70 | // Restart any tasks that were paused (or not yet started) while the application was inactive. 71 | // If the application was previously in the background, optionally refresh the user interface. 72 | } 73 | 74 | func applicationWillTerminate(_ application: UIApplication) { 75 | // Called when the application is about to terminate. 76 | // Save data if appropriate. See also applicationDidEnterBackground:. 77 | // Saves changes in the application's managed object context before the application terminates. 78 | applicationCoordinator?.ezCoreData.privateThreadContext.saveContextToStore() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | iOS Bootstrap 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | Fabric 24 | 25 | APIKey 26 | 5b37db55b54bde8390da1be0ad8f8ad623c63660 27 | Kits 28 | 29 | 30 | KitInfo 31 | 32 | KitName 33 | Crashlytics 34 | 35 | 36 | 37 | LSRequiresIPhoneOS 38 | 39 | UIAppFonts 40 | 41 | ProximaNova-Black.ttf 42 | ProximaNova-Bold.ttf 43 | ProximaNova-Bold-Italic.ttf 44 | ProximaNova-Extra-Bold.ttf 45 | ProximaNova-Light-Italic.ttf 46 | ProximaNova-Light.ttf 47 | ProximaNova-Regular-Italic.ttf 48 | ProximaNova-Regular.ttf 49 | ProximaNova-Semibold-Italic.ttf 50 | ProximaNova-Semibold.ttf 51 | Colfax-Regular.ttf 52 | Colfax-Medium.ttf 53 | Colfax-Bold.ttf 54 | 55 | UILaunchStoryboardName 56 | LaunchScreen 57 | UIRequiredDeviceCapabilities 58 | 59 | armv7 60 | 61 | UISupportedInterfaceOrientations 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | UISupportedInterfaceOrientations~ipad 68 | 69 | UIInterfaceOrientationPortrait 70 | UIInterfaceOrientationPortraitUpsideDown 71 | UIInterfaceOrientationLandscapeLeft 72 | UIInterfaceOrientationLandscapeRight 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /iOS Bootstrap/iOS Bootstrap/Supporting Files/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // iOS Bootstrap 4 | // 5 | // Created by Marcelo Salloum dos Santos on 22/01/19. 6 | // Copyright © 2019 Marcelo Salloum dos Santos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | let isRunningTests = NSClassFromString("XCTestCase") != nil 13 | let appDelegateClass = isRunningTests ? nil : NSStringFromClass(AppDelegate.self) 14 | let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer.self, 15 | capacity: Int(CommandLine.argc)) 16 | UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, appDelegateClass) 17 | --------------------------------------------------------------------------------