├── iOS-MVVM-Template ├── Api │ ├── Models │ │ ├── VoidEnvelope.swift │ │ ├── ErrorCode.swift │ │ ├── Templates │ │ │ └── AccessTokenTemplates.swift │ │ ├── AccessToken.swift │ │ └── ErrorEnvelope.swift │ ├── EnvironmentType.swift │ ├── Lib │ │ ├── RequestMethod.swift │ │ └── Route.swift │ ├── Service.swift │ ├── MockService.swift │ ├── Service+RequestHelpers.swift │ ├── ServerConfig.swift │ ├── Extensions │ │ └── URLSession+RequestHelpers.swift │ └── ServiceType.swift ├── Resources │ └── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.swift ├── Configs │ ├── Global.swift │ └── Secrets.swift ├── Library │ ├── Extensions │ │ ├── DictionaryExtensions.swift │ │ ├── OptionalExtensions.swift │ │ ├── UITableViewExtensions.swift │ │ ├── UIScreenExtensions.swift │ │ ├── EncodableExtensions.swift │ │ ├── UIFontExtensions.swift │ │ ├── DecodableExtensions.swift │ │ ├── StringExtensions.swift │ │ ├── UIAlertControllerExtensions.swift │ │ ├── ErrorExtensions.swift │ │ ├── UIViewController+Preparation.swift │ │ ├── UIColorExtensions.swift │ │ ├── UIViewExtensions.swift │ │ └── UIViewControllerExtensions.swift │ ├── PushRegistrationType.swift │ ├── App.swift │ ├── Storyboard.swift │ ├── Notifications.swift │ ├── Environment.swift │ ├── Nib.swift │ ├── PushRegistration.swift │ ├── Keyboard.swift │ └── AppEnvironment.swift ├── DataModels │ ├── Templates │ │ └── UserTemplates.swift │ └── User.swift ├── DataSources │ └── Base │ │ ├── ValueCell.swift │ │ └── ValueCellDataSource.swift ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard ├── Info.plist ├── ViewModels │ └── AppDelegateViewModel.swift └── AppDelegate.swift ├── iOS-MVVM-Template.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── lebron.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ ├── iOS-MVVM-Template_Staging.xcscheme │ │ └── iOS-MVVM-Template.xcscheme └── project.pbxproj ├── Podfile ├── iOS-MVVM-Template.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── .swiftlint.yml ├── iOS-MVVM-TemplateTests ├── Info.plist ├── Api │ ├── ServiceTests.swift │ └── ServiceTypeTests.swift └── DataSources │ └── ValueCellDataSourceTest.swift ├── Podfile.lock ├── .gitignore └── README.md /iOS-MVVM-Template/Api/Models/VoidEnvelope.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct VoidEnvelope: Codable {} 4 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/EnvironmentType.swift: -------------------------------------------------------------------------------- 1 | enum EnvironmentType { 2 | case production 3 | case staging 4 | } 5 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Models/ErrorCode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ErrorCode: Int { 4 | case needUpdate = 10011 5 | } 6 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Lib/RequestMethod.swift: -------------------------------------------------------------------------------- 1 | enum RequestMethod: String { 2 | case GET 3 | case POST 4 | case PUT 5 | case DELETE 6 | } 7 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Models/Templates/AccessTokenTemplates.swift: -------------------------------------------------------------------------------- 1 | extension AccessToken { 2 | static let template = AccessToken(token: "template") 3 | } 4 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController { 4 | 5 | override func viewDidLoad() { 6 | super.viewDidLoad() 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Configs/Global.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | static let appBackgroundColor: UIColor = .hex("#202020") 5 | } 6 | 7 | enum Constant { 8 | static let jsonDecoder = JSONDecoder() 9 | } 10 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/DictionaryExtensions.swift: -------------------------------------------------------------------------------- 1 | extension Dictionary { 2 | func withAllValuesFrom(_ other: Dictionary) -> Dictionary { 3 | var result = self 4 | other.forEach { result[$0] = $1 } 5 | return result 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/OptionalExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Optional { 4 | func doIfSome(_ body: (Wrapped) -> Void) { 5 | if let value = self.optional { 6 | body(value) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UITableViewExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableView { 4 | func registerCellClass(_ cellClass: UITableViewCell.Type) { 5 | register(cellClass, forCellReuseIdentifier: cellClass.defaultReusableId) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '11.4' 4 | inhibit_all_warnings! 5 | 6 | target 'iOS-MVVM-Template' do 7 | use_frameworks! 8 | 9 | pod 'JWT' 10 | pod 'Lebron-ReactiveExtensions' 11 | pod 'PKHUD' 12 | pod 'ReactiveSwift', '~> 6.0' 13 | 14 | end 15 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIScreenExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIScreen { 4 | static var width: CGFloat { 5 | return UIScreen.main.bounds.width 6 | } 7 | 8 | static var height: CGFloat { 9 | return UIScreen.main.bounds.height 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Lib/Route.swift: -------------------------------------------------------------------------------- 1 | enum Route { 2 | case yourRoute 3 | 4 | var requestProperties: (method: RequestMethod, path: String, query: [String: Any]) { 5 | switch self { 6 | case .yourRoute: 7 | return (.GET, "/users/me", [:]) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/PushRegistrationType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserNotifications 3 | import ReactiveSwift 4 | 5 | protocol PushRegistrationType { 6 | static func register(for options: UNAuthorizationOptions) -> SignalProducer 7 | static func hasAuthorizedNotifications() -> SignalProducer 8 | } 9 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/EncodableExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let jsonEncoder = JSONEncoder() 4 | 5 | extension Encodable { 6 | var dictionary: [String: Any] { 7 | let obj = try? JSONSerialization.jsonObject(with: jsonEncoder.encode(self)) 8 | return obj as? [String: Any] ?? [:] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/App.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum App { 4 | static let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 5 | static let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String 6 | static let currentVersionDescription = "\(versionNumber ?? "")(\(buildNumber ?? ""))" 7 | } 8 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIFontExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | enum SFProTextFontStyle: String { 5 | case regular = "SFProText-Regular" 6 | } 7 | 8 | static func SFProText(style: SFProTextFontStyle, size: CGFloat) -> UIFont { 9 | return UIFont(name: style.rawValue, size: size)! 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/DecodableExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Decodable { 4 | static func decodeJSONDictionary(_ json: Any) -> Self? { 5 | if let data = try? JSONSerialization.data(withJSONObject: json, options: []), 6 | let value = try? JSONDecoder().decode(Self.self, from: data) { 7 | return value 8 | } 9 | return nil 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Configs/Secrets.swift: -------------------------------------------------------------------------------- 1 | enum Secrets { 2 | static let isMockService = false 3 | static let jwtSecret = "YourSecret" 4 | 5 | enum Api { 6 | // swiftlint:disable nesting 7 | enum Endpoint { 8 | static let production = "https://api.example.com/api" 9 | static let staging = "https://api-staging.example.com/api" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Storyboard.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum Storyboard: String { 4 | case YourStoryboard 5 | 6 | func instantiate(_ viewController: VC.Type) -> VC { 7 | guard let vc = UIStoryboard(name: rawValue, bundle: nil) 8 | .instantiateViewController(withIdentifier: VC.typeName) as? VC else { 9 | fatalError("Couldn't instantiate \(VC.typeName) from \(rawValue)") 10 | } 11 | return vc 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - ./ 3 | excluded: # paths to ignore during linting. Takes precedence over `included`. 4 | - Pods 5 | - Carthage 6 | disabled_rules: # rule identifiers to exclude from running 7 | - type_name 8 | - large_tuple 9 | - todo 10 | - identifier_name 11 | - function_parameter_count 12 | cyclomatic_complexity: 13 | warning: 20 14 | error: 30 15 | function_body_length: 16 | warning: 75 17 | error: 100 18 | file_length: 19 | warning: 800 20 | error: 1000 21 | type_body_length: 22 | warning: 400 23 | error: 500 24 | identifier_name: 25 | excluded: id # id is allowed 26 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Notifications.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CurrentUserNotifications { 4 | static let sessionStarted = "CurrentUserNotifications.sessionStarted" 5 | static let sessionEnded = "CurrentUserNotifications.sessionEnded" 6 | static let currentUserUpdated = "CurrentUserNotifications.sessionUpdated" 7 | } 8 | 9 | extension Notification.Name { 10 | static let sessionStarted = Notification.Name(rawValue: CurrentUserNotifications.sessionStarted) 11 | static let sessionEnded = Notification.Name(rawValue: CurrentUserNotifications.sessionEnded) 12 | static let currentUserUpdated = Notification.Name(rawValue: CurrentUserNotifications.currentUserUpdated) 13 | } 14 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/DataModels/Templates/UserTemplates.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension User { 4 | static let template: User = { 5 | do { 6 | let data = Data(userJson.utf8) 7 | let user = try Constant.jsonDecoder.decode(User.self, from: data) 8 | return user 9 | } catch { 10 | fatalError("decode userJson error: \(error.localizedDescription)") 11 | } 12 | }() 13 | 14 | private static let userJson = """ 15 | { 16 | "token": "xxxxxxxxxxxxxxxxxxxxx", 17 | "user_id": "xxxxxxxxxxxxxxxxxxxxx", 18 | "username": "lebronjames", 19 | "first_name": "Lebron", 20 | "last_name": "James" 21 | } 22 | """ 23 | } 24 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/DataSources/Base/ValueCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol ValueCell: AnyObject { 4 | associatedtype Value 5 | static var defaultReusableId: String { get } 6 | func configureWith(value: Value) 7 | } 8 | 9 | extension UITableViewCell { 10 | static var defaultReusableId: String { 11 | return description() 12 | .components(separatedBy: ".") 13 | .dropFirst() 14 | .joined(separator: ".") 15 | } 16 | } 17 | 18 | extension UICollectionViewCell { 19 | static var defaultReusableId: String { 20 | return description() 21 | .components(separatedBy: ".") 22 | .dropFirst() 23 | .joined(separator: ".") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Properties 4 | extension String { 5 | var isEmail: Bool { 6 | let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" 7 | let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) 8 | return emailPred.evaluate(with: self) 9 | } 10 | } 11 | 12 | // MARK: - Methods 13 | extension String { 14 | func trim() -> String { 15 | return trimmingCharacters(in: .whitespacesAndNewlines) 16 | } 17 | } 18 | 19 | // MARK: - Optional 20 | extension Optional where Wrapped == String { 21 | var isEmpty: Bool { 22 | if let str = self { 23 | return str.isEmpty 24 | } 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIAlertControllerExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIAlertController { 4 | static func alert( 5 | title: String? = nil, 6 | message: String? = nil, 7 | confirm: String? = nil, 8 | handler: ((UIAlertAction) -> Void)? = nil 9 | ) -> UIAlertController { 10 | let alertController = UIAlertController( 11 | title: title, 12 | message: message, 13 | preferredStyle: .alert 14 | ) 15 | alertController.addAction( 16 | UIAlertAction( 17 | title: confirm ?? "OK", 18 | style: .cancel, 19 | handler: handler 20 | ) 21 | ) 22 | return alertController 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Service.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | struct Service: ServiceType { 5 | let serverConfig: ServerConfigType 6 | let accessToken: AccessTokenType? 7 | 8 | init(serverConfig: ServerConfigType = ServerConfig.production, 9 | accessToken: AccessTokenType? = nil) { 10 | self.serverConfig = serverConfig 11 | self.accessToken = accessToken 12 | } 13 | 14 | func login(accessToken: AccessTokenType) -> Service { 15 | return Service( 16 | serverConfig: serverConfig, 17 | accessToken: accessToken 18 | ) 19 | } 20 | 21 | func logout() -> Service { 22 | return Service( 23 | serverConfig: serverConfig, 24 | accessToken: nil 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/MockService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | struct MockService: ServiceType { 5 | let serverConfig: ServerConfigType 6 | let accessToken: AccessTokenType? 7 | 8 | init(serverConfig: ServerConfigType = ServerConfig.production, 9 | accessToken: AccessTokenType? = nil) { 10 | self.serverConfig = serverConfig 11 | self.accessToken = accessToken 12 | } 13 | 14 | func login(accessToken: AccessTokenType) -> MockService { 15 | return MockService( 16 | serverConfig: self.serverConfig, 17 | accessToken: accessToken 18 | ) 19 | } 20 | 21 | func logout() -> MockService { 22 | return MockService( 23 | serverConfig: self.serverConfig, 24 | accessToken: nil 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS-MVVM-TemplateTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Base64 (1.1.2) 3 | - JWT (2.2.0): 4 | - Base64 (~> 1.1.2) 5 | - Lebron-ReactiveExtensions (0.0.1): 6 | - ReactiveSwift (~> 6.1) 7 | - PKHUD (5.3.0) 8 | - ReactiveSwift (6.1.0) 9 | 10 | DEPENDENCIES: 11 | - JWT 12 | - Lebron-ReactiveExtensions 13 | - PKHUD 14 | - ReactiveSwift (~> 6.0) 15 | 16 | SPEC REPOS: 17 | https://github.com/CocoaPods/Specs.git: 18 | - Base64 19 | - JWT 20 | - Lebron-ReactiveExtensions 21 | - PKHUD 22 | - ReactiveSwift 23 | 24 | SPEC CHECKSUMS: 25 | Base64: cecfb41a004124895a7bcee567a89bae5a89d49b 26 | JWT: cf73c0ab0dac762df3de8a3f1164352089d97780 27 | Lebron-ReactiveExtensions: f915659ef5d898faee0622c33c93bd65d68e4f0b 28 | PKHUD: 98f3e4bc904b9c916f1c5bb6d765365b5357291b 29 | ReactiveSwift: b78c7259f8ca46368238c895ae33a6a3bf9f811c 30 | 31 | PODFILE CHECKSUM: 03f3f5895c230e3e1c1b5b21484fb027f0441411 32 | 33 | COCOAPODS: 1.10.0 34 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/xcuserdata/lebron.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iOS-MVVM-Template.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 6 11 | 12 | iOS-MVVM-Template_Staging.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 7 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | FDD85AAB23B4BB6B00D7B8DD 21 | 22 | primary 23 | 24 | 25 | FDD85AC123B4BB6D00D7B8DD 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /iOS-MVVM-TemplateTests/Api/ServiceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import iOS_MVVM_Template 3 | 4 | final class ServiceTests: XCTestCase { 5 | 6 | func testDefaults() { 7 | XCTAssertTrue(Service().serverConfig == ServerConfig.production) 8 | } 9 | 10 | func testEquals() { 11 | let s1 = Service() 12 | let s2 = Service(serverConfig: ServerConfig.staging) 13 | 14 | XCTAssertTrue(s1 == s1) 15 | XCTAssertTrue(s2 == s2) 16 | 17 | XCTAssertFalse(s1 == s2) 18 | } 19 | 20 | func testLogin() { 21 | let loggedOut = Service() 22 | let loggedIn = loggedOut.login(accessToken: AccessToken.template) 23 | 24 | XCTAssertTrue(loggedIn == Service(accessToken: AccessToken.template)) 25 | } 26 | 27 | func testLogout() { 28 | let loggedIn = Service(accessToken: AccessToken.template) 29 | let loggedOut = loggedIn.logout() 30 | 31 | XCTAssertTrue(loggedOut == Service()) 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Models/AccessToken.swift: -------------------------------------------------------------------------------- 1 | import JWT 2 | 3 | protocol AccessTokenType { 4 | var token: String { get } 5 | var jwtToken: String { get } 6 | } 7 | 8 | func == (lhs: AccessTokenType, rhs: AccessTokenType) -> Bool { 9 | return type(of: lhs) == type(of: rhs) && 10 | lhs.token == rhs.token 11 | } 12 | 13 | func == (lhs: AccessTokenType?, rhs: AccessTokenType?) -> Bool { 14 | return type(of: lhs) == type(of: rhs) && 15 | lhs?.token == rhs?.token 16 | } 17 | 18 | struct AccessToken: AccessTokenType, Codable { 19 | let token: String 20 | 21 | init(token: String) { 22 | self.token = token 23 | } 24 | 25 | var jwtToken: String { 26 | let algorithm = JWTAlgorithmFactory.algorithm(byName: "HS256") 27 | let payload: [String: Any] = ["token": token] 28 | let jwtToken = JWT.encodePayload( 29 | payload, 30 | withSecret: Secrets.jwtSecret, 31 | algorithm: algorithm 32 | ) ?? "" 33 | return jwtToken 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/ErrorExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSError { 4 | convenience init(code: Int = -1, reason: String) { 5 | let userInfo = [NSLocalizedDescriptionKey: reason, 6 | NSLocalizedFailureReasonErrorKey: reason] 7 | self.init(domain: "", code: code, userInfo: userInfo) 8 | } 9 | 10 | static func error(fromCode code: Int) -> Error { 11 | let userInfo = [ 12 | NSLocalizedDescriptionKey: errorReasonString(fromCode: code), 13 | NSLocalizedFailureReasonErrorKey: errorReasonString(fromCode: code) 14 | ] 15 | return NSError(domain: "", code: code, userInfo: userInfo) 16 | } 17 | 18 | private static func errorReasonString(fromCode code: Int) -> String { 19 | var errorString = "" 20 | if code >= 400 { 21 | errorString = "service unavailable" 22 | } else { 23 | errorString = "failed to connect to server" 24 | } 25 | return errorString 26 | } 27 | 28 | static var failedToDecodeJson: Error { 29 | return NSError(reason: "failed to decode json") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Environment.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | 3 | struct Environment { 4 | let apiService: ServiceType 5 | let currentUser: User? 6 | 7 | /// A scheduler to use for all time-based RAC operators. Default value is 8 | /// `QueueScheduler.mainQueueScheduler`. 9 | let scheduler: DateScheduler 10 | 11 | /// A type that manages registration for push notifications. 12 | let pushRegistrationType: PushRegistrationType.Type 13 | 14 | init( 15 | apiService: ServiceType = Service(), 16 | currentUser: User? = nil, 17 | pushRegistrationType: PushRegistrationType.Type = PushRegistration.self, 18 | scheduler: DateScheduler = QueueScheduler.main 19 | ) { 20 | self.apiService = apiService 21 | self.currentUser = currentUser 22 | self.pushRegistrationType = pushRegistrationType 23 | self.scheduler = scheduler 24 | } 25 | 26 | // MARK: - Getters 27 | 28 | var environmentType: EnvironmentType { 29 | return apiService.serverConfig.environment 30 | } 31 | 32 | var isStaging: Bool { 33 | return apiService.serverConfig.environment == .staging 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Nib.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum Nib: String { 4 | case YourCustomView 5 | } 6 | 7 | extension UITableView { 8 | func register(nib: Nib) { 9 | register(UINib(nibName: nib.rawValue, bundle: .main), forCellReuseIdentifier: nib.rawValue) 10 | } 11 | 12 | func registerHeaderFooter(nib: Nib) { 13 | register(UINib(nibName: nib.rawValue, bundle: .main), 14 | forHeaderFooterViewReuseIdentifier: nib.rawValue) 15 | } 16 | } 17 | 18 | protocol NibLoading { 19 | associatedtype CustomNibType 20 | 21 | static func fromNib(nib: Nib) -> CustomNibType? 22 | } 23 | 24 | extension NibLoading { 25 | static func fromNib(nib: Nib) -> Self? { 26 | guard let view = UINib(nibName: nib.rawValue, bundle: .main) 27 | .instantiate(withOwner: self, options: nil) 28 | .first as? Self else { 29 | assertionFailure("Nib not found") 30 | return nil 31 | } 32 | 33 | return view 34 | } 35 | 36 | func view(fromNib nib: Nib) -> UIView? { 37 | return UINib(nibName: nib.rawValue, bundle: .main).instantiate(withOwner: self, options: nil).first 38 | as? UIView 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Service+RequestHelpers.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | 3 | extension Service { 4 | private static let session = URLSession(configuration: .default) 5 | 6 | func request(_ route: Route) -> SignalProducer { 7 | let properties = route.requestProperties 8 | let url = serverConfig.apiBaseUrl 9 | .appendingPathComponent(properties.path) 10 | let request = preparedRequest( 11 | forURL: url, 12 | method: properties.method, 13 | query: properties.query 14 | ) 15 | return Service.session.rac_dataResponse(request) 16 | .flatMap(decodeModel) 17 | } 18 | 19 | // MARK: - Private 20 | 21 | private func decodeModel(data: Data) -> SignalProducer { 22 | return SignalProducer(value: data) 23 | .flatMap { (data) -> SignalProducer in 24 | do { 25 | let model = try Constant.jsonDecoder.decode(T.self, from: data) 26 | return .init(value: model) 27 | } catch { 28 | return .init(error: .couldNotDecodeJSON(error)) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/ServerConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol ServerConfigType { 4 | var apiBaseUrl: URL { get } 5 | var environment: EnvironmentType { get } 6 | } 7 | 8 | func == (lhs: ServerConfigType, rhs: ServerConfigType) -> Bool { 9 | return type(of: lhs) == type(of: rhs) && 10 | lhs.apiBaseUrl == rhs.apiBaseUrl && 11 | lhs.environment == rhs.environment 12 | } 13 | 14 | struct ServerConfig: ServerConfigType { 15 | private(set) var apiBaseUrl: URL 16 | private(set) var environment: EnvironmentType 17 | 18 | static let production: ServerConfig = ServerConfig( 19 | apiBaseUrl: URL(string: Secrets.Api.Endpoint.production)!, 20 | environment: .production 21 | ) 22 | 23 | static let staging: ServerConfig = ServerConfig( 24 | apiBaseUrl: URL(string: Secrets.Api.Endpoint.staging)!, 25 | environment: .staging 26 | ) 27 | 28 | init(apiBaseUrl: URL, 29 | environment: EnvironmentType = .production) { 30 | self.apiBaseUrl = apiBaseUrl 31 | self.environment = environment 32 | } 33 | 34 | static func config(for environment: EnvironmentType) -> ServerConfigType { 35 | switch environment { 36 | case .production: 37 | return ServerConfig.production 38 | case .staging: 39 | return ServerConfig.staging 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIViewController+Preparation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private func swizzle(_ vc: UIViewController.Type) { 4 | [(#selector(vc.viewDidLoad), #selector(vc.custom_ViewDidLoad))] 5 | .forEach { (original, swizzled) in 6 | guard let originalMethod = class_getInstanceMethod(vc, original), 7 | let swizzledMethod = class_getInstanceMethod(vc, swizzled) else { 8 | return 9 | } 10 | 11 | let didAddViewDidLoadMethod = class_addMethod( 12 | vc, 13 | original, 14 | method_getImplementation(swizzledMethod), 15 | method_getTypeEncoding(swizzledMethod) 16 | ) 17 | if didAddViewDidLoadMethod { 18 | class_replaceMethod( 19 | vc, 20 | swizzled, method_getImplementation(originalMethod), 21 | method_getTypeEncoding(originalMethod) 22 | ) 23 | } else { 24 | method_exchangeImplementations(originalMethod, swizzledMethod) 25 | } 26 | } 27 | } 28 | 29 | private var hasSwizzled = false 30 | 31 | extension UIViewController { 32 | static func doBadSwizzleStuff() { 33 | guard !hasSwizzled else { return } 34 | 35 | hasSwizzled = true 36 | swizzle(self) 37 | } 38 | 39 | @objc internal func custom_ViewDidLoad() { 40 | custom_ViewDidLoad() 41 | bindViewModel() 42 | } 43 | 44 | /// The entry point to bind all view model outputs. Called just before `viewDidLoad`. 45 | @objc func bindViewModel() { 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/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 | -------------------------------------------------------------------------------- /.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 | *.DS_Store 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 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 | Carthage/Checkout 58 | Carthage/ 59 | Carthage/Cachefile 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | Gemfile 73 | Gemfile.lock 74 | fastlane/Pluginfile 75 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/PushRegistration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserNotifications 3 | import ReactiveSwift 4 | 5 | struct PushRegistration: PushRegistrationType { 6 | 7 | /// Returns a signal producer that emits an option `Bool` value representing whether or not the user 8 | /// granted the requested push notification permissions. This value is not returned on iOS versions < 10.0. 9 | /// The returned producer emits once and completes. 10 | /// - Parameter options: The types to register that we will request permissions for. 11 | /// - returns: A signal producer. 12 | static func register(for options: UNAuthorizationOptions) -> SignalProducer { 13 | return SignalProducer { observer, _ in 14 | UNUserNotificationCenter.current() 15 | .requestAuthorization(options: options, completionHandler: { isGranted, _ in 16 | if isGranted { 17 | DispatchQueue.main.async { 18 | UIApplication.shared.registerForRemoteNotifications() 19 | } 20 | } 21 | 22 | observer.send(value: isGranted) 23 | observer.sendCompleted() 24 | }) 25 | } 26 | } 27 | 28 | /// Returns a signal producer that emits a `Bool` value representing whether the user has allowed push 29 | /// notification permissions in the past. 30 | /// The returned producer emits once and completes 31 | /// - returns: A signal producer. 32 | static func hasAuthorizedNotifications() -> SignalProducer { 33 | return SignalProducer { observer, _ in 34 | UNUserNotificationCenter.current().getNotificationSettings { settings in 35 | observer.send(value: settings.authorizationStatus == .authorized) 36 | observer.sendCompleted() 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIColorExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | static func RGB( 5 | _ r: CGFloat, 6 | _ g: CGFloat, 7 | _ b: CGFloat, 8 | _ alpha: CGFloat = 1) -> UIColor { 9 | return UIColor( 10 | displayP3Red: r / 255, 11 | green: g / 255, 12 | blue: b / 255, 13 | alpha: alpha 14 | ) 15 | } 16 | 17 | static func random() -> UIColor { 18 | let red: CGFloat = CGFloat(drand48()) 19 | let green: CGFloat = CGFloat(drand48()) 20 | let blue: CGFloat = CGFloat(drand48()) 21 | return UIColor( 22 | red: red, 23 | green: green, 24 | blue: blue, 25 | alpha: 1.0 26 | ) 27 | } 28 | 29 | static func hex(_ hexStr: String) -> UIColor { 30 | var cString: String = hexStr.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() 31 | 32 | if cString.hasPrefix("#") { 33 | cString.remove(at: cString.startIndex) 34 | } 35 | 36 | if cString.count != 6 { 37 | fatalError("hexStr must be similar to `#2F2F2F` or `2F2F2F`") 38 | } 39 | 40 | var rgbValue: UInt32 = 0 41 | Scanner(string: cString).scanHexInt32(&rgbValue) 42 | 43 | return UIColor( 44 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 45 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 46 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 47 | alpha: CGFloat(1.0) 48 | ) 49 | } 50 | 51 | func toImage() -> UIImage? { 52 | let rect = CGRect(x: 0, y: 0, width: 1, height: 1) 53 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0) 54 | setFill() 55 | UIRectFill(rect) 56 | let image = UIGraphicsGetImageFromCurrentImageContext() 57 | UIGraphicsEndImageContext() 58 | return image 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Models/ErrorEnvelope.swift: -------------------------------------------------------------------------------- 1 | struct ErrorEnvelopeResult: Codable { 2 | let error: ErrorEnvelope 3 | } 4 | 5 | struct ErrorEnvelope: Error { 6 | let code: Int 7 | let reason: String 8 | let detail: String 9 | let moreInfo: String 10 | 11 | // MARK: - Overrides 12 | 13 | var localizedDescription: String { 14 | return reason 15 | } 16 | } 17 | 18 | // MARK: Codable 19 | extension ErrorEnvelope: Codable { 20 | init(from decoder: Decoder) throws { 21 | let values = try decoder.container(keyedBy: CodingKeys.self) 22 | code = try values.decode(Int.self, forKey: .code) 23 | reason = try values.decode(String.self, forKey: .reason) 24 | detail = try values.decode(String.self, forKey: .detail) 25 | moreInfo = try values.decode(String.self, forKey: .moreInfo) 26 | } 27 | 28 | enum CodingKeys: String, CodingKey { 29 | case code 30 | case reason 31 | case detail 32 | case moreInfo = "more_info" 33 | } 34 | } 35 | 36 | // MARK: - Errors 37 | extension ErrorEnvelope { 38 | 39 | static let couldNotParseErrorEnvelopeJSON = ErrorEnvelope( 40 | code: 400, 41 | reason: "could not parse error envelope json", 42 | detail: "could not parse error envelope json", 43 | moreInfo: "" 44 | ) 45 | 46 | static let couldNotParseJSON = ErrorEnvelope( 47 | code: 400, 48 | reason: "could not parse json", 49 | detail: "could not parse json", 50 | moreInfo: "" 51 | ) 52 | 53 | static let internalServerError = ErrorEnvelope( 54 | code: 500, 55 | reason: "Internal Server Error", 56 | detail: "Internal Server Error", 57 | moreInfo: "" 58 | ) 59 | 60 | static func couldNotDecodeJSON(_ error: Error) -> ErrorEnvelope { 61 | return ErrorEnvelope( 62 | code: 400, 63 | reason: error.localizedDescription, 64 | detail: error.localizedDescription, 65 | moreInfo: "" 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | // MARK: - Constraints 6 | 7 | func constraintEdges(to superview: UIView, useSafeArea: Bool = true) { 8 | translatesAutoresizingMaskIntoConstraints = false 9 | if #available(iOS 11.0, *), useSafeArea { 10 | NSLayoutConstraint.activate([ 11 | topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor), 12 | bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor), 13 | leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor), 14 | trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor) 15 | ]) 16 | } else { 17 | NSLayoutConstraint.activate([ 18 | topAnchor.constraint(equalTo: superview.topAnchor), 19 | bottomAnchor.constraint(equalTo: superview.bottomAnchor), 20 | leadingAnchor.constraint(equalTo: superview.leadingAnchor), 21 | trailingAnchor.constraint(equalTo: superview.trailingAnchor) 22 | ]) 23 | } 24 | } 25 | 26 | func setSubviewsTranslatingMasksToConstraints(to value: Bool, _ except: UIView? = nil) { 27 | subviews.forEach { (subview) in 28 | if subview === except { 29 | return 30 | } 31 | subview.translatesAutoresizingMaskIntoConstraints = value 32 | if subview.subviews.count > 0 { 33 | subview.setSubviewsTranslatingMasksToConstraints(to: value, except) 34 | } 35 | } 36 | } 37 | 38 | // MARK: - Corner 39 | 40 | func roundCorners(corners: UIRectCorner, radius: CGFloat) { 41 | let path = UIBezierPath( 42 | roundedRect: bounds, 43 | byRoundingCorners: corners, 44 | cornerRadii: CGSize(width: radius, height: radius) 45 | ) 46 | let mask = CAShapeLayer() 47 | mask.path = path.cgPath 48 | layer.mask = mask 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/DataModels/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct User: Codable { 4 | let id: String 5 | let email: String 6 | let username: String? 7 | let firstName: String? 8 | let lastName: String? 9 | let avatar: String? 10 | let token: String? 11 | 12 | init(from decoder: Decoder) throws { 13 | let values = try decoder.container(keyedBy: CodingKeys.self) 14 | id = try values.decode(String.self, forKey: .id) 15 | email = try values.decode(String.self, forKey: .email) 16 | username = try values.decodeIfPresent(String.self, forKey: .username) 17 | firstName = try values.decodeIfPresent(String.self, forKey: .firstName) 18 | lastName = try values.decodeIfPresent(String.self, forKey: .lastName) 19 | avatar = try values.decodeIfPresent(String.self, forKey: .avatar) 20 | token = try values.decodeIfPresent(String.self, forKey: .token) 21 | } 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case id = "user_id" 25 | case email 26 | case username 27 | case firstName = "first_name" 28 | case lastName = "last_name" 29 | case avatar 30 | case token 31 | } 32 | } 33 | 34 | // MARK: Encode & Decode 35 | extension User { 36 | func encode() -> [String: Any] { 37 | var result: [String: Any] = [:] 38 | 39 | result["user_id"] = id 40 | result["email"] = email 41 | result["username"] = username 42 | result["first_name"] = firstName 43 | result["last_name"] = lastName 44 | result["avatar"] = avatar 45 | result["token"] = token 46 | 47 | return result 48 | } 49 | 50 | static func decode(from json: [String: Any]?) -> User? { 51 | do { 52 | guard let json = json else { return nil } 53 | let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) 54 | return try Constant.jsonDecoder.decode(User.self, from: data) 55 | } catch { 56 | print("Decode user error: \(error.localizedDescription)") 57 | return nil 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Keyboard.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import UIKit 4 | 5 | final class Keyboard { 6 | enum KeyboardStatus { 7 | case show 8 | case hide 9 | 10 | var isHidden: Bool { 11 | return self == .hide 12 | } 13 | } 14 | 15 | typealias Change = ( 16 | status: KeyboardStatus, 17 | frame: CGRect, 18 | duration: TimeInterval, 19 | options: UIView.AnimationOptions, 20 | notificationName: Notification.Name 21 | ) 22 | 23 | static let shared = Keyboard() 24 | private let (changeSignal, changeObserver) = Signal.pipe() 25 | 26 | static var change: Signal { 27 | return shared.changeSignal 28 | } 29 | 30 | private init() { 31 | NotificationCenter.default.addObserver( 32 | self, 33 | selector: #selector(change(_:)), 34 | name: UIResponder.keyboardWillShowNotification, 35 | object: nil 36 | ) 37 | NotificationCenter.default.addObserver( 38 | self, 39 | selector: #selector(change(_:)), 40 | name: UIResponder.keyboardWillHideNotification, 41 | object: nil 42 | ) 43 | } 44 | 45 | @objc private func change(_ notification: Notification) { 46 | guard let userInfo = notification.userInfo, 47 | let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, 48 | let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber, 49 | let curveNumber = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber, 50 | let curve = UIView.AnimationCurve(rawValue: curveNumber.intValue) 51 | else { 52 | return 53 | } 54 | let isHidden = frame.cgRectValue.origin.y == UIScreen.main.bounds.height 55 | changeObserver.send(value: ( 56 | status: isHidden ? .hide : .show, 57 | frame.cgRectValue, 58 | duration.doubleValue, 59 | UIView.AnimationOptions(rawValue: UInt(curve.rawValue)), 60 | notificationName: notification.name 61 | )) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IS_STAGING 6 | $(IS_STAGING) 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /iOS-MVVM-TemplateTests/Api/ServiceTypeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import iOS_MVVM_Template 3 | 4 | final class ServiceTypeTests: XCTestCase { 5 | 6 | let service = Service( 7 | serverConfig: ServerConfig(apiBaseUrl: URL(string: "http://api.example.com")!), 8 | accessToken: AccessToken.template 9 | ) 10 | 11 | func testEquals() { 12 | XCTAssertTrue(Service() != MockService()) 13 | } 14 | 15 | func testPreparedRequest() { 16 | let url = URL(string: "http://api.example.com/v1/test?key=value")! 17 | let request = service.preparedRequest(forRequest: .init(url: url)) 18 | 19 | XCTAssertEqual(["Authorization": "Bearer \(AccessToken.template.jwtToken)"], request.allHTTPHeaderFields) 20 | } 21 | 22 | func testPreparedGetURL() { 23 | let url = URL(string: "http://api.example.com/v1/test?key=value")! 24 | let request = service.preparedRequest(forURL: url, query: ["extra": "1"]) 25 | 26 | XCTAssertEqual("http://api.example.com/v1/test?extra=1&key=value", request.url?.absoluteString) 27 | XCTAssertEqual("GET", request.httpMethod) 28 | XCTAssertEqual(["Authorization": "Bearer \(AccessToken.template.jwtToken)"], request.allHTTPHeaderFields) 29 | } 30 | 31 | func testPreparedDeleteURL() { 32 | let url = URL(string: "http://api.example.com/v1/test?key=value")! 33 | let request = service.preparedRequest(forURL: url, method: .DELETE, query: ["extra": "1"]) 34 | 35 | XCTAssertEqual("http://api.example.com/v1/test?extra=1&key=value", request.url?.absoluteString) 36 | XCTAssertEqual("DELETE", request.httpMethod) 37 | XCTAssertEqual(["Authorization": "Bearer \(AccessToken.template.jwtToken)"], request.allHTTPHeaderFields) 38 | } 39 | 40 | func testPreparedPostURL() { 41 | let url = URL(string: "http://api.example.com/v1/test?key=value")! 42 | let request = service.preparedRequest(forURL: url, method: .POST, query: ["extra": "1"]) 43 | 44 | XCTAssertEqual("http://api.example.com/v1/test?key=value", request.url?.absoluteString) 45 | XCTAssertEqual("POST", request.httpMethod) 46 | XCTAssertEqual([ 47 | "Authorization": "Bearer \(AccessToken.template.jwtToken)", 48 | "Content-Type": "application/json; charset=utf-8" 49 | ], request.allHTTPHeaderFields) 50 | XCTAssertEqual("{\"extra\":\"1\"}", String(data: request.httpBody ?? Data(), encoding: .utf8)) 51 | } 52 | 53 | func testPreparedPutURL() { 54 | let url = URL(string: "http://api.example.com/v1/test?key=value")! 55 | let request = service.preparedRequest(forURL: url, method: .PUT, query: ["extra": "1"]) 56 | 57 | XCTAssertEqual("http://api.example.com/v1/test?key=value", request.url?.absoluteString) 58 | XCTAssertEqual("PUT", request.httpMethod) 59 | XCTAssertEqual([ 60 | "Authorization": "Bearer \(AccessToken.template.jwtToken)", 61 | "Content-Type": "application/json; charset=utf-8" 62 | ], request.allHTTPHeaderFields) 63 | XCTAssertEqual("{\"extra\":\"1\"}", String(data: request.httpBody ?? Data(), encoding: .utf8)) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/Extensions/UIViewControllerExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PKHUD 3 | 4 | extension UIViewController { 5 | 6 | // MARK: - Type Name 7 | 8 | static var typeName: String { 9 | return String(describing: self) 10 | } 11 | 12 | // MARK: - Hide Keyboard 13 | 14 | func hideKeyboardWhenTappedAround(cancelTouches: Bool = false) { 15 | let tap = UITapGestureRecognizer( 16 | target: self, 17 | action: #selector(viewDidTapped) 18 | ) 19 | tap.cancelsTouchesInView = cancelTouches 20 | view.addGestureRecognizer(tap) 21 | } 22 | 23 | @objc func viewDidTapped() { 24 | view.endEditing(true) 25 | } 26 | 27 | // MARK: - Navigation Items 28 | 29 | var backButtonItem: UIBarButtonItem { 30 | let item = UIBarButtonItem( 31 | image: UIImage(named: "left-arrow"), 32 | style: .plain, 33 | target: self, 34 | action: #selector(backButtonItemTapped) 35 | ) 36 | return item 37 | } 38 | 39 | @objc private func backButtonItemTapped() { 40 | navigationController?.popViewController(animated: true) 41 | } 42 | 43 | var closeButtonItem: UIBarButtonItem { 44 | let item = UIBarButtonItem( 45 | image: UIImage(named: "close"), 46 | style: .plain, 47 | target: self, 48 | action: #selector(closeButtonItemTapped) 49 | ) 50 | return item 51 | } 52 | 53 | @objc func closeButtonItemTapped() { 54 | dismiss(animated: true, completion: nil) 55 | } 56 | 57 | // MARK: - HUD 58 | 59 | func hideHUD() { 60 | DispatchQueue.main.async { 61 | HUD.hide() 62 | } 63 | } 64 | 65 | func showProgress(title: String?, subtitle: String? = nil) { 66 | DispatchQueue.main.async { 67 | HUD.show(.labeledProgress(title: title, subtitle: subtitle)) 68 | } 69 | } 70 | 71 | func showLoading() { 72 | DispatchQueue.main.async { 73 | HUD.show(.progress) 74 | } 75 | } 76 | 77 | // MARK: - Alert 78 | 79 | func alertUser( 80 | title: String = "", 81 | message: String, 82 | confirm: String = "Close", 83 | handler: ((UIAlertAction) -> Void)? = nil 84 | ) { 85 | let alert = UIAlertController.alert( 86 | title: title, 87 | message: message, 88 | confirm: confirm, 89 | handler: handler 90 | ) 91 | customPresent(alert, animated: true) 92 | } 93 | } 94 | 95 | extension UIViewController { 96 | func customPresent( 97 | _ viewControllerToPresent: UIViewController, 98 | animated flag: Bool, 99 | completion: (() -> Void)? = nil 100 | ) { 101 | if let presented = presentedViewController { 102 | presented.customPresent( 103 | viewControllerToPresent, 104 | animated: flag, 105 | completion: completion 106 | ) 107 | } else { 108 | present(viewControllerToPresent, animated: flag, completion: completion) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/xcshareddata/xcschemes/iOS-MVVM-Template_Staging.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /iOS-MVVM-TemplateTests/DataSources/ValueCellDataSourceTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import UIKit 3 | @testable import iOS_MVVM_Template 4 | 5 | final class IntTableCell: UITableViewCell, ValueCell { 6 | var value: Int = 0 7 | func configureWith(value: Int) { 8 | self.value = value 9 | } 10 | } 11 | 12 | final class IntDataSource: ValueCellDataSource { 13 | override func registerClasses(tableView: UITableView?) { 14 | tableView?.registerCellClass(IntTableCell.self) 15 | } 16 | } 17 | 18 | final class ValueCellDataSourceTest: XCTestCase { 19 | private let dataSource = IntDataSource() 20 | private let tableView = UITableView() 21 | 22 | override func setUp() { 23 | super.setUp() 24 | 25 | dataSource.registerClasses(tableView: tableView) 26 | 27 | dataSource.appendRow(value: 1, cellClass: IntTableCell.self, toSection: 0) 28 | dataSource.appendRow(value: 2, cellClass: IntTableCell.self, toSection: 0) 29 | dataSource.set(values: [1, 2, 3], cellClass: IntTableCell.self, inSection: 2) 30 | } 31 | 32 | func testTableViewDataSourceMethods() { 33 | XCTAssertEqual(3, dataSource.numberOfSections(in: tableView)) 34 | XCTAssertEqual(2, dataSource.tableView(tableView, numberOfRowsInSection: 0)) 35 | XCTAssertEqual(0, dataSource.tableView(tableView, numberOfRowsInSection: 1)) 36 | XCTAssertEqual(3, dataSource.tableView(tableView, numberOfRowsInSection: 2)) 37 | } 38 | 39 | func testClearValues() { 40 | dataSource.clearValues() 41 | XCTAssertEqual(0, dataSource.numberOfItems()) 42 | } 43 | 44 | func testClearValuesInSection() { 45 | dataSource.clearValues(inSection: 0) 46 | XCTAssertEqual(3, dataSource.numberOfSections(in: tableView)) 47 | XCTAssertEqual(0, dataSource.tableView(tableView, numberOfRowsInSection: 0)) 48 | XCTAssertEqual(0, dataSource.tableView(tableView, numberOfRowsInSection: 1)) 49 | XCTAssertEqual(3, dataSource.tableView(tableView, numberOfRowsInSection: 2)) 50 | } 51 | 52 | func testMoveItem() { 53 | dataSource.moveItem( 54 | value: 3, 55 | cellClass: IntTableCell.self, 56 | at: 2, 57 | to: 0, 58 | inSection: 2 59 | ) 60 | // swiftlint:disable force_cast 61 | XCTAssertTrue(dataSource[IndexPath(item: 0, section: 2)] as! Int == 3) 62 | 63 | dataSource.moveItem( 64 | value: 3, 65 | cellClass: IntTableCell.self, 66 | at: 0, 67 | to: 1, 68 | inSection: 2 69 | ) 70 | // swiftlint:disable force_cast 71 | XCTAssertTrue(dataSource[IndexPath(item: 1, section: 2)] as! Int == 3) 72 | 73 | dataSource.moveItem( 74 | value: 3, 75 | cellClass: IntTableCell.self, 76 | at: 1, 77 | to: 2, 78 | inSection: 2 79 | ) 80 | // swiftlint:disable force_cast 81 | XCTAssertTrue(dataSource[IndexPath(item: 2, section: 2)] as! Int == 3) 82 | } 83 | 84 | func testSetValue() { 85 | XCTAssertTrue(dataSource[IndexPath(item: 1, section: 0)] as! Int == 2) 86 | dataSource.set(value: 3, cellClass: IntTableCell.self, atIndex: 1, inSection: 0) 87 | XCTAssertTrue(dataSource[IndexPath(item: 1, section: 0)] as! Int == 3) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/ViewModels/AppDelegateViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import Lebron_ReactiveExtensions 4 | 5 | protocol AppDelegateViewModelInputs { 6 | /// Call when the application finishes launching. 7 | func applicationDidFinishLaunching( 8 | application: UIApplication?, 9 | launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) 11 | 12 | /// Call when the application did become active. 13 | func applicationDidBecomeActive() 14 | 15 | /// Call when the application will enter foreground. 16 | func applicationWillEnterForeground() 17 | 18 | /// Call when receive remote notificaiton 19 | func didReceiveRemoteNotification(_ notification: UNNotification) 20 | } 21 | 22 | protocol AppDelegateViewModelOutputs { 23 | /// Emits when the push token registration request begins. 24 | var pushTokenRegistrationRequestStarted: Signal<(), Never> { get } 25 | 26 | /// Emits the push token that has been successfully generated. 27 | var pushTokenSuccessfullyGenerated: Signal { get } 28 | } 29 | 30 | protocol AppDelegateViewModelType { 31 | var inputs: AppDelegateViewModelInputs { get } 32 | var outputs: AppDelegateViewModelOutputs { get } 33 | } 34 | 35 | final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateViewModelInputs, AppDelegateViewModelOutputs { 36 | 37 | init() { 38 | let pushTokenRegistrationRequestStartedEvents = applicationLaunchOptionsProperty.signal.ignoreValues() 39 | .flatMap { 40 | AppEnvironment.current.pushRegistrationType 41 | .register(for: [.alert, .badge, .sound]) 42 | .materialize() 43 | } 44 | 45 | pushTokenRegistrationRequestStarted = pushTokenRegistrationRequestStartedEvents.values().ignoreValues() 46 | 47 | pushTokenSuccessfullyGenerated = deviceTokenStringProperty.signal.skipNil() 48 | } 49 | 50 | private typealias ApplicationWithOptions = ( 51 | application: UIApplication?, 52 | options: [UIApplication.LaunchOptionsKey: Any]? 53 | ) 54 | private let applicationLaunchOptionsProperty = MutableProperty(nil) 55 | func applicationDidFinishLaunching( 56 | application: UIApplication?, 57 | launchOptions: [UIApplication.LaunchOptionsKey: Any]? 58 | ) { 59 | applicationLaunchOptionsProperty.value = (application, launchOptions) 60 | } 61 | 62 | private let applicationDidBecomeActiveProperty = MutableProperty(()) 63 | func applicationDidBecomeActive() { 64 | applicationDidBecomeActiveProperty.value = () 65 | } 66 | 67 | private let applicationWillEnterForegroundProperty = MutableProperty(()) 68 | func applicationWillEnterForeground() { 69 | applicationWillEnterForegroundProperty.value = () 70 | } 71 | 72 | let deviceTokenStringProperty = MutableProperty(nil) 73 | func didRegisterForRemoteNotifications(withFcmToken token: String) { 74 | deviceTokenStringProperty.value = token 75 | } 76 | 77 | func didReceiveRemoteNotification(_ notification: UNNotification) { 78 | 79 | } 80 | 81 | let pushTokenRegistrationRequestStarted: Signal<(), Never> 82 | let pushTokenSuccessfullyGenerated: Signal 83 | 84 | var inputs: AppDelegateViewModelInputs { return self } 85 | var outputs: AppDelegateViewModelOutputs { return self } 86 | } 87 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/Extensions/URLSession+RequestHelpers.swift: -------------------------------------------------------------------------------- 1 | import ReactiveSwift 2 | 3 | private let scheduler = QueueScheduler( 4 | qos: .background, 5 | name: "com.example.api", 6 | targeting: nil 7 | ) 8 | 9 | extension URLSession { 10 | 11 | func rac_dataResponse(_ request: URLRequest) -> SignalProducer { 12 | 13 | let producer = reactive.data(with: request) 14 | 15 | print("⚪️ [Api] Starting request \(self.sanitized(request))") 16 | 17 | return producer 18 | .start(on: scheduler) 19 | .flatMapError { _ in SignalProducer(error: .couldNotParseErrorEnvelopeJSON) } 20 | .flatMap { (data, response) -> SignalProducer in 21 | 22 | if AppEnvironment.current.isStaging { 23 | print("json response: \(String(data: data, encoding: .utf8) ?? "")") 24 | } 25 | 26 | guard let res = response as? HTTPURLResponse else { 27 | fatalError() 28 | } 29 | 30 | guard (200..<300).contains(res.statusCode), 31 | let headers = res.allHeaderFields as? [String: String], 32 | let contentType = headers["Content-Type"], contentType.hasPrefix("application/json") else { 33 | 34 | if res.statusCode == 500 { 35 | return .init(error: .internalServerError) 36 | } 37 | 38 | do { 39 | let error = try Constant.jsonDecoder.decode(ErrorEnvelope.self, from: data) 40 | print("🔴 [Api] Failure \(self.sanitized(request)) \n Error - \(error)") 41 | 42 | return .init(error: error) 43 | } catch { 44 | print("🔴 [Api] Failure \(self.sanitized(request)) \n decoding error - \(error)") 45 | return .init(error: .couldNotDecodeJSON(error)) 46 | } 47 | } 48 | 49 | if let errorResult = try? Constant.jsonDecoder.decode(ErrorEnvelopeResult.self, from: data) { 50 | let error = errorResult.error 51 | print("🔴 [Api] Failure \(self.sanitized(request)) \n Error - \(error)") 52 | return .init(error: error) 53 | } 54 | 55 | print("🔵 [Api] Success \(self.sanitized(request))") 56 | return .init(value: data) 57 | } 58 | } 59 | 60 | // MARK: - Private 61 | 62 | // swiftlint:disable force_try 63 | private static let sanitationRules = [ 64 | "access_token=[REDACTED]": 65 | try! NSRegularExpression(pattern: "access_token=([a-zA-Z0-9]*)", options: .caseInsensitive) 66 | ] 67 | 68 | private func sanitized(_ request: URLRequest) -> String { 69 | guard let urlString = request.url?.absoluteString else { 70 | return "" 71 | } 72 | 73 | return URLSession.sanitationRules.reduce(urlString) { accum, templateAndRule in 74 | let (template, rule) = templateAndRule 75 | let range = NSRange(location: 0, length: accum.count) 76 | return rule.stringByReplacingMatches( 77 | in: accum, 78 | options: .withTransparentBounds, 79 | range: range, 80 | withTemplate: template 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Api/ServiceType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | 4 | protocol ServiceType { 5 | var serverConfig: ServerConfigType { get } 6 | var accessToken: AccessTokenType? { get } 7 | 8 | init(serverConfig: ServerConfigType, accessToken: AccessTokenType?) 9 | 10 | func login(accessToken: AccessTokenType) -> Self 11 | 12 | func logout() -> Self 13 | } 14 | 15 | func == (lhs: ServiceType, rhs: ServiceType) -> Bool { 16 | return type(of: lhs) == type(of: rhs) && 17 | lhs.serverConfig == rhs.serverConfig && 18 | lhs.accessToken == rhs.accessToken 19 | } 20 | 21 | func != (lhs: ServiceType, rhs: ServiceType) -> Bool { 22 | return !(lhs == rhs) 23 | } 24 | 25 | // MARK: - Prepare Request 26 | extension ServiceType { 27 | func preparedRequest(forURL url: URL, 28 | method: RequestMethod = .GET, 29 | query: [String: Any] = [:]) -> URLRequest { 30 | 31 | var request = URLRequest(url: url) 32 | request.httpMethod = method.rawValue 33 | return preparedRequest(forRequest: request, query: query) 34 | } 35 | 36 | func preparedRequest(forRequest originalRequest: URLRequest, 37 | query: [String: Any] = [:]) -> URLRequest { 38 | 39 | var request = originalRequest 40 | guard let url = request.url else { 41 | return originalRequest 42 | } 43 | 44 | var headers = defaultHeaders 45 | 46 | let method = request.httpMethod?.uppercased() 47 | var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! 48 | var queryItems = components.queryItems ?? [] 49 | 50 | if method == "POST" || method == "PUT" { 51 | if request.httpBody == nil { 52 | headers["Content-Type"] = "application/json; charset=utf-8" 53 | request.httpBody = try? JSONSerialization.data(withJSONObject: query, options: []) 54 | } 55 | } else { 56 | queryItems.append( 57 | contentsOf: query 58 | .flatMap(queryComponents) 59 | .map(URLQueryItem.init(name:value:)) 60 | ) 61 | } 62 | components.queryItems = queryItems.sorted { $0.name < $1.name } 63 | request.url = components.url 64 | 65 | let currentHeaders = request.allHTTPHeaderFields ?? [:] 66 | request.allHTTPHeaderFields = currentHeaders.withAllValuesFrom(headers) 67 | 68 | return request 69 | } 70 | 71 | private func queryComponents(_ key: String, _ value: Any) -> [(String, String)] { 72 | var components: [(String, String)] = [] 73 | 74 | if let dictionary = value as? [String: Any] { 75 | for (nestedKey, value) in dictionary { 76 | components += queryComponents("\(key)[\(nestedKey)]", value) 77 | } 78 | } else if let array = value as? [Any] { 79 | for value in array { 80 | components += queryComponents("\(key)[]", value) 81 | } 82 | } else { 83 | components.append((key, String(describing: value))) 84 | } 85 | 86 | return components 87 | } 88 | 89 | private var defaultHeaders: [String: String] { 90 | var headers: [String: String] = [:] 91 | headers["Authorization"] = authorizationHeader 92 | return headers 93 | } 94 | 95 | private var authorizationHeader: String? { 96 | if let token = accessToken?.jwtToken { 97 | return "Bearer \(token)" 98 | } 99 | return nil 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/xcshareddata/xcschemes/iOS-MVVM-Template.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | private let viewModel: AppDelegateViewModelType = AppDelegateViewModel() 9 | 10 | func application( 11 | _ application: UIApplication, 12 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 13 | ) -> Bool { 14 | 15 | let restoredEnv = AppEnvironment.environmentFormStorage() 16 | AppEnvironment.replaceCurrentEnvironment(restoredEnv) 17 | 18 | #if DEBUG 19 | if Secrets.isMockService { 20 | AppEnvironment.replaceCurrentEnvironment(apiService: MockService()) 21 | } 22 | #endif 23 | 24 | UIViewController.doBadSwizzleStuff() 25 | UNUserNotificationCenter.current().delegate = self 26 | 27 | bindViewModel() 28 | viewModel.inputs.applicationDidFinishLaunching( 29 | application: application, 30 | launchOptions: launchOptions 31 | ) 32 | 33 | return true 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | viewModel.inputs.applicationWillEnterForeground() 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | viewModel.inputs.applicationDidBecomeActive() 42 | } 43 | 44 | // MARK: - Remote Notification 45 | 46 | func application( 47 | _: UIApplication, 48 | didFailToRegisterForRemoteNotificationsWithError error: Error 49 | ) { 50 | print("🔴 Failed to register for remote notifications: \(error.localizedDescription)") 51 | } 52 | 53 | } 54 | 55 | // MARK: - Bind View Model 56 | extension AppDelegate { 57 | private func bindViewModel() { 58 | viewModel.outputs.pushTokenRegistrationRequestStarted 59 | .observeForUI() 60 | .observeValues { 61 | print("📲 [Push Registration] Push token registration started 🚀") 62 | } 63 | 64 | viewModel.outputs.pushTokenSuccessfullyGenerated 65 | .observeForUI() 66 | .observeValues { token in 67 | print("📲 [Push Registration] Push token successfully generated (\(token)) ✨") 68 | } 69 | } 70 | } 71 | 72 | // MARK: - UNUserNotificationCenterDelegate 73 | extension AppDelegate: UNUserNotificationCenterDelegate { 74 | func userNotificationCenter( 75 | _: UNUserNotificationCenter, 76 | willPresent notification: UNNotification, 77 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void 78 | ) { 79 | completionHandler([.alert, .sound]) 80 | } 81 | 82 | func userNotificationCenter( 83 | _: UNUserNotificationCenter, 84 | didReceive response: UNNotificationResponse, 85 | withCompletionHandler completion: @escaping () -> Void 86 | ) { 87 | viewModel.inputs.didReceiveRemoteNotification(response.notification) 88 | completion() 89 | } 90 | } 91 | 92 | // MARK: - Style App 93 | extension AppDelegate { 94 | private func styleApp() { 95 | let navigationBar = UINavigationBar 96 | .appearance(whenContainedInInstancesOf: [UINavigationController.self]) 97 | navigationBar.barTintColor = .appBackgroundColor 98 | navigationBar.isTranslucent = false 99 | let titleTextAttributes: [NSAttributedString.Key: Any] = [ 100 | .foregroundColor: UIColor.white as Any, 101 | .font: UIFont.SFProText(style: .regular, size: 18) as Any 102 | ] 103 | navigationBar.titleTextAttributes = titleTextAttributes 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS-MVVM-Template 2 | 3 | 本项目是参考 [kickstarter/ios-oss](https://github.com/kickstarter/ios-oss) 编写的一个 iOS 模板项目,目的是为了快速创建新的项目。这里感谢 Kickstarter 的开源,让我们学习到了如此优秀的 iOS 项目结构。 4 | 5 | 本项目是根据我自己的项目需求、对 Kickstarter 源码中一些不必要的功能进行精简,并且在一个实际项目中应用后整理出的一个模板。如果想要详细了解 Kickstarter 的源码,请自己去仔细阅读。本人之前在阅读 Kickstarter 项目源码时,也整理了一份笔记,有需要的同学也可以去看下。[Kickstarter-iOS 源码分析 - 文集 - 简书](https://www.jianshu.com/nb/36807985) 6 | 7 | ## 目录结构 8 | 9 | ``` 10 | iOS-MVVM-Template 11 | ├── Api : 存放网络请求相关的文件 12 | │ ├── Models 13 | │ │ ├── AccessToken.swift : AccessToken 模型 14 | │ │ ├── ErrorCode.swift:错误代码 15 | │ │ ├── ErrorEnvelope.swift : 后台返回的 Error 数据模型 16 | │ │ ├── Templates : 存放数据模型的模板 17 | │ │ └── VoidEnvelope.swift : 后台返回空 json `{}` 数据时的数据模型 18 | │ ├── Extensions 19 | │ │ └── URLSession+RequestHelpers.swift : 使用 ReactiveSwift 对 URLSession 的扩展方法 20 | │ ├── Lib 21 | │ │ ├── RequestMethod.swift : 请求方法 22 | │ │ └── Route.swift : 所有的网络请求 23 | │ ├── EnvironmentType.swift : 应用环境类型 24 | │ ├── MockService.swift : 模拟的 Service 25 | │ ├── ServerConfig.swift : 服务器配置的数据模型 26 | │ ├── Service+RequestHelpers.swift : 对 Service 的扩展,发送网络请求和解析数据的方法 27 | │ ├── Service.swift : Service 类型,负责整个应用的网络请求 28 | │ └── ServiceType.swift : ServiceType 协议,Service 和 MockService 都遵循这个协议 29 | ├── Configs 30 | │ ├── Global.swift : 存放全局的常量 31 | │ └── Secrets.swift : 存放应用的一些配置 32 | ├── DataModels : 存放数据模型 33 | │ ├── Templates : 存放数据模型的模板 34 | │ └── User.swift : User 数据模型 35 | ├── DataSources : 存放 DataSource 36 | │ └── Base 37 | │ ├── ValueCell.swift : ValueCell 协议。想要使用 ValueCellDataSource 作为 UITableView 和 UICollectionView 的数据源,他们对应的 Cell 都需要实现这个协议 38 | │ └── ValueCellDataSource.swift : 实现了 UITableViewDataSource 和 UICollectionViewDataSource,只整理了一些常用的数据处理的方法,有其他需求的可以继续往这里添加 39 | ├── Library : 存放在整个应用中比较通用的类型 40 | │ ├── Extensions : 存放对类型的扩展 41 | │ ├── App.swift : App 的相关信息 42 | │ ├── AppEnvironment.swift : 管理应用环境 43 | │ ├── Environment.swift : 应用环境的数据模型 44 | │ ├── Keyboard.swift : 使用 ReactiveSwift 对键盘的弹出和隐藏进行处理 45 | │ ├── Nib.swift : 方便初始化使用 xib 自定义的 View 46 | │ ├── Notifications.swift : 管理应用中用到的通知类型 47 | │ ├── PushRegistration.swift : 使用 ReactiveSwift 对 Push 通知的注册进行处理,实现了 PushRegistrationType 协议 48 | │ ├── PushRegistrationType.swift : PushRegistrationType 协议 49 | │ └── Storyboard.swift : 方便初始化使用 Storyboard 自定义的 ViewController 50 | ├── Resources : 存放应用中用到的字体、图标等文件 51 | ├── ViewModels : 存放 View Model 52 | └── Views&Controllers : 存放 View 和 Controller 53 | ``` 54 | 55 | ## 使用到的第三方库 56 | 57 | - [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) 58 | - [ReactiveExtensions](https://github.com/Lebron1992/Kickstarter-ReactiveExtensions) 59 | - [PKHUD](https://github.com/pkluz/PKHUD) 60 | - [JWT](https://github.com/yourkarma/JWT) 61 | 62 | ## License 63 | 64 | ``` 65 | MIT License 66 | 67 | Copyright (c) 2019 Lebron 68 | 69 | Permission is hereby granted, free of charge, to any person obtaining a copy 70 | of this software and associated documentation files (the “Software”), to deal 71 | in the Software without restriction, including without limitation the rights 72 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 73 | copies of the Software, and to permit persons to whom the Software is 74 | furnished to do so, subject to the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be included in all 77 | copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 80 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 81 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 82 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 83 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 84 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 85 | SOFTWARE. 86 | ``` 87 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/Library/AppEnvironment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AppEnvironment { 4 | 5 | private static var stack: [Environment] = { 6 | let isStaging: Bool = { 7 | if let isStagingStr = Bundle.main.infoDictionary?["IS_STAGING"] as? String { 8 | return isStagingStr == "YES" 9 | } 10 | return false 11 | }() 12 | let serverConfig: ServerConfig = isStaging ? .staging : .production 13 | let service = Service(serverConfig: serverConfig) 14 | let env = Environment(apiService: service) 15 | return [env] 16 | }() 17 | 18 | static var current: Environment! { 19 | return stack.last 20 | } 21 | 22 | // MARK: - Manage User Sessions 23 | 24 | static func login(token: String, user: User) { 25 | let accessToken = AccessToken(token: token) 26 | replaceCurrentEnvironment( 27 | apiService: current.apiService.login(accessToken: accessToken), 28 | currentUser: user 29 | ) 30 | } 31 | 32 | static func logout() { 33 | replaceCurrentEnvironment( 34 | apiService: current.apiService.logout(), 35 | currentUser: nil 36 | ) 37 | } 38 | 39 | static func updateCurrentUser(_ user: User) { 40 | replaceCurrentEnvironment( 41 | currentUser: user 42 | ) 43 | } 44 | 45 | static var isUserLogin: Bool { 46 | return current.currentUser != nil 47 | } 48 | 49 | // MARK: - Manage Environments 50 | 51 | static func pushEnvironment(_ env: Environment) { 52 | saveEnvironment(env) 53 | stack.append(env) 54 | } 55 | 56 | static func popEnvironment() -> Environment? { 57 | let last = stack.popLast() 58 | let next = current ?? Environment() 59 | saveEnvironment(next) 60 | return last 61 | } 62 | 63 | static func replaceCurrentEnvironment(_ env: Environment) { 64 | pushEnvironment(env) 65 | stack.remove(at: stack.count - 2) 66 | } 67 | 68 | static func replaceCurrentEnvironment( 69 | apiService: ServiceType = AppEnvironment.current.apiService, 70 | currentUser: User? = AppEnvironment.current.currentUser) { 71 | replaceCurrentEnvironment( 72 | Environment( 73 | apiService: apiService, 74 | currentUser: currentUser 75 | ) 76 | ) 77 | } 78 | 79 | static func pushEnvironment( 80 | apiService: ServiceType = AppEnvironment.current.apiService, 81 | currentUser: User? = AppEnvironment.current.currentUser) { 82 | pushEnvironment( 83 | Environment( 84 | apiService: apiService, 85 | currentUser: currentUser 86 | ) 87 | ) 88 | } 89 | 90 | // MARK: Save & Restore 91 | 92 | static let environmentStorageKey = "com.example.AppEnvironment.current" 93 | 94 | static func saveEnvironment(_ env: Environment = AppEnvironment.current) { 95 | 96 | var data: [String: Any] = [:] 97 | 98 | data["apiService.accessToken.token"] = env.apiService.accessToken?.token 99 | data["currentUser"] = env.currentUser?.encode() 100 | 101 | UserDefaults.standard.set(data, forKey: environmentStorageKey) 102 | UserDefaults.standard.synchronize() 103 | } 104 | 105 | static func environmentFormStorage() -> Environment { 106 | let data = UserDefaults.standard.dictionary(forKey: environmentStorageKey) ?? [:] 107 | 108 | var service = current.apiService 109 | var currentUser: User? 110 | 111 | if let token = data["apiService.accessToken.token"] as? String { 112 | service = service.login(accessToken: AccessToken(token: token)) 113 | } 114 | 115 | if service.accessToken != nil { 116 | currentUser = User.decode(from: data["currentUser"] as? [String: Any]) 117 | } 118 | 119 | return Environment( 120 | apiService: service, 121 | currentUser: currentUser 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /iOS-MVVM-Template/DataSources/Base/ValueCellDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ValueCellDataSource: NSObject { 4 | 5 | private(set) var values: [[(value: Any, reusableId: String)]] = [] 6 | 7 | // MARK: - Methods to be Overridden 8 | 9 | func registerClasses(tableView: UITableView?) { 10 | } 11 | 12 | func configureTableCell(_ cell: UITableViewCell, with value: Any) { 13 | } 14 | 15 | func registerClasses(collectionView: UICollectionView?) { 16 | } 17 | 18 | func configureCollectionCell(_ cell: UICollectionViewCell, with value: Any) { 19 | } 20 | 21 | // MARK: - Public Methods 22 | 23 | final func prependRow( 24 | value: Value, 25 | cellClass: Cell.Type, 26 | toSection section: Int 27 | ) where Cell.Value == Value { 28 | 29 | padValues(forSection: section) 30 | values[section].insert((value, cellClass.defaultReusableId), at: 0) 31 | } 32 | 33 | final func appendRow 34 | (value: Value, 35 | cellClass: Cell.Type, 36 | toSection section: Int) 37 | where Cell.Value == Value { 38 | 39 | padValues(forSection: section) 40 | values[section].append((value, cellClass.defaultReusableId)) 41 | } 42 | 43 | final func deleteRow( 44 | value: Value, 45 | cellClass: Cell.Type, 46 | atIndex index: Int, 47 | inSection section: Int 48 | ) where Cell.Value == Value { 49 | 50 | padValues(forSection: section) 51 | values[section].remove(at: index) 52 | } 53 | 54 | final func set 55 | (values: [Value], 56 | cellClass: Cell.Type, 57 | inSection section: Int) 58 | where Cell.Value == Value { 59 | 60 | padValues(forSection: section) 61 | self.values[section] = values.map { ($0, cellClass.defaultReusableId) } 62 | } 63 | 64 | final func set 65 | (value: Value, 66 | cellClass _: Cell.Type, 67 | atIndex index: Int, 68 | inSection section: Int 69 | ) where Cell.Value == Value { 70 | values[section][index] = (value, Cell.defaultReusableId) 71 | } 72 | 73 | final func moveItem( 74 | value _: Value, 75 | cellClass _: Cell.Type, 76 | at oldIndex: Int, 77 | to newIndex: Int, 78 | inSection section: Int 79 | ) where Cell.Value == Value { 80 | var newSection = values[section] 81 | let removed = newSection.remove(at: oldIndex) 82 | newSection.insert(removed, at: newIndex) 83 | values[section] = newSection 84 | } 85 | 86 | final func clearValues() { 87 | values = [[]] 88 | } 89 | 90 | final func clearValues(inSection section: Int) { 91 | padValues(forSection: section) 92 | values[section] = [] 93 | } 94 | 95 | final func numberOfItems() -> Int { 96 | return values.reduce(0) { accum, section in accum + section.count } 97 | } 98 | 99 | final subscript(indexPath: IndexPath) -> Any { 100 | return values[indexPath.section][indexPath.item].value 101 | } 102 | 103 | // MARK: - Private Methods 104 | 105 | private func padValues(forSection section: Int) { 106 | guard values.count <= section else { return } 107 | 108 | (values.count...section).forEach { _ in 109 | self.values.append([]) 110 | } 111 | } 112 | } 113 | 114 | // MARK: - UITableViewDataSource 115 | extension ValueCellDataSource: UITableViewDataSource { 116 | 117 | final func numberOfSections(in tableView: UITableView) -> Int { 118 | return values.count 119 | } 120 | 121 | final func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 122 | return values[section].count 123 | } 124 | 125 | final func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 126 | let (value, reusableId) = values[indexPath.section][indexPath.row] 127 | let cell = tableView.dequeueReusableCell( 128 | withIdentifier: reusableId, 129 | for: indexPath 130 | ) 131 | configureTableCell(cell, with: value) 132 | return cell 133 | } 134 | } 135 | 136 | // MARK: - UICollectionViewDataSource 137 | extension ValueCellDataSource: UICollectionViewDataSource { 138 | final func numberOfSections(in collectionView: UICollectionView) -> Int { 139 | return values.count 140 | } 141 | 142 | final func collectionView(_ collectionView: UICollectionView, 143 | numberOfItemsInSection section: Int) -> Int { 144 | return values[section].count 145 | } 146 | 147 | final func collectionView(_ collectionView: UICollectionView, 148 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 149 | let (value, reusableId) = values[indexPath.section][indexPath.row] 150 | let cell = collectionView.dequeueReusableCell( 151 | withReuseIdentifier: reusableId, 152 | for: indexPath 153 | ) 154 | configureCollectionCell(cell, with: value) 155 | return cell 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /iOS-MVVM-Template.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AD8FA66F098FC2C2B5DD72AB /* Pods_iOS_MVVM_Template.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62F501160D9CF3394B0C531D /* Pods_iOS_MVVM_Template.framework */; }; 11 | FD7D4EB4249DBF5C00246661 /* ErrorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7D4EB3249DBF5C00246661 /* ErrorExtensions.swift */; }; 12 | FD7E543426677F0800ABFE61 /* EncodableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7E543326677F0800ABFE61 /* EncodableExtensions.swift */; }; 13 | FD7E543626677F1D00ABFE61 /* DecodableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7E543526677F1D00ABFE61 /* DecodableExtensions.swift */; }; 14 | FDD85AB023B4BB6B00D7B8DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AAF23B4BB6B00D7B8DD /* AppDelegate.swift */; }; 15 | FDD85AB423B4BB6B00D7B8DD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AB323B4BB6B00D7B8DD /* ViewController.swift */; }; 16 | FDD85AB723B4BB6B00D7B8DD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDD85AB523B4BB6B00D7B8DD /* Main.storyboard */; }; 17 | FDD85AB923B4BB6C00D7B8DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FDD85AB823B4BB6C00D7B8DD /* Assets.xcassets */; }; 18 | FDD85ABC23B4BB6C00D7B8DD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDD85ABA23B4BB6C00D7B8DD /* LaunchScreen.storyboard */; }; 19 | FDD85AD623B4F2BD00D7B8DD /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AD423B4F2BC00D7B8DD /* Secrets.swift */; }; 20 | FDD85AD723B4F2BD00D7B8DD /* Global.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AD523B4F2BC00D7B8DD /* Global.swift */; }; 21 | FDD85AE623B4F77600D7B8DD /* OptionalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADA23B4F77500D7B8DD /* OptionalExtensions.swift */; }; 22 | FDD85AE723B4F77600D7B8DD /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADB23B4F77500D7B8DD /* StringExtensions.swift */; }; 23 | FDD85AE823B4F77600D7B8DD /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADC23B4F77500D7B8DD /* UIColorExtensions.swift */; }; 24 | FDD85AE923B4F77600D7B8DD /* UIViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADD23B4F77500D7B8DD /* UIViewControllerExtensions.swift */; }; 25 | FDD85AEA23B4F77600D7B8DD /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADE23B4F77500D7B8DD /* UIScreenExtensions.swift */; }; 26 | FDD85AEB23B4F77600D7B8DD /* UIFontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85ADF23B4F77600D7B8DD /* UIFontExtensions.swift */; }; 27 | FDD85AEE23B4F77600D7B8DD /* DictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AE223B4F77600D7B8DD /* DictionaryExtensions.swift */; }; 28 | FDD85AEF23B4F77600D7B8DD /* UIViewController+Preparation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AE323B4F77600D7B8DD /* UIViewController+Preparation.swift */; }; 29 | FDD85AF023B4F77600D7B8DD /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AE423B4F77600D7B8DD /* UIViewExtensions.swift */; }; 30 | FDD85AF123B4F77600D7B8DD /* UIAlertControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AE523B4F77600D7B8DD /* UIAlertControllerExtensions.swift */; }; 31 | FDD85AFB23B5985D00D7B8DD /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF223B5985D00D7B8DD /* Keyboard.swift */; }; 32 | FDD85AFC23B5985D00D7B8DD /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF323B5985D00D7B8DD /* Notifications.swift */; }; 33 | FDD85AFD23B5985D00D7B8DD /* Storyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF423B5985D00D7B8DD /* Storyboard.swift */; }; 34 | FDD85AFE23B5985D00D7B8DD /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF523B5985D00D7B8DD /* App.swift */; }; 35 | FDD85AFF23B5985D00D7B8DD /* PushRegistrationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF623B5985D00D7B8DD /* PushRegistrationType.swift */; }; 36 | FDD85B0023B5985D00D7B8DD /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF723B5985D00D7B8DD /* AppEnvironment.swift */; }; 37 | FDD85B0123B5985D00D7B8DD /* Nib.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF823B5985D00D7B8DD /* Nib.swift */; }; 38 | FDD85B0223B5985D00D7B8DD /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AF923B5985D00D7B8DD /* Environment.swift */; }; 39 | FDD85B0323B5985D00D7B8DD /* PushRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85AFA23B5985D00D7B8DD /* PushRegistration.swift */; }; 40 | FDD85B2323B8C49000D7B8DD /* ServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1123B8C49000D7B8DD /* ServiceType.swift */; }; 41 | FDD85B2423B8C49000D7B8DD /* EnvironmentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1223B8C49000D7B8DD /* EnvironmentType.swift */; }; 42 | FDD85B2523B8C49000D7B8DD /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1323B8C49000D7B8DD /* Service.swift */; }; 43 | FDD85B2623B8C49000D7B8DD /* Service+RequestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1423B8C49000D7B8DD /* Service+RequestHelpers.swift */; }; 44 | FDD85B2723B8C49000D7B8DD /* VoidEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1623B8C49000D7B8DD /* VoidEnvelope.swift */; }; 45 | FDD85B2823B8C49000D7B8DD /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1723B8C49000D7B8DD /* AccessToken.swift */; }; 46 | FDD85B2923B8C49000D7B8DD /* ErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1823B8C49000D7B8DD /* ErrorCode.swift */; }; 47 | FDD85B2A23B8C49000D7B8DD /* AccessTokenTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1A23B8C49000D7B8DD /* AccessTokenTemplates.swift */; }; 48 | FDD85B2B23B8C49000D7B8DD /* ErrorEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1B23B8C49000D7B8DD /* ErrorEnvelope.swift */; }; 49 | FDD85B2C23B8C49000D7B8DD /* URLSession+RequestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1D23B8C49000D7B8DD /* URLSession+RequestHelpers.swift */; }; 50 | FDD85B2D23B8C49000D7B8DD /* ServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B1E23B8C49000D7B8DD /* ServerConfig.swift */; }; 51 | FDD85B2E23B8C49000D7B8DD /* RequestMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B2023B8C49000D7B8DD /* RequestMethod.swift */; }; 52 | FDD85B2F23B8C49000D7B8DD /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B2123B8C49000D7B8DD /* Route.swift */; }; 53 | FDD85B3023B8C49000D7B8DD /* MockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B2223B8C49000D7B8DD /* MockService.swift */; }; 54 | FDD85B3923B8CF4A00D7B8DD /* ValueCellDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B3523B8CF4A00D7B8DD /* ValueCellDataSource.swift */; }; 55 | FDD85B3A23B8CF4A00D7B8DD /* ValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B3623B8CF4A00D7B8DD /* ValueCell.swift */; }; 56 | FDD85B4923B8CFD600D7B8DD /* AppDelegateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B4023B8CFD600D7B8DD /* AppDelegateViewModel.swift */; }; 57 | FDD85B5B23B8D13B00D7B8DD /* ValueCellDataSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B5423B8D13A00D7B8DD /* ValueCellDataSourceTest.swift */; }; 58 | FDD85B5C23B8D13B00D7B8DD /* ServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B5623B8D13A00D7B8DD /* ServiceTests.swift */; }; 59 | FDD85B5D23B8D13B00D7B8DD /* ServiceTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B5723B8D13A00D7B8DD /* ServiceTypeTests.swift */; }; 60 | FDD85B8023B8DD0600D7B8DD /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B6F23B8DD0500D7B8DD /* User.swift */; }; 61 | FDD85B8623B8DD0600D7B8DD /* UserTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B7623B8DD0500D7B8DD /* UserTemplates.swift */; }; 62 | FDD85B8E23B8E44000D7B8DD /* UITableViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD85B8D23B8E44000D7B8DD /* UITableViewExtensions.swift */; }; 63 | /* End PBXBuildFile section */ 64 | 65 | /* Begin PBXContainerItemProxy section */ 66 | FDD85AC323B4BB6D00D7B8DD /* PBXContainerItemProxy */ = { 67 | isa = PBXContainerItemProxy; 68 | containerPortal = FDD85AA423B4BB6B00D7B8DD /* Project object */; 69 | proxyType = 1; 70 | remoteGlobalIDString = FDD85AAB23B4BB6B00D7B8DD; 71 | remoteInfo = "iOS-MVVM-Template"; 72 | }; 73 | /* End PBXContainerItemProxy section */ 74 | 75 | /* Begin PBXFileReference section */ 76 | 344DE535F1B529B9C384CB87 /* Pods-iOS-MVVM-Template.debug staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-MVVM-Template.debug staging.xcconfig"; path = "Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template.debug staging.xcconfig"; sourceTree = ""; }; 77 | 62F501160D9CF3394B0C531D /* Pods_iOS_MVVM_Template.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_MVVM_Template.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 826C91F05D2CAE3FF0BF34EE /* Pods-iOS-MVVM-Template.release staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-MVVM-Template.release staging.xcconfig"; path = "Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template.release staging.xcconfig"; sourceTree = ""; }; 79 | 827D6180C69E25BA54D3B059 /* Pods-iOS-MVVM-Template.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-MVVM-Template.release.xcconfig"; path = "Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template.release.xcconfig"; sourceTree = ""; }; 80 | D8410366F6464789AD774A60 /* Pods-iOS-MVVM-Template.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-MVVM-Template.debug.xcconfig"; path = "Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template.debug.xcconfig"; sourceTree = ""; }; 81 | FD7D4EB3249DBF5C00246661 /* ErrorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorExtensions.swift; sourceTree = ""; }; 82 | FD7E543326677F0800ABFE61 /* EncodableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableExtensions.swift; sourceTree = ""; }; 83 | FD7E543526677F1D00ABFE61 /* DecodableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableExtensions.swift; sourceTree = ""; }; 84 | FDD85AAC23B4BB6B00D7B8DD /* iOS-MVVM-Template.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS-MVVM-Template.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 85 | FDD85AAF23B4BB6B00D7B8DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 86 | FDD85AB323B4BB6B00D7B8DD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 87 | FDD85AB623B4BB6B00D7B8DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 88 | FDD85AB823B4BB6C00D7B8DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 89 | FDD85ABB23B4BB6C00D7B8DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 90 | FDD85ABD23B4BB6C00D7B8DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 91 | FDD85AC223B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "iOS-MVVM-TemplateTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 92 | FDD85AC823B4BB6D00D7B8DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | FDD85AD423B4F2BC00D7B8DD /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; 94 | FDD85AD523B4F2BC00D7B8DD /* Global.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Global.swift; sourceTree = ""; }; 95 | FDD85ADA23B4F77500D7B8DD /* OptionalExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExtensions.swift; sourceTree = ""; }; 96 | FDD85ADB23B4F77500D7B8DD /* StringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 97 | FDD85ADC23B4F77500D7B8DD /* UIColorExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; 98 | FDD85ADD23B4F77500D7B8DD /* UIViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtensions.swift; sourceTree = ""; }; 99 | FDD85ADE23B4F77500D7B8DD /* UIScreenExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = ""; }; 100 | FDD85ADF23B4F77600D7B8DD /* UIFontExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIFontExtensions.swift; sourceTree = ""; }; 101 | FDD85AE223B4F77600D7B8DD /* DictionaryExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryExtensions.swift; sourceTree = ""; }; 102 | FDD85AE323B4F77600D7B8DD /* UIViewController+Preparation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Preparation.swift"; sourceTree = ""; }; 103 | FDD85AE423B4F77600D7B8DD /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 104 | FDD85AE523B4F77600D7B8DD /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtensions.swift; sourceTree = ""; }; 105 | FDD85AF223B5985D00D7B8DD /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; 106 | FDD85AF323B5985D00D7B8DD /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 107 | FDD85AF423B5985D00D7B8DD /* Storyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storyboard.swift; sourceTree = ""; }; 108 | FDD85AF523B5985D00D7B8DD /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 109 | FDD85AF623B5985D00D7B8DD /* PushRegistrationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationType.swift; sourceTree = ""; }; 110 | FDD85AF723B5985D00D7B8DD /* AppEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; 111 | FDD85AF823B5985D00D7B8DD /* Nib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nib.swift; sourceTree = ""; }; 112 | FDD85AF923B5985D00D7B8DD /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 113 | FDD85AFA23B5985D00D7B8DD /* PushRegistration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistration.swift; sourceTree = ""; }; 114 | FDD85B1123B8C49000D7B8DD /* ServiceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceType.swift; sourceTree = ""; }; 115 | FDD85B1223B8C49000D7B8DD /* EnvironmentType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentType.swift; sourceTree = ""; }; 116 | FDD85B1323B8C49000D7B8DD /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; 117 | FDD85B1423B8C49000D7B8DD /* Service+RequestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Service+RequestHelpers.swift"; sourceTree = ""; }; 118 | FDD85B1623B8C49000D7B8DD /* VoidEnvelope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoidEnvelope.swift; sourceTree = ""; }; 119 | FDD85B1723B8C49000D7B8DD /* AccessToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = ""; }; 120 | FDD85B1823B8C49000D7B8DD /* ErrorCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorCode.swift; sourceTree = ""; }; 121 | FDD85B1A23B8C49000D7B8DD /* AccessTokenTemplates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessTokenTemplates.swift; sourceTree = ""; }; 122 | FDD85B1B23B8C49000D7B8DD /* ErrorEnvelope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorEnvelope.swift; sourceTree = ""; }; 123 | FDD85B1D23B8C49000D7B8DD /* URLSession+RequestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLSession+RequestHelpers.swift"; sourceTree = ""; }; 124 | FDD85B1E23B8C49000D7B8DD /* ServerConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = ""; }; 125 | FDD85B2023B8C49000D7B8DD /* RequestMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestMethod.swift; sourceTree = ""; }; 126 | FDD85B2123B8C49000D7B8DD /* Route.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 127 | FDD85B2223B8C49000D7B8DD /* MockService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockService.swift; sourceTree = ""; }; 128 | FDD85B3523B8CF4A00D7B8DD /* ValueCellDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueCellDataSource.swift; sourceTree = ""; }; 129 | FDD85B3623B8CF4A00D7B8DD /* ValueCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueCell.swift; sourceTree = ""; }; 130 | FDD85B4023B8CFD600D7B8DD /* AppDelegateViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateViewModel.swift; sourceTree = ""; }; 131 | FDD85B5423B8D13A00D7B8DD /* ValueCellDataSourceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueCellDataSourceTest.swift; sourceTree = ""; }; 132 | FDD85B5623B8D13A00D7B8DD /* ServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTests.swift; sourceTree = ""; }; 133 | FDD85B5723B8D13A00D7B8DD /* ServiceTypeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTypeTests.swift; sourceTree = ""; }; 134 | FDD85B6023B8D5DE00D7B8DD /* PKHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PKHUD.framework; path = Carthage/Build/iOS/PKHUD.framework; sourceTree = ""; }; 135 | FDD85B6123B8D5DE00D7B8DD /* ReactiveExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveExtensions.framework; path = Carthage/Build/iOS/ReactiveExtensions.framework; sourceTree = ""; }; 136 | FDD85B6223B8D5DE00D7B8DD /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/iOS/ReactiveSwift.framework; sourceTree = ""; }; 137 | FDD85B6F23B8DD0500D7B8DD /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 138 | FDD85B7623B8DD0500D7B8DD /* UserTemplates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserTemplates.swift; sourceTree = ""; }; 139 | FDD85B8D23B8E44000D7B8DD /* UITableViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewExtensions.swift; sourceTree = ""; }; 140 | /* End PBXFileReference section */ 141 | 142 | /* Begin PBXFrameworksBuildPhase section */ 143 | FDD85AA923B4BB6B00D7B8DD /* Frameworks */ = { 144 | isa = PBXFrameworksBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | AD8FA66F098FC2C2B5DD72AB /* Pods_iOS_MVVM_Template.framework in Frameworks */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | FDD85ABF23B4BB6D00D7B8DD /* Frameworks */ = { 152 | isa = PBXFrameworksBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXFrameworksBuildPhase section */ 159 | 160 | /* Begin PBXGroup section */ 161 | 329A594DCE1D498E29E178E8 /* Pods */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | D8410366F6464789AD774A60 /* Pods-iOS-MVVM-Template.debug.xcconfig */, 165 | 827D6180C69E25BA54D3B059 /* Pods-iOS-MVVM-Template.release.xcconfig */, 166 | 344DE535F1B529B9C384CB87 /* Pods-iOS-MVVM-Template.debug staging.xcconfig */, 167 | 826C91F05D2CAE3FF0BF34EE /* Pods-iOS-MVVM-Template.release staging.xcconfig */, 168 | ); 169 | path = Pods; 170 | sourceTree = ""; 171 | }; 172 | FDD85AA323B4BB6B00D7B8DD = { 173 | isa = PBXGroup; 174 | children = ( 175 | FDD85AAE23B4BB6B00D7B8DD /* iOS-MVVM-Template */, 176 | FDD85AC523B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests */, 177 | FDD85AAD23B4BB6B00D7B8DD /* Products */, 178 | FDD85B5F23B8D5DE00D7B8DD /* Frameworks */, 179 | 329A594DCE1D498E29E178E8 /* Pods */, 180 | ); 181 | sourceTree = ""; 182 | }; 183 | FDD85AAD23B4BB6B00D7B8DD /* Products */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | FDD85AAC23B4BB6B00D7B8DD /* iOS-MVVM-Template.app */, 187 | FDD85AC223B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests.xctest */, 188 | ); 189 | name = Products; 190 | sourceTree = ""; 191 | }; 192 | FDD85AAE23B4BB6B00D7B8DD /* iOS-MVVM-Template */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | FDD85B1023B8C49000D7B8DD /* Api */, 196 | FDD85AD323B4F2A800D7B8DD /* Configs */, 197 | FDD85B6B23B8DD0500D7B8DD /* DataModels */, 198 | FDD85B3123B8CF4A00D7B8DD /* DataSources */, 199 | FDD85AD823B4F75300D7B8DD /* Library */, 200 | FDD85AD223B4F28F00D7B8DD /* Resources */, 201 | FDD85B3B23B8CFD600D7B8DD /* ViewModels */, 202 | FDD85B4E23B8D0DB00D7B8DD /* Views&Controllers */, 203 | FDD85AAF23B4BB6B00D7B8DD /* AppDelegate.swift */, 204 | FDD85AB323B4BB6B00D7B8DD /* ViewController.swift */, 205 | FDD85AB523B4BB6B00D7B8DD /* Main.storyboard */, 206 | FDD85ABA23B4BB6C00D7B8DD /* LaunchScreen.storyboard */, 207 | FDD85ABD23B4BB6C00D7B8DD /* Info.plist */, 208 | ); 209 | path = "iOS-MVVM-Template"; 210 | sourceTree = ""; 211 | }; 212 | FDD85AC523B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | FDD85B5523B8D13A00D7B8DD /* Api */, 216 | FDD85B5E23B8D1A100D7B8DD /* DataSources */, 217 | FDD85AC823B4BB6D00D7B8DD /* Info.plist */, 218 | ); 219 | path = "iOS-MVVM-TemplateTests"; 220 | sourceTree = ""; 221 | }; 222 | FDD85AD223B4F28F00D7B8DD /* Resources */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | FDD85AB823B4BB6C00D7B8DD /* Assets.xcassets */, 226 | ); 227 | path = Resources; 228 | sourceTree = ""; 229 | }; 230 | FDD85AD323B4F2A800D7B8DD /* Configs */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | FDD85AD523B4F2BC00D7B8DD /* Global.swift */, 234 | FDD85AD423B4F2BC00D7B8DD /* Secrets.swift */, 235 | ); 236 | path = Configs; 237 | sourceTree = ""; 238 | }; 239 | FDD85AD823B4F75300D7B8DD /* Library */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | FDD85AD923B4F75E00D7B8DD /* Extensions */, 243 | FDD85AF523B5985D00D7B8DD /* App.swift */, 244 | FDD85AF723B5985D00D7B8DD /* AppEnvironment.swift */, 245 | FDD85AF923B5985D00D7B8DD /* Environment.swift */, 246 | FDD85AF223B5985D00D7B8DD /* Keyboard.swift */, 247 | FDD85AF823B5985D00D7B8DD /* Nib.swift */, 248 | FDD85AF323B5985D00D7B8DD /* Notifications.swift */, 249 | FDD85AFA23B5985D00D7B8DD /* PushRegistration.swift */, 250 | FDD85AF623B5985D00D7B8DD /* PushRegistrationType.swift */, 251 | FDD85AF423B5985D00D7B8DD /* Storyboard.swift */, 252 | ); 253 | path = Library; 254 | sourceTree = ""; 255 | }; 256 | FDD85AD923B4F75E00D7B8DD /* Extensions */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | FD7E543526677F1D00ABFE61 /* DecodableExtensions.swift */, 260 | FDD85AE223B4F77600D7B8DD /* DictionaryExtensions.swift */, 261 | FD7E543326677F0800ABFE61 /* EncodableExtensions.swift */, 262 | FD7D4EB3249DBF5C00246661 /* ErrorExtensions.swift */, 263 | FDD85ADA23B4F77500D7B8DD /* OptionalExtensions.swift */, 264 | FDD85ADB23B4F77500D7B8DD /* StringExtensions.swift */, 265 | FDD85AE523B4F77600D7B8DD /* UIAlertControllerExtensions.swift */, 266 | FDD85ADC23B4F77500D7B8DD /* UIColorExtensions.swift */, 267 | FDD85ADF23B4F77600D7B8DD /* UIFontExtensions.swift */, 268 | FDD85ADE23B4F77500D7B8DD /* UIScreenExtensions.swift */, 269 | FDD85B8D23B8E44000D7B8DD /* UITableViewExtensions.swift */, 270 | FDD85AE323B4F77600D7B8DD /* UIViewController+Preparation.swift */, 271 | FDD85ADD23B4F77500D7B8DD /* UIViewControllerExtensions.swift */, 272 | FDD85AE423B4F77600D7B8DD /* UIViewExtensions.swift */, 273 | ); 274 | path = Extensions; 275 | sourceTree = ""; 276 | }; 277 | FDD85B1023B8C49000D7B8DD /* Api */ = { 278 | isa = PBXGroup; 279 | children = ( 280 | FDD85B1523B8C49000D7B8DD /* Models */, 281 | FDD85B1C23B8C49000D7B8DD /* Extensions */, 282 | FDD85B1F23B8C49000D7B8DD /* Lib */, 283 | FDD85B1223B8C49000D7B8DD /* EnvironmentType.swift */, 284 | FDD85B2223B8C49000D7B8DD /* MockService.swift */, 285 | FDD85B1323B8C49000D7B8DD /* Service.swift */, 286 | FDD85B1E23B8C49000D7B8DD /* ServerConfig.swift */, 287 | FDD85B1423B8C49000D7B8DD /* Service+RequestHelpers.swift */, 288 | FDD85B1123B8C49000D7B8DD /* ServiceType.swift */, 289 | ); 290 | path = Api; 291 | sourceTree = ""; 292 | }; 293 | FDD85B1523B8C49000D7B8DD /* Models */ = { 294 | isa = PBXGroup; 295 | children = ( 296 | FDD85B1723B8C49000D7B8DD /* AccessToken.swift */, 297 | FDD85B1823B8C49000D7B8DD /* ErrorCode.swift */, 298 | FDD85B1B23B8C49000D7B8DD /* ErrorEnvelope.swift */, 299 | FDD85B1623B8C49000D7B8DD /* VoidEnvelope.swift */, 300 | FDD85B1923B8C49000D7B8DD /* Templates */, 301 | ); 302 | path = Models; 303 | sourceTree = ""; 304 | }; 305 | FDD85B1923B8C49000D7B8DD /* Templates */ = { 306 | isa = PBXGroup; 307 | children = ( 308 | FDD85B1A23B8C49000D7B8DD /* AccessTokenTemplates.swift */, 309 | ); 310 | path = Templates; 311 | sourceTree = ""; 312 | }; 313 | FDD85B1C23B8C49000D7B8DD /* Extensions */ = { 314 | isa = PBXGroup; 315 | children = ( 316 | FDD85B1D23B8C49000D7B8DD /* URLSession+RequestHelpers.swift */, 317 | ); 318 | path = Extensions; 319 | sourceTree = ""; 320 | }; 321 | FDD85B1F23B8C49000D7B8DD /* Lib */ = { 322 | isa = PBXGroup; 323 | children = ( 324 | FDD85B2023B8C49000D7B8DD /* RequestMethod.swift */, 325 | FDD85B2123B8C49000D7B8DD /* Route.swift */, 326 | ); 327 | path = Lib; 328 | sourceTree = ""; 329 | }; 330 | FDD85B3123B8CF4A00D7B8DD /* DataSources */ = { 331 | isa = PBXGroup; 332 | children = ( 333 | FDD85B3423B8CF4A00D7B8DD /* Base */, 334 | ); 335 | path = DataSources; 336 | sourceTree = ""; 337 | }; 338 | FDD85B3423B8CF4A00D7B8DD /* Base */ = { 339 | isa = PBXGroup; 340 | children = ( 341 | FDD85B3623B8CF4A00D7B8DD /* ValueCell.swift */, 342 | FDD85B3523B8CF4A00D7B8DD /* ValueCellDataSource.swift */, 343 | ); 344 | path = Base; 345 | sourceTree = ""; 346 | }; 347 | FDD85B3B23B8CFD600D7B8DD /* ViewModels */ = { 348 | isa = PBXGroup; 349 | children = ( 350 | FDD85B4023B8CFD600D7B8DD /* AppDelegateViewModel.swift */, 351 | ); 352 | path = ViewModels; 353 | sourceTree = ""; 354 | }; 355 | FDD85B4E23B8D0DB00D7B8DD /* Views&Controllers */ = { 356 | isa = PBXGroup; 357 | children = ( 358 | ); 359 | path = "Views&Controllers"; 360 | sourceTree = ""; 361 | }; 362 | FDD85B5523B8D13A00D7B8DD /* Api */ = { 363 | isa = PBXGroup; 364 | children = ( 365 | FDD85B5623B8D13A00D7B8DD /* ServiceTests.swift */, 366 | FDD85B5723B8D13A00D7B8DD /* ServiceTypeTests.swift */, 367 | ); 368 | path = Api; 369 | sourceTree = ""; 370 | }; 371 | FDD85B5E23B8D1A100D7B8DD /* DataSources */ = { 372 | isa = PBXGroup; 373 | children = ( 374 | FDD85B5423B8D13A00D7B8DD /* ValueCellDataSourceTest.swift */, 375 | ); 376 | path = DataSources; 377 | sourceTree = ""; 378 | }; 379 | FDD85B5F23B8D5DE00D7B8DD /* Frameworks */ = { 380 | isa = PBXGroup; 381 | children = ( 382 | FDD85B6023B8D5DE00D7B8DD /* PKHUD.framework */, 383 | FDD85B6123B8D5DE00D7B8DD /* ReactiveExtensions.framework */, 384 | FDD85B6223B8D5DE00D7B8DD /* ReactiveSwift.framework */, 385 | 62F501160D9CF3394B0C531D /* Pods_iOS_MVVM_Template.framework */, 386 | ); 387 | name = Frameworks; 388 | sourceTree = ""; 389 | }; 390 | FDD85B6B23B8DD0500D7B8DD /* DataModels */ = { 391 | isa = PBXGroup; 392 | children = ( 393 | FDD85B6F23B8DD0500D7B8DD /* User.swift */, 394 | FDD85B7423B8DD0500D7B8DD /* Templates */, 395 | ); 396 | path = DataModels; 397 | sourceTree = ""; 398 | }; 399 | FDD85B7423B8DD0500D7B8DD /* Templates */ = { 400 | isa = PBXGroup; 401 | children = ( 402 | FDD85B7623B8DD0500D7B8DD /* UserTemplates.swift */, 403 | ); 404 | path = Templates; 405 | sourceTree = ""; 406 | }; 407 | /* End PBXGroup section */ 408 | 409 | /* Begin PBXNativeTarget section */ 410 | FDD85AAB23B4BB6B00D7B8DD /* iOS-MVVM-Template */ = { 411 | isa = PBXNativeTarget; 412 | buildConfigurationList = FDD85ACB23B4BB6D00D7B8DD /* Build configuration list for PBXNativeTarget "iOS-MVVM-Template" */; 413 | buildPhases = ( 414 | 4540A5AE9D6008F8A591F95E /* [CP] Check Pods Manifest.lock */, 415 | FDD85AA823B4BB6B00D7B8DD /* Sources */, 416 | FDD85AA923B4BB6B00D7B8DD /* Frameworks */, 417 | FDD85AAA23B4BB6B00D7B8DD /* Resources */, 418 | FDD85AD123B4F15200D7B8DD /* SwiftLint */, 419 | B09216E026132C9F0DB7C89E /* [CP] Embed Pods Frameworks */, 420 | ); 421 | buildRules = ( 422 | ); 423 | dependencies = ( 424 | ); 425 | name = "iOS-MVVM-Template"; 426 | productName = "iOS-MVVM-Template"; 427 | productReference = FDD85AAC23B4BB6B00D7B8DD /* iOS-MVVM-Template.app */; 428 | productType = "com.apple.product-type.application"; 429 | }; 430 | FDD85AC123B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests */ = { 431 | isa = PBXNativeTarget; 432 | buildConfigurationList = FDD85ACE23B4BB6D00D7B8DD /* Build configuration list for PBXNativeTarget "iOS-MVVM-TemplateTests" */; 433 | buildPhases = ( 434 | FDD85ABE23B4BB6D00D7B8DD /* Sources */, 435 | FDD85ABF23B4BB6D00D7B8DD /* Frameworks */, 436 | FDD85AC023B4BB6D00D7B8DD /* Resources */, 437 | ); 438 | buildRules = ( 439 | ); 440 | dependencies = ( 441 | FDD85AC423B4BB6D00D7B8DD /* PBXTargetDependency */, 442 | ); 443 | name = "iOS-MVVM-TemplateTests"; 444 | productName = "iOS-MVVM-TemplateTests"; 445 | productReference = FDD85AC223B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests.xctest */; 446 | productType = "com.apple.product-type.bundle.unit-test"; 447 | }; 448 | /* End PBXNativeTarget section */ 449 | 450 | /* Begin PBXProject section */ 451 | FDD85AA423B4BB6B00D7B8DD /* Project object */ = { 452 | isa = PBXProject; 453 | attributes = { 454 | LastSwiftUpdateCheck = 1130; 455 | LastUpgradeCheck = 1250; 456 | ORGANIZATIONNAME = Lebron; 457 | TargetAttributes = { 458 | FDD85AAB23B4BB6B00D7B8DD = { 459 | CreatedOnToolsVersion = 11.3; 460 | }; 461 | FDD85AC123B4BB6D00D7B8DD = { 462 | CreatedOnToolsVersion = 11.3; 463 | LastSwiftMigration = 1130; 464 | TestTargetID = FDD85AAB23B4BB6B00D7B8DD; 465 | }; 466 | }; 467 | }; 468 | buildConfigurationList = FDD85AA723B4BB6B00D7B8DD /* Build configuration list for PBXProject "iOS-MVVM-Template" */; 469 | compatibilityVersion = "Xcode 9.3"; 470 | developmentRegion = en; 471 | hasScannedForEncodings = 0; 472 | knownRegions = ( 473 | en, 474 | Base, 475 | ); 476 | mainGroup = FDD85AA323B4BB6B00D7B8DD; 477 | productRefGroup = FDD85AAD23B4BB6B00D7B8DD /* Products */; 478 | projectDirPath = ""; 479 | projectRoot = ""; 480 | targets = ( 481 | FDD85AAB23B4BB6B00D7B8DD /* iOS-MVVM-Template */, 482 | FDD85AC123B4BB6D00D7B8DD /* iOS-MVVM-TemplateTests */, 483 | ); 484 | }; 485 | /* End PBXProject section */ 486 | 487 | /* Begin PBXResourcesBuildPhase section */ 488 | FDD85AAA23B4BB6B00D7B8DD /* Resources */ = { 489 | isa = PBXResourcesBuildPhase; 490 | buildActionMask = 2147483647; 491 | files = ( 492 | FDD85ABC23B4BB6C00D7B8DD /* LaunchScreen.storyboard in Resources */, 493 | FDD85AB923B4BB6C00D7B8DD /* Assets.xcassets in Resources */, 494 | FDD85AB723B4BB6B00D7B8DD /* Main.storyboard in Resources */, 495 | ); 496 | runOnlyForDeploymentPostprocessing = 0; 497 | }; 498 | FDD85AC023B4BB6D00D7B8DD /* Resources */ = { 499 | isa = PBXResourcesBuildPhase; 500 | buildActionMask = 2147483647; 501 | files = ( 502 | ); 503 | runOnlyForDeploymentPostprocessing = 0; 504 | }; 505 | /* End PBXResourcesBuildPhase section */ 506 | 507 | /* Begin PBXShellScriptBuildPhase section */ 508 | 4540A5AE9D6008F8A591F95E /* [CP] Check Pods Manifest.lock */ = { 509 | isa = PBXShellScriptBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | ); 513 | inputFileListPaths = ( 514 | ); 515 | inputPaths = ( 516 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 517 | "${PODS_ROOT}/Manifest.lock", 518 | ); 519 | name = "[CP] Check Pods Manifest.lock"; 520 | outputFileListPaths = ( 521 | ); 522 | outputPaths = ( 523 | "$(DERIVED_FILE_DIR)/Pods-iOS-MVVM-Template-checkManifestLockResult.txt", 524 | ); 525 | runOnlyForDeploymentPostprocessing = 0; 526 | shellPath = /bin/sh; 527 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 528 | showEnvVarsInLog = 0; 529 | }; 530 | B09216E026132C9F0DB7C89E /* [CP] Embed Pods Frameworks */ = { 531 | isa = PBXShellScriptBuildPhase; 532 | buildActionMask = 2147483647; 533 | files = ( 534 | ); 535 | inputFileListPaths = ( 536 | "${PODS_ROOT}/Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template-frameworks-${CONFIGURATION}-input-files.xcfilelist", 537 | ); 538 | name = "[CP] Embed Pods Frameworks"; 539 | outputFileListPaths = ( 540 | "${PODS_ROOT}/Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template-frameworks-${CONFIGURATION}-output-files.xcfilelist", 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | shellPath = /bin/sh; 544 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS-MVVM-Template/Pods-iOS-MVVM-Template-frameworks.sh\"\n"; 545 | showEnvVarsInLog = 0; 546 | }; 547 | FDD85AD123B4F15200D7B8DD /* SwiftLint */ = { 548 | isa = PBXShellScriptBuildPhase; 549 | buildActionMask = 2147483647; 550 | files = ( 551 | ); 552 | inputFileListPaths = ( 553 | ); 554 | inputPaths = ( 555 | ); 556 | name = SwiftLint; 557 | outputFileListPaths = ( 558 | ); 559 | outputPaths = ( 560 | ); 561 | runOnlyForDeploymentPostprocessing = 0; 562 | shellPath = /bin/sh; 563 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 564 | }; 565 | /* End PBXShellScriptBuildPhase section */ 566 | 567 | /* Begin PBXSourcesBuildPhase section */ 568 | FDD85AA823B4BB6B00D7B8DD /* Sources */ = { 569 | isa = PBXSourcesBuildPhase; 570 | buildActionMask = 2147483647; 571 | files = ( 572 | FDD85B4923B8CFD600D7B8DD /* AppDelegateViewModel.swift in Sources */, 573 | FDD85B2D23B8C49000D7B8DD /* ServerConfig.swift in Sources */, 574 | FDD85B2723B8C49000D7B8DD /* VoidEnvelope.swift in Sources */, 575 | FDD85AB423B4BB6B00D7B8DD /* ViewController.swift in Sources */, 576 | FDD85B8623B8DD0600D7B8DD /* UserTemplates.swift in Sources */, 577 | FDD85AEE23B4F77600D7B8DD /* DictionaryExtensions.swift in Sources */, 578 | FDD85AE623B4F77600D7B8DD /* OptionalExtensions.swift in Sources */, 579 | FDD85AFE23B5985D00D7B8DD /* App.swift in Sources */, 580 | FDD85B0323B5985D00D7B8DD /* PushRegistration.swift in Sources */, 581 | FDD85B2523B8C49000D7B8DD /* Service.swift in Sources */, 582 | FDD85AD623B4F2BD00D7B8DD /* Secrets.swift in Sources */, 583 | FDD85AF123B4F77600D7B8DD /* UIAlertControllerExtensions.swift in Sources */, 584 | FDD85B2923B8C49000D7B8DD /* ErrorCode.swift in Sources */, 585 | FDD85AFF23B5985D00D7B8DD /* PushRegistrationType.swift in Sources */, 586 | FDD85AE723B4F77600D7B8DD /* StringExtensions.swift in Sources */, 587 | FDD85AEB23B4F77600D7B8DD /* UIFontExtensions.swift in Sources */, 588 | FDD85AE923B4F77600D7B8DD /* UIViewControllerExtensions.swift in Sources */, 589 | FDD85AEF23B4F77600D7B8DD /* UIViewController+Preparation.swift in Sources */, 590 | FDD85B2E23B8C49000D7B8DD /* RequestMethod.swift in Sources */, 591 | FD7E543426677F0800ABFE61 /* EncodableExtensions.swift in Sources */, 592 | FDD85AF023B4F77600D7B8DD /* UIViewExtensions.swift in Sources */, 593 | FDD85AFB23B5985D00D7B8DD /* Keyboard.swift in Sources */, 594 | FDD85B8023B8DD0600D7B8DD /* User.swift in Sources */, 595 | FDD85B0223B5985D00D7B8DD /* Environment.swift in Sources */, 596 | FDD85B8E23B8E44000D7B8DD /* UITableViewExtensions.swift in Sources */, 597 | FDD85B2B23B8C49000D7B8DD /* ErrorEnvelope.swift in Sources */, 598 | FDD85B2323B8C49000D7B8DD /* ServiceType.swift in Sources */, 599 | FD7D4EB4249DBF5C00246661 /* ErrorExtensions.swift in Sources */, 600 | FDD85B3923B8CF4A00D7B8DD /* ValueCellDataSource.swift in Sources */, 601 | FDD85B2C23B8C49000D7B8DD /* URLSession+RequestHelpers.swift in Sources */, 602 | FDD85AB023B4BB6B00D7B8DD /* AppDelegate.swift in Sources */, 603 | FDD85B2423B8C49000D7B8DD /* EnvironmentType.swift in Sources */, 604 | FDD85B2623B8C49000D7B8DD /* Service+RequestHelpers.swift in Sources */, 605 | FDD85B0023B5985D00D7B8DD /* AppEnvironment.swift in Sources */, 606 | FDD85B2A23B8C49000D7B8DD /* AccessTokenTemplates.swift in Sources */, 607 | FDD85B0123B5985D00D7B8DD /* Nib.swift in Sources */, 608 | FDD85B3023B8C49000D7B8DD /* MockService.swift in Sources */, 609 | FDD85AD723B4F2BD00D7B8DD /* Global.swift in Sources */, 610 | FDD85AFC23B5985D00D7B8DD /* Notifications.swift in Sources */, 611 | FD7E543626677F1D00ABFE61 /* DecodableExtensions.swift in Sources */, 612 | FDD85B2F23B8C49000D7B8DD /* Route.swift in Sources */, 613 | FDD85AEA23B4F77600D7B8DD /* UIScreenExtensions.swift in Sources */, 614 | FDD85B3A23B8CF4A00D7B8DD /* ValueCell.swift in Sources */, 615 | FDD85AFD23B5985D00D7B8DD /* Storyboard.swift in Sources */, 616 | FDD85AE823B4F77600D7B8DD /* UIColorExtensions.swift in Sources */, 617 | FDD85B2823B8C49000D7B8DD /* AccessToken.swift in Sources */, 618 | ); 619 | runOnlyForDeploymentPostprocessing = 0; 620 | }; 621 | FDD85ABE23B4BB6D00D7B8DD /* Sources */ = { 622 | isa = PBXSourcesBuildPhase; 623 | buildActionMask = 2147483647; 624 | files = ( 625 | FDD85B5B23B8D13B00D7B8DD /* ValueCellDataSourceTest.swift in Sources */, 626 | FDD85B5C23B8D13B00D7B8DD /* ServiceTests.swift in Sources */, 627 | FDD85B5D23B8D13B00D7B8DD /* ServiceTypeTests.swift in Sources */, 628 | ); 629 | runOnlyForDeploymentPostprocessing = 0; 630 | }; 631 | /* End PBXSourcesBuildPhase section */ 632 | 633 | /* Begin PBXTargetDependency section */ 634 | FDD85AC423B4BB6D00D7B8DD /* PBXTargetDependency */ = { 635 | isa = PBXTargetDependency; 636 | target = FDD85AAB23B4BB6B00D7B8DD /* iOS-MVVM-Template */; 637 | targetProxy = FDD85AC323B4BB6D00D7B8DD /* PBXContainerItemProxy */; 638 | }; 639 | /* End PBXTargetDependency section */ 640 | 641 | /* Begin PBXVariantGroup section */ 642 | FDD85AB523B4BB6B00D7B8DD /* Main.storyboard */ = { 643 | isa = PBXVariantGroup; 644 | children = ( 645 | FDD85AB623B4BB6B00D7B8DD /* Base */, 646 | ); 647 | name = Main.storyboard; 648 | sourceTree = ""; 649 | }; 650 | FDD85ABA23B4BB6C00D7B8DD /* LaunchScreen.storyboard */ = { 651 | isa = PBXVariantGroup; 652 | children = ( 653 | FDD85ABB23B4BB6C00D7B8DD /* Base */, 654 | ); 655 | name = LaunchScreen.storyboard; 656 | sourceTree = ""; 657 | }; 658 | /* End PBXVariantGroup section */ 659 | 660 | /* Begin XCBuildConfiguration section */ 661 | FDD85AC923B4BB6D00D7B8DD /* Debug */ = { 662 | isa = XCBuildConfiguration; 663 | buildSettings = { 664 | ALWAYS_SEARCH_USER_PATHS = NO; 665 | CLANG_ANALYZER_NONNULL = YES; 666 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 667 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 668 | CLANG_CXX_LIBRARY = "libc++"; 669 | CLANG_ENABLE_MODULES = YES; 670 | CLANG_ENABLE_OBJC_ARC = YES; 671 | CLANG_ENABLE_OBJC_WEAK = YES; 672 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 673 | CLANG_WARN_BOOL_CONVERSION = YES; 674 | CLANG_WARN_COMMA = YES; 675 | CLANG_WARN_CONSTANT_CONVERSION = YES; 676 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 677 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 678 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 679 | CLANG_WARN_EMPTY_BODY = YES; 680 | CLANG_WARN_ENUM_CONVERSION = YES; 681 | CLANG_WARN_INFINITE_RECURSION = YES; 682 | CLANG_WARN_INT_CONVERSION = YES; 683 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 684 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 685 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 686 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 687 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 688 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 689 | CLANG_WARN_STRICT_PROTOTYPES = YES; 690 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 691 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 692 | CLANG_WARN_UNREACHABLE_CODE = YES; 693 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 694 | COPY_PHASE_STRIP = NO; 695 | DEBUG_INFORMATION_FORMAT = dwarf; 696 | ENABLE_STRICT_OBJC_MSGSEND = YES; 697 | ENABLE_TESTABILITY = YES; 698 | GCC_C_LANGUAGE_STANDARD = gnu11; 699 | GCC_DYNAMIC_NO_PIC = NO; 700 | GCC_NO_COMMON_BLOCKS = YES; 701 | GCC_OPTIMIZATION_LEVEL = 0; 702 | GCC_PREPROCESSOR_DEFINITIONS = ( 703 | "DEBUG=1", 704 | "$(inherited)", 705 | ); 706 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 707 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 708 | GCC_WARN_UNDECLARED_SELECTOR = YES; 709 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 710 | GCC_WARN_UNUSED_FUNCTION = YES; 711 | GCC_WARN_UNUSED_VARIABLE = YES; 712 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 713 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 714 | MTL_FAST_MATH = YES; 715 | ONLY_ACTIVE_ARCH = YES; 716 | SDKROOT = iphoneos; 717 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 718 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 719 | }; 720 | name = Debug; 721 | }; 722 | FDD85ACA23B4BB6D00D7B8DD /* Release */ = { 723 | isa = XCBuildConfiguration; 724 | buildSettings = { 725 | ALWAYS_SEARCH_USER_PATHS = NO; 726 | CLANG_ANALYZER_NONNULL = YES; 727 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 728 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 729 | CLANG_CXX_LIBRARY = "libc++"; 730 | CLANG_ENABLE_MODULES = YES; 731 | CLANG_ENABLE_OBJC_ARC = YES; 732 | CLANG_ENABLE_OBJC_WEAK = YES; 733 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 734 | CLANG_WARN_BOOL_CONVERSION = YES; 735 | CLANG_WARN_COMMA = YES; 736 | CLANG_WARN_CONSTANT_CONVERSION = YES; 737 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 738 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 739 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 740 | CLANG_WARN_EMPTY_BODY = YES; 741 | CLANG_WARN_ENUM_CONVERSION = YES; 742 | CLANG_WARN_INFINITE_RECURSION = YES; 743 | CLANG_WARN_INT_CONVERSION = YES; 744 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 745 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 746 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 747 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 748 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 749 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 750 | CLANG_WARN_STRICT_PROTOTYPES = YES; 751 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 752 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 753 | CLANG_WARN_UNREACHABLE_CODE = YES; 754 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 755 | COPY_PHASE_STRIP = NO; 756 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 757 | ENABLE_NS_ASSERTIONS = NO; 758 | ENABLE_STRICT_OBJC_MSGSEND = YES; 759 | GCC_C_LANGUAGE_STANDARD = gnu11; 760 | GCC_NO_COMMON_BLOCKS = YES; 761 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 762 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 763 | GCC_WARN_UNDECLARED_SELECTOR = YES; 764 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 765 | GCC_WARN_UNUSED_FUNCTION = YES; 766 | GCC_WARN_UNUSED_VARIABLE = YES; 767 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 768 | MTL_ENABLE_DEBUG_INFO = NO; 769 | MTL_FAST_MATH = YES; 770 | SDKROOT = iphoneos; 771 | SWIFT_COMPILATION_MODE = wholemodule; 772 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 773 | VALIDATE_PRODUCT = YES; 774 | }; 775 | name = Release; 776 | }; 777 | FDD85ACC23B4BB6D00D7B8DD /* Debug */ = { 778 | isa = XCBuildConfiguration; 779 | baseConfigurationReference = D8410366F6464789AD774A60 /* Pods-iOS-MVVM-Template.debug.xcconfig */; 780 | buildSettings = { 781 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 782 | CODE_SIGN_STYLE = Automatic; 783 | FRAMEWORK_SEARCH_PATHS = ( 784 | "$(inherited)", 785 | "$(PROJECT_DIR)/Carthage/Build/iOS", 786 | ); 787 | INFOPLIST_FILE = "iOS-MVVM-Template/Info.plist"; 788 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 789 | IS_STAGING = NO; 790 | LD_RUNPATH_SEARCH_PATHS = ( 791 | "$(inherited)", 792 | "@executable_path/Frameworks", 793 | ); 794 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-Template"; 795 | PRODUCT_NAME = "$(TARGET_NAME)"; 796 | SWIFT_VERSION = 5.0; 797 | TARGETED_DEVICE_FAMILY = "1,2"; 798 | }; 799 | name = Debug; 800 | }; 801 | FDD85ACD23B4BB6D00D7B8DD /* Release */ = { 802 | isa = XCBuildConfiguration; 803 | baseConfigurationReference = 827D6180C69E25BA54D3B059 /* Pods-iOS-MVVM-Template.release.xcconfig */; 804 | buildSettings = { 805 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 806 | CODE_SIGN_STYLE = Automatic; 807 | FRAMEWORK_SEARCH_PATHS = ( 808 | "$(inherited)", 809 | "$(PROJECT_DIR)/Carthage/Build/iOS", 810 | ); 811 | INFOPLIST_FILE = "iOS-MVVM-Template/Info.plist"; 812 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 813 | IS_STAGING = NO; 814 | LD_RUNPATH_SEARCH_PATHS = ( 815 | "$(inherited)", 816 | "@executable_path/Frameworks", 817 | ); 818 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-Template"; 819 | PRODUCT_NAME = "$(TARGET_NAME)"; 820 | SWIFT_VERSION = 5.0; 821 | TARGETED_DEVICE_FAMILY = "1,2"; 822 | }; 823 | name = Release; 824 | }; 825 | FDD85ACF23B4BB6D00D7B8DD /* Debug */ = { 826 | isa = XCBuildConfiguration; 827 | buildSettings = { 828 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 829 | BUNDLE_LOADER = "$(TEST_HOST)"; 830 | CLANG_ENABLE_MODULES = YES; 831 | CODE_SIGN_STYLE = Automatic; 832 | INFOPLIST_FILE = "iOS-MVVM-TemplateTests/Info.plist"; 833 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 834 | LD_RUNPATH_SEARCH_PATHS = ( 835 | "$(inherited)", 836 | "@executable_path/Frameworks", 837 | "@loader_path/Frameworks", 838 | ); 839 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-TemplateTests"; 840 | PRODUCT_NAME = "$(TARGET_NAME)"; 841 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 842 | SWIFT_VERSION = 5.0; 843 | TARGETED_DEVICE_FAMILY = "1,2"; 844 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-MVVM-Template.app/iOS-MVVM-Template"; 845 | }; 846 | name = Debug; 847 | }; 848 | FDD85AD023B4BB6D00D7B8DD /* Release */ = { 849 | isa = XCBuildConfiguration; 850 | buildSettings = { 851 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 852 | BUNDLE_LOADER = "$(TEST_HOST)"; 853 | CLANG_ENABLE_MODULES = YES; 854 | CODE_SIGN_STYLE = Automatic; 855 | INFOPLIST_FILE = "iOS-MVVM-TemplateTests/Info.plist"; 856 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 857 | LD_RUNPATH_SEARCH_PATHS = ( 858 | "$(inherited)", 859 | "@executable_path/Frameworks", 860 | "@loader_path/Frameworks", 861 | ); 862 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-TemplateTests"; 863 | PRODUCT_NAME = "$(TARGET_NAME)"; 864 | SWIFT_VERSION = 5.0; 865 | TARGETED_DEVICE_FAMILY = "1,2"; 866 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-MVVM-Template.app/iOS-MVVM-Template"; 867 | }; 868 | name = Release; 869 | }; 870 | FDD85B8F23B8E65A00D7B8DD /* Debug Staging */ = { 871 | isa = XCBuildConfiguration; 872 | buildSettings = { 873 | ALWAYS_SEARCH_USER_PATHS = NO; 874 | CLANG_ANALYZER_NONNULL = YES; 875 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 876 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 877 | CLANG_CXX_LIBRARY = "libc++"; 878 | CLANG_ENABLE_MODULES = YES; 879 | CLANG_ENABLE_OBJC_ARC = YES; 880 | CLANG_ENABLE_OBJC_WEAK = YES; 881 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 882 | CLANG_WARN_BOOL_CONVERSION = YES; 883 | CLANG_WARN_COMMA = YES; 884 | CLANG_WARN_CONSTANT_CONVERSION = YES; 885 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 886 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 887 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 888 | CLANG_WARN_EMPTY_BODY = YES; 889 | CLANG_WARN_ENUM_CONVERSION = YES; 890 | CLANG_WARN_INFINITE_RECURSION = YES; 891 | CLANG_WARN_INT_CONVERSION = YES; 892 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 893 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 894 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 895 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 896 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 897 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 898 | CLANG_WARN_STRICT_PROTOTYPES = YES; 899 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 900 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 901 | CLANG_WARN_UNREACHABLE_CODE = YES; 902 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 903 | COPY_PHASE_STRIP = NO; 904 | DEBUG_INFORMATION_FORMAT = dwarf; 905 | ENABLE_STRICT_OBJC_MSGSEND = YES; 906 | ENABLE_TESTABILITY = YES; 907 | GCC_C_LANGUAGE_STANDARD = gnu11; 908 | GCC_DYNAMIC_NO_PIC = NO; 909 | GCC_NO_COMMON_BLOCKS = YES; 910 | GCC_OPTIMIZATION_LEVEL = 0; 911 | GCC_PREPROCESSOR_DEFINITIONS = ( 912 | "DEBUG=1", 913 | "$(inherited)", 914 | ); 915 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 916 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 917 | GCC_WARN_UNDECLARED_SELECTOR = YES; 918 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 919 | GCC_WARN_UNUSED_FUNCTION = YES; 920 | GCC_WARN_UNUSED_VARIABLE = YES; 921 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 922 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 923 | MTL_FAST_MATH = YES; 924 | ONLY_ACTIVE_ARCH = YES; 925 | SDKROOT = iphoneos; 926 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 927 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 928 | }; 929 | name = "Debug Staging"; 930 | }; 931 | FDD85B9023B8E65A00D7B8DD /* Debug Staging */ = { 932 | isa = XCBuildConfiguration; 933 | baseConfigurationReference = 344DE535F1B529B9C384CB87 /* Pods-iOS-MVVM-Template.debug staging.xcconfig */; 934 | buildSettings = { 935 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 936 | CODE_SIGN_STYLE = Automatic; 937 | FRAMEWORK_SEARCH_PATHS = ( 938 | "$(inherited)", 939 | "$(PROJECT_DIR)/Carthage/Build/iOS", 940 | ); 941 | INFOPLIST_FILE = "iOS-MVVM-Template/Info.plist"; 942 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 943 | IS_STAGING = YES; 944 | LD_RUNPATH_SEARCH_PATHS = ( 945 | "$(inherited)", 946 | "@executable_path/Frameworks", 947 | ); 948 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-Template"; 949 | PRODUCT_NAME = "$(TARGET_NAME)"; 950 | SWIFT_VERSION = 5.0; 951 | TARGETED_DEVICE_FAMILY = "1,2"; 952 | }; 953 | name = "Debug Staging"; 954 | }; 955 | FDD85B9123B8E65A00D7B8DD /* Debug Staging */ = { 956 | isa = XCBuildConfiguration; 957 | buildSettings = { 958 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 959 | BUNDLE_LOADER = "$(TEST_HOST)"; 960 | CLANG_ENABLE_MODULES = YES; 961 | CODE_SIGN_STYLE = Automatic; 962 | INFOPLIST_FILE = "iOS-MVVM-TemplateTests/Info.plist"; 963 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 964 | LD_RUNPATH_SEARCH_PATHS = ( 965 | "$(inherited)", 966 | "@executable_path/Frameworks", 967 | "@loader_path/Frameworks", 968 | ); 969 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-TemplateTests"; 970 | PRODUCT_NAME = "$(TARGET_NAME)"; 971 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 972 | SWIFT_VERSION = 5.0; 973 | TARGETED_DEVICE_FAMILY = "1,2"; 974 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-MVVM-Template.app/iOS-MVVM-Template"; 975 | }; 976 | name = "Debug Staging"; 977 | }; 978 | FDD85B9223B8E66300D7B8DD /* Release Staging */ = { 979 | isa = XCBuildConfiguration; 980 | buildSettings = { 981 | ALWAYS_SEARCH_USER_PATHS = NO; 982 | CLANG_ANALYZER_NONNULL = YES; 983 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 984 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 985 | CLANG_CXX_LIBRARY = "libc++"; 986 | CLANG_ENABLE_MODULES = YES; 987 | CLANG_ENABLE_OBJC_ARC = YES; 988 | CLANG_ENABLE_OBJC_WEAK = YES; 989 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 990 | CLANG_WARN_BOOL_CONVERSION = YES; 991 | CLANG_WARN_COMMA = YES; 992 | CLANG_WARN_CONSTANT_CONVERSION = YES; 993 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 994 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 995 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 996 | CLANG_WARN_EMPTY_BODY = YES; 997 | CLANG_WARN_ENUM_CONVERSION = YES; 998 | CLANG_WARN_INFINITE_RECURSION = YES; 999 | CLANG_WARN_INT_CONVERSION = YES; 1000 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1001 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1002 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1003 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1004 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 1005 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1006 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1007 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1008 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1009 | CLANG_WARN_UNREACHABLE_CODE = YES; 1010 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1011 | COPY_PHASE_STRIP = NO; 1012 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1013 | ENABLE_NS_ASSERTIONS = NO; 1014 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1015 | GCC_C_LANGUAGE_STANDARD = gnu11; 1016 | GCC_NO_COMMON_BLOCKS = YES; 1017 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1018 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1019 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1020 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1021 | GCC_WARN_UNUSED_FUNCTION = YES; 1022 | GCC_WARN_UNUSED_VARIABLE = YES; 1023 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1024 | MTL_ENABLE_DEBUG_INFO = NO; 1025 | MTL_FAST_MATH = YES; 1026 | SDKROOT = iphoneos; 1027 | SWIFT_COMPILATION_MODE = wholemodule; 1028 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1029 | VALIDATE_PRODUCT = YES; 1030 | }; 1031 | name = "Release Staging"; 1032 | }; 1033 | FDD85B9323B8E66300D7B8DD /* Release Staging */ = { 1034 | isa = XCBuildConfiguration; 1035 | baseConfigurationReference = 826C91F05D2CAE3FF0BF34EE /* Pods-iOS-MVVM-Template.release staging.xcconfig */; 1036 | buildSettings = { 1037 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 1038 | CODE_SIGN_STYLE = Automatic; 1039 | FRAMEWORK_SEARCH_PATHS = ( 1040 | "$(inherited)", 1041 | "$(PROJECT_DIR)/Carthage/Build/iOS", 1042 | ); 1043 | INFOPLIST_FILE = "iOS-MVVM-Template/Info.plist"; 1044 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 1045 | IS_STAGING = YES; 1046 | LD_RUNPATH_SEARCH_PATHS = ( 1047 | "$(inherited)", 1048 | "@executable_path/Frameworks", 1049 | ); 1050 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-Template"; 1051 | PRODUCT_NAME = "$(TARGET_NAME)"; 1052 | SWIFT_VERSION = 5.0; 1053 | TARGETED_DEVICE_FAMILY = "1,2"; 1054 | }; 1055 | name = "Release Staging"; 1056 | }; 1057 | FDD85B9423B8E66300D7B8DD /* Release Staging */ = { 1058 | isa = XCBuildConfiguration; 1059 | buildSettings = { 1060 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1061 | BUNDLE_LOADER = "$(TEST_HOST)"; 1062 | CLANG_ENABLE_MODULES = YES; 1063 | CODE_SIGN_STYLE = Automatic; 1064 | INFOPLIST_FILE = "iOS-MVVM-TemplateTests/Info.plist"; 1065 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1066 | LD_RUNPATH_SEARCH_PATHS = ( 1067 | "$(inherited)", 1068 | "@executable_path/Frameworks", 1069 | "@loader_path/Frameworks", 1070 | ); 1071 | PRODUCT_BUNDLE_IDENTIFIER = "com.lebron.iOS-MVVM-TemplateTests"; 1072 | PRODUCT_NAME = "$(TARGET_NAME)"; 1073 | SWIFT_VERSION = 5.0; 1074 | TARGETED_DEVICE_FAMILY = "1,2"; 1075 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-MVVM-Template.app/iOS-MVVM-Template"; 1076 | }; 1077 | name = "Release Staging"; 1078 | }; 1079 | /* End XCBuildConfiguration section */ 1080 | 1081 | /* Begin XCConfigurationList section */ 1082 | FDD85AA723B4BB6B00D7B8DD /* Build configuration list for PBXProject "iOS-MVVM-Template" */ = { 1083 | isa = XCConfigurationList; 1084 | buildConfigurations = ( 1085 | FDD85AC923B4BB6D00D7B8DD /* Debug */, 1086 | FDD85B8F23B8E65A00D7B8DD /* Debug Staging */, 1087 | FDD85ACA23B4BB6D00D7B8DD /* Release */, 1088 | FDD85B9223B8E66300D7B8DD /* Release Staging */, 1089 | ); 1090 | defaultConfigurationIsVisible = 0; 1091 | defaultConfigurationName = Release; 1092 | }; 1093 | FDD85ACB23B4BB6D00D7B8DD /* Build configuration list for PBXNativeTarget "iOS-MVVM-Template" */ = { 1094 | isa = XCConfigurationList; 1095 | buildConfigurations = ( 1096 | FDD85ACC23B4BB6D00D7B8DD /* Debug */, 1097 | FDD85B9023B8E65A00D7B8DD /* Debug Staging */, 1098 | FDD85ACD23B4BB6D00D7B8DD /* Release */, 1099 | FDD85B9323B8E66300D7B8DD /* Release Staging */, 1100 | ); 1101 | defaultConfigurationIsVisible = 0; 1102 | defaultConfigurationName = Release; 1103 | }; 1104 | FDD85ACE23B4BB6D00D7B8DD /* Build configuration list for PBXNativeTarget "iOS-MVVM-TemplateTests" */ = { 1105 | isa = XCConfigurationList; 1106 | buildConfigurations = ( 1107 | FDD85ACF23B4BB6D00D7B8DD /* Debug */, 1108 | FDD85B9123B8E65A00D7B8DD /* Debug Staging */, 1109 | FDD85AD023B4BB6D00D7B8DD /* Release */, 1110 | FDD85B9423B8E66300D7B8DD /* Release Staging */, 1111 | ); 1112 | defaultConfigurationIsVisible = 0; 1113 | defaultConfigurationName = Release; 1114 | }; 1115 | /* End XCConfigurationList section */ 1116 | }; 1117 | rootObject = FDD85AA423B4BB6B00D7B8DD /* Project object */; 1118 | } 1119 | --------------------------------------------------------------------------------