├── .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 | [](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 |
--------------------------------------------------------------------------------