├── init ├── Gemfile ├── swift-ui-baseUITests ├── Resources │ ├── LogOutSuccessfully.json │ ├── AuthenticationError.json │ ├── GetProfileSuccessfully.json │ ├── LoginSuccessfully.json │ └── SignUpSuccessfully.json ├── XCTestCaseExtension.swift ├── XCUIElementExtension.swift ├── Info.plist ├── NetworkMockerExtension.swift ├── NetworkMocker.swift ├── XCUIApplicationExtension.swift └── swift_ui_baseUITests.swift ├── swift-ui-base ├── Assets.xcassets │ ├── Contents.json │ ├── Palette │ │ ├── Contents.json │ │ ├── Rootstrap │ │ │ ├── Contents.json │ │ │ ├── rsBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsGreen.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsPink.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsRed.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsWhite.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsYellow.colorset │ │ │ │ └── Contents.json │ │ │ ├── rsDarkGray.colorset │ │ │ │ └── Contents.json │ │ │ └── rsLightGray.colorset │ │ │ │ └── Contents.json │ │ ├── darkGray.colorset │ │ │ └── Contents.json │ │ ├── altoGray.colorset │ │ │ └── Contents.json │ │ ├── athensGray.colorset │ │ │ └── Contents.json │ │ ├── cadetBlue.colorset │ │ │ └── Contents.json │ │ └── errorRed.colorset │ │ │ └── Contents.json │ ├── User │ │ ├── Contents.json │ │ ├── edit_icon.imageset │ │ │ ├── pencilEditButton.png │ │ │ ├── pencilEditButton@2x.png │ │ │ ├── pencilEditButton@3x.png │ │ │ └── Contents.json │ │ └── user_avatar_placeholder.imageset │ │ │ ├── avatarUser.png │ │ │ ├── avatarUser@2x.png │ │ │ ├── avatarUser@3x.png │ │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Networking │ ├── Model │ │ ├── HTTPHeader.swift │ │ ├── Base64Media.swift │ │ ├── MultipartMedia.swift │ │ └── Session.swift │ └── Service │ │ ├── UserServices.swift │ │ ├── AuthenticationServices.swift │ │ └── APIClient.swift ├── Extensions │ ├── ColorExtension.swift │ ├── DictionaryExtension.swift │ ├── ImageExtension.swift │ └── StringExtension.swift ├── Navigation │ ├── Model │ │ └── ViewRouter.swift │ └── View │ │ └── RootView.swift ├── Common │ ├── Model │ │ ├── App.swift │ │ └── User.swift │ ├── Modifier │ │ ├── TitleModifier.swift │ │ └── RoundedButtonModifier.swift │ └── View │ │ ├── ActivityIndicator │ │ └── ActivityIndicatorView.swift │ │ └── TextField │ │ ├── Model │ │ └── TextFieldData.swift │ │ └── View │ │ └── TextFieldView.swift ├── SceneDelegate.swift ├── Managers │ ├── UserDataManager.swift │ ├── ConfigurationManager.swift │ └── SessionManager.swift ├── Home │ ├── View │ │ ├── AvatarView.swift │ │ ├── HomeView.swift │ │ └── ProfileView.swift │ └── ViewModel │ │ └── ProfileViewModel.swift ├── ImagePicker │ └── View │ │ └── ImagePicker.swift ├── Authentication │ ├── ViewModel │ │ ├── LoginViewModel.swift │ │ └── SignUpViewModel.swift │ └── View │ │ ├── LoginView.swift │ │ └── SignUpView.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── AppDelegate.swift └── Info.plist ├── .slather.yml ├── CODEOWNERS ├── swift-ui-base.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── swift-ui-base.xcscheme └── project.pbxproj ├── swift-ui-base.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Podfile ├── ThirdPartyKeys.example.plist ├── Podfile.lock ├── swift-ui-baseTests ├── Info.plist ├── Extensions │ └── StringExtensionUnitTests.swift └── Services │ └── UserServiceUnitTests.swift ├── .codeclimate.yml ├── .travis.yml ├── LICENSE ├── LICENSE.md ├── .gitignore ├── .swiftlint.yml ├── init.swift └── README.md /init: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/init -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'slather' 4 | 5 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/Resources/LogOutSuccessfully.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": 1 3 | } 4 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/Resources/AuthenticationError.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "Authentication error" 3 | } 4 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /swift-ui-base/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton.png -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton@2x.png -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/edit_icon.imageset/pencilEditButton@3x.png -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: cobertura_xml 2 | workspace: swift-ui-base.xcworkspace 3 | xcodeproj: swift-ui-base.xcodeproj 4 | scheme: swift-ui-base 5 | binary_basename: swift-ui-base 6 | ignore: 7 | - Pods/* 8 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser.png -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence 3 | 4 | * @pMalvasio @glm4 @CamilaMoscatelli @mato2593 @germanStabile @fernandatoledo @ximenaperez 5 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser@2x.png -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/swift-ui-base/HEAD/swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/avatarUser@3x.png -------------------------------------------------------------------------------- /swift-ui-base.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/Resources/GetProfileSuccessfully.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": 1, 4 | "email": "automation@test.com", 5 | "first_name": "FirstName", 6 | "last_name": "LastName", 7 | "username": "test" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /swift-ui-base.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /swift-ui-base.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /swift-ui-base.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/Resources/LoginSuccessfully.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": 1, 4 | "email": "automation@test.com", 5 | "provider": "email", 6 | "uid": "automation@test.com", 7 | "first_name": "FirstName", 8 | "last_name": "LastName", 9 | "username": "test", 10 | "created_at": "2017-02-23T13:54:33.283Z", 11 | "updated_at": "2017-02-23T13:54:33.425Z" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/Resources/SignUpSuccessfully.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": 1, 4 | "email": "automation@test.com", 5 | "provider": "email", 6 | "uid": "automation@test.com", 7 | "first_name": "FirstName", 8 | "last_name": "LastName", 9 | "username": "test", 10 | "created_at": "2017-02-23T13:54:33.283Z", 11 | "updated_at": "2017-02-23T13:54:33.425Z" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | target 'swift-ui-base' do 5 | use_frameworks! 6 | 7 | pod 'Alamofire', '~> 5.0' 8 | 9 | target 'swift-ui-baseTests' do 10 | inherit! :complete 11 | end 12 | 13 | target 'swift-ui-baseUITests' do 14 | inherit! :complete 15 | pod 'Swifter', '~> 1.4.7' 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /ThirdPartyKeys.example.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ServiceKey 6 | 7 | Release 8 | 9 | Staging 10 | 11 | Debug 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.2.1) 3 | - Swifter (1.4.7) 4 | 5 | DEPENDENCIES: 6 | - Alamofire (~> 5.0) 7 | - Swifter (~> 1.4.7) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - Alamofire 12 | - Swifter 13 | 14 | SPEC CHECKSUMS: 15 | Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd 16 | Swifter: 2327ef5d872c638aebab79646ce494af508b0c8f 17 | 18 | PODFILE CHECKSUM: 6f4c7e077f8655ff1363aa04d271fec4008c04a2 19 | 20 | COCOAPODS: 1.8.4 21 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Model/HTTPHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeader.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum HTTPHeader: String { 12 | case uid = "uid" 13 | case client = "client" 14 | case token = "access-token" 15 | case expiry = "expiry" 16 | case accept = "Accept" 17 | case contentType = "Content-Type" 18 | } 19 | -------------------------------------------------------------------------------- /swift-ui-base/Extensions/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtension.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/11/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Color { 12 | 13 | static let darkGray = Color("darkGray") 14 | static let athensGray = Color("athensGray") 15 | static let altoGray = Color("altoGray") 16 | static let cadetBlue = Color("cadetBlue") 17 | static let errorRed = Color("errorRed") 18 | } 19 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Model/Base64Media.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64Media.swift 3 | // ios-base 4 | // 5 | // Created by German on 7/7/17. 6 | // Copyright © 2017 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | class Base64Media: MultipartMedia { 13 | var base64: String 14 | 15 | override init(key: String, data: Data, type: MimeType = .jpeg) { 16 | self.base64 = data.asBase64Param(withType: type) 17 | super.init(key: key, data: data, type: type) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /swift-ui-base/Navigation/Model/ViewRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRouter.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/13/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | enum Roots { 13 | case home 14 | case profile 15 | } 16 | 17 | class ViewRouter: ObservableObject { 18 | @Published var currentRoot: Roots = SessionManager.isValidSession ? .profile : .home 19 | 20 | static let shared = ViewRouter() 21 | 22 | fileprivate init() { } 23 | } 24 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/user_avatar_placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "avatarUser.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "avatarUser@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "avatarUser@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/User/edit_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pencilEditButton.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "pencilEditButton@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "pencilEditButton@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/XCTestCaseExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestCaseExtension.swift 3 | // swift-ui-baseUITests 4 | // 5 | // Created by Germán Stábile on 2/13/20. 6 | // Copyright © 2020 TopTier labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | extension XCTestCase { 12 | func waitFor(element: XCUIElement, timeOut: TimeInterval = 2) { 13 | let exists = NSPredicate(format: "exists == 1") 14 | 15 | expectation(for: exists, evaluatedWith: element, handler: nil) 16 | waitForExpectations(timeout: timeOut, handler: nil) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/XCUIElementExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCUIElementExtension.swift 3 | // swift-ui-baseUITests 4 | // 5 | // Created by Germán Stábile on 2/13/20. 6 | // Copyright © 2020 TopTier labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | extension XCUIElement { 12 | 13 | func clearText(text: String? = nil) { 14 | guard let stringValue = value as? String ?? text else { 15 | return 16 | } 17 | 18 | tap() 19 | let deleteString = 20 | stringValue.map { _ in XCUIKeyboardKey.delete.rawValue }.joined() 21 | typeText(deleteString) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /swift-ui-base/Navigation/View/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/13/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct RootView: View { 12 | 13 | @EnvironmentObject var router: ViewRouter 14 | 15 | var body: some View { 16 | VStack { 17 | containedView() 18 | .id(router.currentRoot) 19 | .transition(.slide).animation(.linear(duration: 0.2)) 20 | } 21 | } 22 | 23 | func containedView() -> AnyView { 24 | switch router.currentRoot { 25 | case .profile: 26 | return AnyView(ProfileView()) 27 | default: 28 | return AnyView(HomeView()) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /swift-ui-base/Common/Model/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct App { 12 | static let domain = Bundle.main.bundleIdentifier ?? "" 13 | 14 | static func error( 15 | domain: ErrorDomain = .generic, 16 | code: Int? = nil, 17 | localizedDescription: String = "" 18 | ) -> NSError { 19 | return NSError(domain: App.domain + "." + domain.rawValue, 20 | code: code ?? 0, 21 | userInfo: [NSLocalizedDescriptionKey: localizedDescription]) 22 | } 23 | } 24 | 25 | enum ErrorDomain: String { 26 | case generic = "GenericError" 27 | case parsing = "ParsingError" 28 | } 29 | -------------------------------------------------------------------------------- /swift-ui-base/Common/Modifier/TitleModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleModifier.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/11/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MainTitle: ViewModifier { 12 | func body(content: Content) -> some View { 13 | content 14 | .padding() 15 | .foregroundColor(.darkGray) 16 | .font(.largeTitle) 17 | } 18 | } 19 | 20 | struct MainTitle_Previews: PreviewProvider { 21 | static var previews: some View { 22 | Group { 23 | Text("Hello, World!") 24 | .previewLayout(.sizeThatFits) 25 | 26 | Text("Hello, World!") 27 | .modifier(MainTitle()) 28 | .previewLayout(.sizeThatFits) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /swift-ui-baseTests/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 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/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 | -------------------------------------------------------------------------------- /swift-ui-base/Common/View/ActivityIndicator/ActivityIndicatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicatorView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/13/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ActivityIndicatorView: UIViewRepresentable { 12 | 13 | @Binding var isAnimating: Bool 14 | let style: UIActivityIndicatorView.Style 15 | 16 | func makeUIView( 17 | context: UIViewRepresentableContext 18 | ) -> UIActivityIndicatorView { 19 | return UIActivityIndicatorView(style: style) 20 | } 21 | 22 | func updateUIView( 23 | _ uiView: UIActivityIndicatorView, 24 | context: UIViewRepresentableContext 25 | ) { 26 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /swift-ui-base/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, 17 | willConnectTo session: UISceneSession, 18 | options connectionOptions: UIScene.ConnectionOptions) { 19 | 20 | if let windowScene = scene as? UIWindowScene { 21 | let window = UIWindow(windowScene: windowScene) 22 | window.rootViewController = UIHostingController( 23 | rootView: RootView().environmentObject(ViewRouter.shared) 24 | ) 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" # required to adjust maintainability checks 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 8 6 | complex-logic: 7 | config: 8 | threshold: 5 9 | file-lines: 10 | config: 11 | threshold: 500 12 | method-complexity: 13 | config: 14 | threshold: 5 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 25 21 | nested-control-flow: 22 | config: 23 | threshold: 3 24 | return-statements: 25 | enabled: false 26 | similar-code: 27 | config: 28 | threshold: # language-specific defaults. an override will affect all languages. 29 | identical-code: 30 | config: 31 | threshold: # language-specific defaults. an override will affect all languages. 32 | plugins: 33 | swiftlint: 34 | enabled: true 35 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/darkGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "79", 9 | "green" : "61", 10 | "red" : "54" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.310", 27 | "green" : "0.239", 28 | "red" : "0.212" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/altoGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "216", 9 | "green" : "216", 10 | "red" : "216" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.847", 27 | "green" : "0.847", 28 | "red" : "0.847" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/athensGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "237", 9 | "green" : "221", 10 | "red" : "229" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.929", 27 | "green" : "0.867", 28 | "red" : "0.898" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/cadetBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "199", 9 | "green" : "175", 10 | "red" : "167" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.780", 27 | "green" : "0.686", 28 | "red" : "0.655" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "247", 9 | "green" : "54", 10 | "red" : "0" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.969", 27 | "green" : "0.212", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "133", 9 | "green" : "187", 10 | "red" : "44" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.522", 27 | "green" : "0.733", 28 | "red" : "0.173" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsPink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "142", 9 | "green" : "145", 10 | "red" : "255" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.557", 27 | "green" : "0.569", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "49", 9 | "green" : "64", 10 | "red" : "207" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.192", 27 | "green" : "0.251", 28 | "red" : "0.812" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsWhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "245", 9 | "green" : "242", 10 | "red" : "241" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.961", 27 | "green" : "0.949", 28 | "red" : "0.945" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "57", 9 | "green" : "230", 10 | "red" : "234" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.224", 27 | "green" : "0.902", 28 | "red" : "0.918" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/errorRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.318", 9 | "green" : "0.318", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.318", 27 | "green" : "0.318", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsDarkGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "131", 9 | "green" : "129", 10 | "red" : "128" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.514", 27 | "green" : "0.506", 28 | "red" : "0.502" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Assets.xcassets/Palette/Rootstrap/rsLightGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "184", 9 | "green" : "181", 10 | "red" : "181" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.722", 27 | "green" : "0.710", 28 | "red" : "0.710" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /swift-ui-base/Extensions/DictionaryExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryExtension.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //+ Operator definition for Dictionary types 12 | func + (left: [K: V], right: [K: V]) -> [K: V] { 13 | var merge = left 14 | for (key, value) in right { 15 | merge[key] = value 16 | } 17 | return merge 18 | } 19 | 20 | // swiftlint:disable shorthand_operator 21 | func += (left: inout [K: V], right: [K: V]) { 22 | left = left + right 23 | } 24 | // swiftlint:enable shorthand_operator 25 | 26 | extension Dictionary where Key: ExpressibleByStringLiteral { 27 | mutating func lowercaseKeys() { 28 | for key in self.keys { 29 | if let loweredKey = String(describing: key).lowercased() as? Key { 30 | self[loweredKey] = self.removeValue(forKey: key) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /swift-ui-base/Extensions/ImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageExtension.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/3/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIImage { 13 | class func random(size: CGSize = CGSize(width: 100, height: 100)) -> UIImage { 14 | let red = CGFloat(arc4random_uniform(255)) 15 | let green = CGFloat(arc4random_uniform(255)) 16 | let blue = CGFloat(arc4random_uniform(255)) 17 | let color = UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0) 18 | UIGraphicsBeginImageContext(size) 19 | let context = UIGraphicsGetCurrentContext() 20 | context?.setFillColor(color.cgColor) 21 | context?.addRect(CGRect(origin: .zero, size: size)) 22 | context?.fillPath() 23 | let image = UIGraphicsGetImageFromCurrentImageContext() 24 | UIGraphicsEndImageContext() 25 | return image ?? UIImage() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /swift-ui-base/Common/Modifier/RoundedButtonModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedButtonModifier.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 6/9/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MainRoundedButton: ViewModifier { 12 | 13 | var backgroundColor: Color = .blue 14 | var radius: CGFloat = 8 15 | 16 | func body(content: Content) -> some View { 17 | content 18 | .frame(width: 300, height: 50) 19 | .font(.headline) 20 | .foregroundColor(.white) 21 | .background(backgroundColor) 22 | .cornerRadius(radius) 23 | } 24 | } 25 | 26 | struct MainRoundedButton_Previews: PreviewProvider { 27 | static var previews: some View { 28 | Group { 29 | Text("Hello, World!") 30 | .padding() 31 | .previewLayout(.sizeThatFits) 32 | 33 | Text("Hello, World!") 34 | .modifier(MainRoundedButton()) 35 | .padding() 36 | .previewLayout(.sizeThatFits) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Service/UserServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserServices.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 6/9/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UserServices { 12 | class func getMyProfile( 13 | success: @escaping (_ user: User) -> Void, 14 | failure: @escaping (_ error: Error) -> Void 15 | ) { 16 | APIClient.request( 17 | .get, 18 | url: "/user/profile", 19 | success: { response, _ in 20 | guard 21 | let userDictionary = response["user"] as? [String: Any], 22 | let user = User(dictionary: userDictionary) 23 | else { 24 | failure(App.error( 25 | domain: .parsing, 26 | localizedDescription: "Could not parse a valid user".localized 27 | )) 28 | return 29 | } 30 | 31 | UserDataManager.currentUser = user 32 | success(user) 33 | }, 34 | failure: failure 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /swift-ui-base/Managers/UserDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDataManager.swift 3 | // ios-base 4 | // 5 | // Created by Rootstrap on 15/2/16. 6 | // Copyright © 2016 Rootstrap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserDataManager: NSObject { 12 | 13 | static private let userDefaultUserKey = "swift-ui-base-user" 14 | 15 | static var currentUser: User? { 16 | get { 17 | let defaults = UserDefaults.standard 18 | if 19 | let data = defaults.data(forKey: userDefaultUserKey), 20 | let user = try? JSONDecoder().decode(User.self, from: data) 21 | { 22 | return user 23 | } 24 | return nil 25 | } 26 | 27 | set { 28 | let user = try? JSONEncoder().encode(newValue) 29 | UserDefaults.standard.set(user, forKey: userDefaultUserKey) 30 | } 31 | } 32 | 33 | class func deleteUser() { 34 | UserDefaults.standard.removeObject(forKey: userDefaultUserKey) 35 | } 36 | 37 | static var isUserLogged: Bool { 38 | return currentUser != nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CC_TEST_REPORTER_ID: c160f6f1729bb88e0b34b95f02292fcc9622317cd77b840252462a996a5eb677 4 | - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi) 5 | language: swift 6 | xcode_workspace: swift-ui-base.xcworkspace 7 | xcode_scheme: swift-ui-base 8 | osx_image: xcode11.4 9 | cache: 10 | - bundler 11 | - cocoapods 12 | before_install: 13 | - pod repo update 14 | before_script: 15 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64 > ./cc-test-reporter 16 | - chmod +x ./cc-test-reporter 17 | - ./cc-test-reporter before-build 18 | install: 19 | - bundle install 20 | - pod install 21 | script: 22 | - set -o pipefail 23 | - xcodebuild -workspace swift-ui-base.xcworkspace -scheme swift-ui-base -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.3' build test | xcpretty --test --color 24 | after_script: 25 | - slather coverage 26 | - ./cc-test-reporter after-build -t cobertura --exit-code $TRAVIS_TEST_RESULT 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rootstrap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rootstrap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /swift-ui-base/Common/View/TextField/Model/TextFieldData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldData.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/11/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class TextFieldData: ObservableObject { 12 | var value: String = "" { 13 | didSet { 14 | validate() 15 | } 16 | } 17 | 18 | var validationType: ValidationType 19 | var errorMessage: String 20 | var title: String 21 | var isSecure = false 22 | 23 | var isValid = true 24 | 25 | var isEmpty: Bool { 26 | return value.isEmpty 27 | } 28 | 29 | init(title: String, 30 | value: String = "", 31 | validationType: ValidationType = .none, 32 | isSecure: Bool = false, 33 | errorMessage: String = "") { 34 | self.title = title 35 | self.value = value 36 | self.validationType = validationType 37 | self.errorMessage = errorMessage 38 | self.isSecure = isSecure 39 | 40 | validate() 41 | } 42 | 43 | func validate() { 44 | isValid = value.isValid(type: validationType) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /swift-ui-base/Managers/ConfigurationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationManager.swift 3 | // swift-ui-base 4 | // 5 | // Created by Pablo Malvasio on 7/30/20. 6 | // Copyright © 2017 Rootstrap Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ConfigurationManager: NSObject { 12 | 13 | class func getValue( 14 | for key: String, 15 | on propertyList: String = "ThirdPartyKeys" 16 | ) -> String? { 17 | if 18 | let path = Bundle.main.path(forResource: propertyList, ofType: "plist"), 19 | let dict = NSDictionary(contentsOfFile: path) as? [String: AnyObject] 20 | { 21 | if let configDict = dict[key] as? [String: AnyObject] { 22 | let key = Bundle.main.object( 23 | forInfoDictionaryKey: "ConfigurationName" 24 | ) as? String ?? "" 25 | return configDict[key] as? String 26 | } else if let value = dict[key] as? String { 27 | return value 28 | } 29 | } 30 | 31 | print(""" 32 | \(propertyList).plist NOT FOUND - 33 | Please check your project configuration in: \n https://github.com/rootstrap/swift-ui-base 34 | """) 35 | return nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /swift-ui-base/Home/View/AvatarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvatarView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 6/9/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AvatarView: View { 12 | 13 | @Binding var image: Image? 14 | @Binding var isShowingImagePicker: Bool 15 | 16 | var body: some View { 17 | ZStack(alignment: .bottomTrailing) { 18 | 19 | avatarImage 20 | .resizable() 21 | .frame(width: 100, height: 100) 22 | .clipShape(Circle()) 23 | 24 | Button(action: { self.editAvatarButtonTapped() }) { 25 | Image("edit_icon") 26 | .resizable() 27 | .aspectRatio(contentMode: .fit) 28 | .frame(width: 20, height: 20) 29 | .foregroundColor(.darkGray) 30 | 31 | } 32 | .padding(EdgeInsets( 33 | top: 0, 34 | leading: 0, 35 | bottom: 10, 36 | trailing: 0 37 | )) 38 | } 39 | } 40 | 41 | var avatarImage: Image { 42 | image ?? Image("user_avatar_placeholder") 43 | } 44 | 45 | func editAvatarButtonTapped() { 46 | isShowingImagePicker = true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ## User settings 3 | xcuserdata/ 4 | 5 | ## Obj-C/Swift specific 6 | *.hmap 7 | 8 | ## App packaging 9 | *.ipa 10 | *.dSYM.zip 11 | *.dSYM 12 | 13 | ## Playgrounds 14 | timeline.xctimeline 15 | playground.xcworkspace 16 | 17 | # Swift Package Manager 18 | # 19 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 20 | # Packages/ 21 | # Package.pins 22 | # Package.resolved 23 | # *.xcodeproj 24 | # 25 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 26 | # hence it is not needed unless you have added a package configuration file to your project 27 | # .swiftpm 28 | 29 | .build/ 30 | 31 | 32 | Pods/ 33 | # 34 | # Add this line if you want to avoid checking in source code from the Xcode workspace 35 | # *.xcworkspace 36 | 37 | 38 | fastlane/report.xml 39 | fastlane/Preview.html 40 | fastlane/screenshots/**/*.png 41 | fastlane/test_output 42 | 43 | # Code Injection 44 | # 45 | # After new code Injection tools there's a generated folder /iOSInjectionProject 46 | # https://github.com/johnno1962/injectionforxcode 47 | 48 | iOSInjectionProject/ 49 | 50 | **/.DS_Store 51 | 52 | # Private keys 53 | **/ThirdPartyKeys.plist -------------------------------------------------------------------------------- /swift-ui-base/Common/Model/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // ios-base 4 | // 5 | // Created by Rootstrap on 1/18/17. 6 | // Copyright © 2017 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct User: Codable { 12 | var id: Int 13 | var username: String 14 | var email: String 15 | var imageURL: URL? 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case id 19 | case username 20 | case email 21 | case imageURL = "profile_picture" 22 | } 23 | 24 | init?(dictionary: [String: Any]) { 25 | guard 26 | let id = dictionary[CodingKeys.id.rawValue] as? Int, 27 | let username = dictionary[CodingKeys.username.rawValue] as? String, 28 | let email = dictionary[CodingKeys.email.rawValue] as? String 29 | else { 30 | return nil 31 | } 32 | 33 | self.id = id 34 | self.username = username 35 | self.email = email 36 | self.imageURL = URL( 37 | string: dictionary[CodingKeys.imageURL.rawValue] as? String ?? "" 38 | ) 39 | } 40 | 41 | init(id: Int, username: String, email: String, imageURL: URL? = nil) { 42 | self.id = id 43 | self.username = username 44 | self.email = email 45 | self.imageURL = imageURL 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /swift-ui-base/Managers/SessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDataManager.swift 3 | // swift-ui-base 4 | // 5 | // Created by Juan Pablo Mazza on 11/8/16. 6 | // Copyright © 2016 Rootstrap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SessionManager: NSObject { 12 | 13 | static private let userDefaultSessionKey = "swift-ui-base-session" 14 | 15 | static var currentSession: Session? { 16 | get { 17 | if 18 | let data = UserDefaults.standard.data(forKey: userDefaultSessionKey), 19 | let session = try? JSONDecoder().decode(Session.self, from: data) 20 | { 21 | return session 22 | } 23 | return nil 24 | } 25 | 26 | set { 27 | let session = try? JSONEncoder().encode(newValue) 28 | UserDefaults.standard.set(session, forKey: userDefaultSessionKey) 29 | } 30 | } 31 | 32 | class func deleteSession() { 33 | UserDefaults.standard.removeObject(forKey: userDefaultSessionKey) 34 | } 35 | 36 | static var isValidSession: Bool { 37 | if 38 | let session = currentSession, 39 | let uid = session.uid, 40 | let token = session.accessToken, 41 | let client = session.client 42 | { 43 | return !uid.isEmpty && !token.isEmpty && !client.isEmpty 44 | } 45 | 46 | return false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - todo 3 | - trailing_whitespace 4 | - type_name # deactivated because swift3 need lowercase enums 5 | opt_in_rules: # some rules are only opt-in 6 | - array_init 7 | - empty_count 8 | - contains_over_first_not_nil 9 | - explicit_init 10 | - fatal_error_message 11 | - first_where 12 | - force_unwrapping 13 | - implicit_return 14 | - joined_default_parameter 15 | - literal_expression_end_indentation 16 | - operator_usage_whitespace 17 | - overridden_super_call 18 | - yoda_condition 19 | # Find all the available rules by running: 20 | # swiftlint rules 21 | whitelist_rules: 22 | excluded: # paths to ignore during linting. Takes precedence over `included`. 23 | - Carthage 24 | - Pods 25 | - init.swift 26 | force_cast: error 27 | force_try: 28 | severity: warning # explicitly 29 | line_length: 30 | warning: 90 31 | error: 100 32 | type_body_length: 33 | warning: 300 34 | error: 400 35 | file_length: 36 | warning: 500 37 | error: 850 38 | function_parameter_count: 39 | warning: 6 40 | error: 8 41 | cyclomatic_complexity: 42 | ignores_case_statements: true 43 | warning: 5 44 | error: 7 45 | function_body_length: 46 | warning: 25 47 | error: 32 48 | identifier_name: 49 | excluded: ["id"] 50 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle) 51 | -------------------------------------------------------------------------------- /swift-ui-base/Home/ViewModel/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/30/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class ProfileViewModel: ObservableObject, Identifiable { 13 | 14 | @Published var image: UIImage? 15 | @Published var isLoading = false 16 | @Published var shouldShowAlert = false 17 | @Published var errorDescription = "" 18 | 19 | var username: String { 20 | return UserDataManager.currentUser?.email ?? "" 21 | } 22 | 23 | func logout() { 24 | AuthenticationServices.logout() 25 | } 26 | 27 | func deleteAccount() { 28 | AuthenticationServices.deleteAccount() 29 | } 30 | 31 | func getMyProfile() { 32 | isLoading = true 33 | UserServices.getMyProfile( 34 | success: { [weak self] _ in 35 | self?.errorDescription = "" 36 | self?.shouldShowAlert = true 37 | self?.isLoading = false 38 | }, 39 | failure: { [weak self] error in 40 | self?.isLoading = false 41 | self?.shouldShowAlert = true 42 | self?.errorDescription = error.localizedDescription 43 | }) 44 | } 45 | 46 | func saveAvatar() { 47 | //TODO 48 | //guard let image = image else { return } 49 | isLoading = true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Model/MultipartMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipartMedia.swift 3 | // ios-base 4 | // 5 | // Created by German on 7/7/17. 6 | // Copyright © 2017 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | //Basic media MIME types, add more if needed. 13 | enum MimeType: String { 14 | case jpeg = "image/jpeg" 15 | case bmp = "image/bmp" 16 | case png = "image/png" 17 | 18 | case mov = "video/quicktime" 19 | case mpeg = "video/mpeg" 20 | case avi = "video/avi" 21 | case json = "application/json" 22 | 23 | var fileExtension: String { 24 | switch self { 25 | case .bmp: return ".bmp" 26 | case .png: return ".png" 27 | case .mov: return ".mov" 28 | case .mpeg: return ".mpeg" 29 | case .avi: return ".avi" 30 | case .json: return ".json" 31 | default: return ".jpg" 32 | } 33 | } 34 | } 35 | 36 | class MultipartMedia { 37 | var key: String 38 | var data: Data 39 | var type: MimeType 40 | var toFile: String { 41 | return key.validFilename + type.fileExtension 42 | } 43 | 44 | init(key: String, data: Data, type: MimeType = .jpeg) { 45 | self.key = key 46 | self.data = data 47 | self.type = type 48 | } 49 | 50 | func embed(inForm multipart: MultipartFormData) { 51 | multipart.append(data, withName: key, fileName: toFile, mimeType: type.rawValue) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Model/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Session: Codable { 12 | var uid: String? 13 | var client: String? 14 | var accessToken: String? 15 | var expiry: Date? 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case uid 19 | case client 20 | case accessToken = "access-token" 21 | case expiry 22 | } 23 | 24 | init( 25 | uid: String? = nil, client: String? = nil, 26 | token: String? = nil, expires: Date? = nil 27 | ) { 28 | self.uid = uid 29 | self.client = client 30 | self.accessToken = token 31 | self.expiry = expires 32 | } 33 | 34 | init?(headers: [String: Any]) { 35 | var loweredHeaders = headers 36 | loweredHeaders.lowercaseKeys() 37 | guard let stringHeaders = loweredHeaders as? [String: String] else { 38 | return nil 39 | } 40 | if let expiryString = stringHeaders[HTTPHeader.expiry.rawValue], 41 | let expiryNumber = Double(expiryString) { 42 | expiry = Date(timeIntervalSince1970: expiryNumber) 43 | } 44 | uid = stringHeaders[HTTPHeader.uid.rawValue] 45 | client = stringHeaders[HTTPHeader.client.rawValue] 46 | accessToken = stringHeaders[HTTPHeader.token.rawValue] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /swift-ui-base/Home/View/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct HomeView: View { 12 | 13 | var body: some View { 14 | NavigationView { 15 | VStack { 16 | Spacer() 17 | 18 | Text("Welcome to RS SwiftUI base!") 19 | .modifier(MainTitle()) 20 | 21 | Spacer() 22 | 23 | NavigationLink(destination: LoginView()) { 24 | Text("Log In") 25 | .frame(width: 300, height: 50) 26 | .font(.subheadline) 27 | .background(Color.blue) 28 | .foregroundColor(.white) 29 | .cornerRadius(8) 30 | .padding(.bottom, 20) 31 | } 32 | .accessibility(identifier: "GoToLoginLink") 33 | 34 | NavigationLink(destination: SignUpView()) { 35 | Text("Don't have an account? Lets create one! ►") 36 | .frame(width: 300, height: 50) 37 | .font(.subheadline) 38 | .foregroundColor(.gray) 39 | .cornerRadius(8) 40 | } 41 | .accessibility(identifier: "GoToSignUpLink") 42 | 43 | Spacer() 44 | } 45 | } 46 | } 47 | } 48 | 49 | struct HomeView_Previews: PreviewProvider { 50 | static var previews: some View { 51 | HomeView() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/NetworkMockerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkMockerExtension.swift 3 | // swift-ui-baseUITests 4 | // 5 | // Created by Germán Stábile on 6/30/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NetworkMocker { 12 | 13 | func stubSignUp(shouldSucceed: Bool = true) { 14 | let responseFileName = shouldSucceed ? "SignUpSuccessfully" : "AuthenticationError" 15 | 16 | setupStub( 17 | url: "/users/", 18 | responseFilename: responseFileName, 19 | method: .POST 20 | ) 21 | } 22 | 23 | func stubLogOut() { 24 | setupStub( 25 | url: "/users/sign_out", 26 | responseFilename: "LogOutSuccessfully", 27 | method: .DELETE 28 | ) 29 | } 30 | 31 | func stubDeleteAccount() { 32 | setupStub( 33 | url: "/user/delete_account", 34 | responseFilename: "LogOutSuccessfully", 35 | method: .DELETE 36 | ) 37 | } 38 | 39 | func stubLogIn(shouldSucceed: Bool = true) { 40 | let responseFilename = shouldSucceed ? "LoginSuccessfully" : "AuthenticationError" 41 | 42 | setupStub( 43 | url: "/users/sign_in", 44 | responseFilename: responseFilename, 45 | method: .POST 46 | ) 47 | } 48 | 49 | func stubGetProfile() { 50 | setupStub( 51 | url: "/user/profile", 52 | responseFilename: "GetProfileSuccessfully", 53 | method: .GET 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /swift-ui-baseTests/Extensions/StringExtensionUnitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensionUnitTests.swift 3 | // ios-baseUnitTests 4 | // 5 | // Created by German on 4/30/20. 6 | // Copyright © 2020 TopTier labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import swift_ui_base 11 | 12 | class StringExtensionUnitTests: XCTestCase { 13 | func testEmailValidation() { 14 | XCTAssertFalse("username".isEmailFormatted()) 15 | XCTAssertFalse("username@test".isEmailFormatted()) 16 | XCTAssert("username@test.com".isEmailFormatted()) 17 | XCTAssert("username.alias+2@gmail.com".isEmailFormatted()) 18 | } 19 | 20 | func testPhoneNumberValidation() { 21 | XCTAssertFalse("dlfkhgakdfs".isPhoneNumber()) 22 | XCTAssertFalse("1241353".isPhoneNumber()) 23 | XCTAssert("123-123-4314".isPhoneNumber()) 24 | XCTAssert("000-000-0000".isPhoneNumber()) 25 | } 26 | 27 | func testAlphanumeric() { 28 | XCTAssertFalse("123598 sdg asd".isAlphanumericWithNoSpaces) 29 | XCTAssert("1231234314".isAlphanumericWithNoSpaces) 30 | XCTAssert("asdgasdg".isAlphanumericWithNoSpaces) 31 | XCTAssert("asdgasd28352".isAlphanumericWithNoSpaces) 32 | } 33 | 34 | func testHasNumbers() { 35 | XCTAssertFalse("asdgasdfgkjasf ".hasNumbers) 36 | XCTAssertFalse("$#^&@".hasNumbers) 37 | XCTAssert("asd235".hasNumbers) 38 | XCTAssert("124".hasNumbers) 39 | } 40 | 41 | func testHasPunctuation() { 42 | XCTAssertFalse("asgfasfdhg123".hasPunctuationCharacters) 43 | XCTAssert("asdg,asd !".hasPunctuationCharacters) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /swift-ui-base/ImagePicker/View/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 5/19/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ImagePicker: UIViewControllerRepresentable { 12 | 13 | @Environment(\.presentationMode) var presentationMode 14 | @Binding var image: UIImage? 15 | 16 | func makeCoordinator() -> Coordinator { 17 | Coordinator(self) 18 | } 19 | 20 | func makeUIViewController( 21 | context: UIViewControllerRepresentableContext 22 | ) -> UIImagePickerController { 23 | let picker = UIImagePickerController() 24 | picker.delegate = context.coordinator 25 | return picker 26 | } 27 | 28 | func updateUIViewController( 29 | _ uiViewController: UIImagePickerController, 30 | context: UIViewControllerRepresentableContext 31 | ) { 32 | 33 | } 34 | 35 | class Coordinator: NSObject, 36 | UINavigationControllerDelegate, UIImagePickerControllerDelegate { 37 | 38 | let parent: ImagePicker 39 | 40 | init(_ parent: ImagePicker) { 41 | self.parent = parent 42 | } 43 | 44 | func imagePickerController( 45 | _ picker: UIImagePickerController, 46 | didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] 47 | ) { 48 | if let uiImage = info[.originalImage] as? UIImage { 49 | parent.image = uiImage 50 | } 51 | 52 | parent.presentationMode.wrappedValue.dismiss() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /swift-ui-base/Authentication/ViewModel/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/12/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | enum LoginViewModelState { 14 | case loading 15 | case idle 16 | case signedIn 17 | case error(String) 18 | } 19 | 20 | class LoginViewModel: ObservableObject, Identifiable { 21 | 22 | @Published var emailData = TextFieldData( 23 | title: "Email", 24 | validationType: .email, 25 | errorMessage: "Please enter a valid email" 26 | ) 27 | 28 | @Published var passwordData = TextFieldData( 29 | title: "Password", 30 | validationType: .nonEmpty, 31 | isSecure: true, 32 | errorMessage: "Please enter a valid password" 33 | ) 34 | 35 | @Published var isLoading = false 36 | @Published var errored = false 37 | var error: String = "" 38 | 39 | var isValidData: Bool { 40 | return [emailData, passwordData].allSatisfy { $0.isValid } 41 | } 42 | 43 | func attemptSignin() { 44 | isLoading = true 45 | 46 | AuthenticationServices.login( 47 | emailData.value, 48 | password: passwordData.value, 49 | success: { [weak self] in 50 | self?.isLoading = false 51 | ViewRouter.shared.currentRoot = .profile 52 | }, 53 | failure: { [weak self] error in 54 | self?.isLoading = false 55 | self?.errored = true 56 | self?.error = error.localizedDescription 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /swift-ui-base/Authentication/View/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct LoginView: View { 12 | 13 | @ObservedObject var viewModel = LoginViewModel() 14 | 15 | var body: some View { 16 | ZStack { 17 | ActivityIndicatorView(isAnimating: $viewModel.isLoading, style: .medium) 18 | 19 | VStack { 20 | Text("Sign In") 21 | .modifier(MainTitle()) 22 | 23 | Spacer() 24 | 25 | VStack(spacing: 20) { 26 | TextFieldView(fieldData: $viewModel.emailData) 27 | TextFieldView(fieldData: $viewModel.passwordData) 28 | } 29 | 30 | Spacer() 31 | 32 | Button(action: loginButtonTapped) { 33 | Text("Sign In") 34 | .font(.headline) 35 | } 36 | .accessibility(identifier: "SignInButton") 37 | .disabled(!viewModel.isValidData) 38 | 39 | Spacer() 40 | } 41 | .disabled(viewModel.isLoading) 42 | .blur(radius: viewModel.isLoading ? 3 : 0) 43 | .alert(isPresented: $viewModel.errored) { 44 | Alert(title: Text("Oops"), 45 | message: Text(viewModel.error), 46 | dismissButton: .default(Text("Got it!"))) 47 | } 48 | } 49 | } 50 | 51 | func loginButtonTapped() { 52 | viewModel.attemptSignin() 53 | } 54 | } 55 | 56 | struct LoginView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | LoginView() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /swift-ui-base/Authentication/View/SignUpView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SignUpView: View { 12 | @ObservedObject var viewModel = SignUpViewModel() 13 | 14 | var body: some View { 15 | ZStack { 16 | ActivityIndicatorView(isAnimating: $viewModel.isLoading, style: .medium) 17 | 18 | VStack { 19 | Text("Sign Up") 20 | .modifier(MainTitle()) 21 | 22 | Spacer() 23 | 24 | VStack(spacing: 20) { 25 | TextFieldView(fieldData: $viewModel.emailData) 26 | TextFieldView(fieldData: $viewModel.passwordData) 27 | TextFieldView(fieldData: $viewModel.confirmPasswordData) 28 | } 29 | Spacer() 30 | 31 | Button(action: signUpButtonTapped, label: { 32 | Text("Sign Up") 33 | .font(.headline) 34 | }) 35 | .accessibility(identifier: "SignUpButton") 36 | .disabled(!viewModel.isValidData) 37 | 38 | Spacer() 39 | } 40 | .disabled(viewModel.isLoading) 41 | .blur(radius: viewModel.isLoading ? 3 : 0) 42 | .alert(isPresented: $viewModel.errored) { 43 | Alert(title: Text("Oops"), 44 | message: Text(viewModel.error), 45 | dismissButton: .default(Text("Got it!"))) 46 | } 47 | } 48 | } 49 | 50 | func signUpButtonTapped() { 51 | viewModel.attemptSingUp() 52 | } 53 | } 54 | 55 | struct SignUpView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | SignUpView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/NetworkMocker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkMocker.swift 3 | // swift-ui-baseUITests 4 | // 5 | // Created by Germán Stábile on 6/30/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swifter 11 | 12 | enum HTTPMethod { 13 | case POST 14 | case GET 15 | case PUT 16 | case DELETE 17 | } 18 | 19 | class NetworkMocker { 20 | 21 | var server = HttpServer() 22 | 23 | func setUp() { 24 | try! server.start() 25 | } 26 | 27 | func tearDown() { 28 | server.stop() 29 | } 30 | 31 | public func setupStub( 32 | url: String, 33 | responseFilename: String, 34 | method: HTTPMethod = .GET 35 | ) { 36 | let testBundle = Bundle(for: type(of: self)) 37 | let filePath = testBundle.path(forResource: responseFilename, ofType: "json") ?? "" 38 | let fileUrl = URL(fileURLWithPath: filePath) 39 | guard let data = try? Data(contentsOf: fileUrl, options: .uncached) else { 40 | fatalError("Could not parse mocked data") 41 | return 42 | } 43 | let json = dataToJSON(data: data) 44 | 45 | let response: ((HttpRequest) -> HttpResponse) = { _ in 46 | return HttpResponse.ok(.json(json as AnyObject)) 47 | } 48 | 49 | switch method { 50 | case .GET: server.GET[url] = response 51 | case .POST: server.POST[url] = response 52 | case .DELETE: server.DELETE[url] = response 53 | case .PUT: server.PUT[url] = response 54 | } 55 | } 56 | 57 | func dataToJSON(data: Data) -> Any? { 58 | do { 59 | return try JSONSerialization.jsonObject(with: data, options: .mutableContainers) 60 | } catch let error { 61 | print(error) 62 | } 63 | return nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /swift-ui-base/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 | -------------------------------------------------------------------------------- /swift-ui-base/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | static let shared: AppDelegate = { 15 | guard let appD = UIApplication.shared.delegate as? AppDelegate else { 16 | return AppDelegate() 17 | } 18 | return appD 19 | }() 20 | 21 | func application( 22 | _ application: UIApplication, 23 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 24 | ) -> Bool { 25 | // Override point for customization after application launch. 26 | return true 27 | } 28 | 29 | // MARK: UISceneSession Lifecycle 30 | 31 | func application( 32 | _ application: UIApplication, 33 | configurationForConnecting connectingSceneSession: UISceneSession, 34 | options: UIScene.ConnectionOptions 35 | ) -> UISceneConfiguration { 36 | // Called when a new scene session is being created. 37 | // Use this method to select a configuration to create the new scene with. 38 | return UISceneConfiguration( 39 | name: "Default Configuration", 40 | sessionRole: connectingSceneSession.role 41 | ) 42 | } 43 | 44 | func unexpectedLogout() { 45 | //TODO 46 | // UserDataManager.deleteUser() 47 | // SessionManager.deleteSession() 48 | // //Clear any local data if needed 49 | // //Take user to onboarding if needed, do NOT redirect the user 50 | // // if is already in the landing to avoid losing the current VC stack state. 51 | // if window?.rootViewController is HomeViewController { 52 | // AppNavigator.shared.navigate(to: OnboardingRoutes.firstScreen, with: .changeRoot) 53 | // } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /swift-ui-base/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 | } -------------------------------------------------------------------------------- /swift-ui-base/Authentication/ViewModel/SignUpViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/3/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class SignUpViewModel: ObservableObject, Identifiable { 13 | @Published var emailData = TextFieldData( 14 | title: "Email", 15 | validationType: .email, 16 | errorMessage: "Please enter a valid email" 17 | ) 18 | 19 | @Published var passwordData = TextFieldData( 20 | title: "Password", 21 | validationType: .nonEmpty, 22 | isSecure: true, 23 | errorMessage: "Passwords don't match" 24 | ) 25 | 26 | @Published var confirmPasswordData = TextFieldData( 27 | title: "Confirm Password", 28 | validationType: .nonEmpty, 29 | isSecure: true, 30 | errorMessage: "Passwords don't match" 31 | ) 32 | 33 | @Published var isLoading = false 34 | @Published var errored = false 35 | var error: String = "" 36 | 37 | init() { 38 | confirmPasswordData.validationType = .custom(isValid: passwordsMatch) 39 | passwordData.validationType = .custom(isValid: passwordsMatch) 40 | } 41 | 42 | var isValidData: Bool { 43 | return [emailData, passwordData, confirmPasswordData].allSatisfy { $0.isValid } 44 | } 45 | 46 | func passwordsMatch() -> Bool { 47 | guard !passwordData.isEmpty else { return false } 48 | let areValidPasswords = passwordData.value == confirmPasswordData.value 49 | passwordData.isValid = areValidPasswords 50 | confirmPasswordData.isValid = areValidPasswords 51 | return areValidPasswords 52 | } 53 | 54 | func attemptSingUp() { 55 | isLoading = true 56 | AuthenticationServices.signup( 57 | emailData.value, 58 | password: passwordData.value, 59 | avatar64: UIImage.random(), 60 | success: { [weak self] _ in 61 | self?.isLoading = false 62 | ViewRouter.shared.currentRoot = .profile 63 | }, 64 | failure: { [weak self] error in 65 | self?.isLoading = false 66 | self?.errored = true 67 | self?.error = error.localizedDescription 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /swift-ui-base/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Base URL 6 | $(BASE_URL) 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 | ConfigurationName 24 | Debug 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsLocalNetworking 30 | 31 | 32 | UIApplicationSceneManifest 33 | 34 | UIApplicationSupportsMultipleScenes 35 | 36 | UISceneConfigurations 37 | 38 | UIWindowSceneSessionRoleApplication 39 | 40 | 41 | UISceneConfigurationName 42 | Default Configuration 43 | UISceneDelegateClassName 44 | $(PRODUCT_MODULE_NAME).SceneDelegate 45 | 46 | 47 | 48 | 49 | UILaunchStoryboardName 50 | LaunchScreen 51 | UIRequiredDeviceCapabilities 52 | 53 | armv7 54 | 55 | UISupportedInterfaceOrientations 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UISupportedInterfaceOrientations~ipad 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationPortraitUpsideDown 65 | UIInterfaceOrientationLandscapeLeft 66 | UIInterfaceOrientationLandscapeRight 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/XCUIApplicationExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCUIApplicationExtension.swift 3 | // swift-ui-baseUITests 4 | // 5 | // Created by Germán Stábile on 2/13/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | extension XCUIApplication { 12 | func type(text: String, on fieldName: String, isSecure: Bool = false) { 13 | let fields = isSecure ? secureTextFields : textFields 14 | let field = fields[fieldName] 15 | field.tap() 16 | field.typeText(text) 17 | } 18 | 19 | func dismissKeyboard() { 20 | //ugly hack to dismiss keyboard 21 | let field = textFields.firstMatch 22 | field.tap() 23 | field.typeText("\n") 24 | } 25 | 26 | func clearText(on fieldName: String) { 27 | let field = textFields[fieldName] 28 | field.tap() 29 | field.clearText() 30 | } 31 | 32 | func deleteAccountIfNeeded(in testCase: XCTestCase) { 33 | let deleteAccountButton = buttons["DeleteAccountButton"] 34 | let goToLoginButton = buttons["GoToLoginLink"] 35 | 36 | if deleteAccountButton.exists { 37 | deleteAccountButton.tap() 38 | testCase.waitFor(element: goToLoginButton, timeOut: 5) 39 | } 40 | } 41 | 42 | func attemptSignIn( 43 | in testCase: XCTestCase, 44 | with email: String, 45 | password: String 46 | ) { 47 | let goToSignInButton = buttons["GoToLoginLink"] 48 | 49 | goToSignInButton.tap() 50 | 51 | let signInButton = buttons["SignInButton"] 52 | 53 | testCase.waitFor(element: signInButton, timeOut: 2) 54 | 55 | type(text: email, on: "EmailTextField") 56 | type(text: password, on: "PasswordTextField", isSecure: true) 57 | 58 | dismissKeyboard() 59 | 60 | signInButton.tap() 61 | } 62 | 63 | func attemptSignUp( 64 | in testCase: XCTestCase, 65 | with email: String, 66 | password: String 67 | ) { 68 | buttons["GoToSignUpLink"].tap() 69 | 70 | type(text: email, on: "EmailTextField") 71 | 72 | type( 73 | text: password, 74 | on: "PasswordTextField", 75 | isSecure: true 76 | ) 77 | 78 | dismissKeyboard() 79 | 80 | type( 81 | text: password, 82 | on: "ConfirmPasswordTextField", 83 | isSecure: true 84 | ) 85 | 86 | dismissKeyboard() 87 | 88 | buttons["SignUpButton"].tap() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /swift-ui-base/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtension.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum ValidationType { 12 | case email 13 | case nonEmpty 14 | case numeric 15 | case date 16 | case phone 17 | case none 18 | case custom(isValid: () -> Bool) 19 | } 20 | 21 | extension String { 22 | var isAlphanumericWithNoSpaces: Bool { 23 | rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil 24 | } 25 | 26 | var hasPunctuationCharacters: Bool { 27 | rangeOfCharacter(from: CharacterSet.punctuationCharacters) != nil 28 | } 29 | 30 | var hasNumbers: Bool { 31 | rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789")) != nil 32 | } 33 | 34 | var localized: String { 35 | localize() 36 | } 37 | 38 | var withNoSpaces: String { 39 | filter { !$0.isWhitespace } 40 | } 41 | 42 | func localize(comment: String = "") -> String { 43 | NSLocalizedString(self, comment: comment) 44 | } 45 | 46 | var validFilename: String { 47 | guard !isEmpty else { return "emptyFilename" } 48 | return addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? "emptyFilename" 49 | } 50 | 51 | func isValid(type: ValidationType, isRequired: Bool = true) -> Bool { 52 | guard isRequired || !isEmpty else { return true } 53 | switch type { 54 | case .email: 55 | return isEmailFormatted() 56 | case .numeric: 57 | return isInteger() 58 | case .phone: 59 | return isPhoneNumber() 60 | case .none: 61 | return true 62 | case .custom(isValid: let validationBlock): 63 | return validationBlock() 64 | default: 65 | return !isEmpty 66 | } 67 | } 68 | 69 | //Regex fulfill RFC 5322 Internet Message format 70 | func isEmailFormatted() -> Bool { 71 | let predicate = NSPredicate( 72 | format: "SELF MATCHES %@", 73 | "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\\.)+[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?" 74 | ) 75 | return predicate.evaluate(with: self) 76 | } 77 | 78 | func isInteger() -> Bool { 79 | Int(self) != nil 80 | } 81 | 82 | func isPhoneNumber() -> Bool { 83 | let phoneTest = NSPredicate(format: "SELF MATCHES %@", "^\\d{3}-\\d{3}-\\d{4}$") 84 | return phoneTest.evaluate(with: self) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /swift-ui-base/Common/View/TextField/View/TextFieldView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/10/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TextFieldView: View { 12 | 13 | @Binding var fieldData: TextFieldData 14 | 15 | var body: some View { 16 | VStack { 17 | ZStack { 18 | Text(fieldData.title) 19 | .frame(maxWidth: .infinity, alignment: .leading) 20 | .font(Font.headline.weight(.regular)) 21 | .foregroundColor(.cadetBlue) 22 | .offset(CGSize(width: 0, height: fieldData.isEmpty ? 0 : -30)) 23 | .opacity(fieldData.isEmpty ? 0.7 : 1) 24 | .animation(.easeOut(duration: 0.2)) 25 | .scaleEffect(fieldData.isEmpty ? 1 : 0.8, anchor: .bottomLeading) 26 | 27 | if fieldData.isSecure { 28 | SecureField("", text: $fieldData.value) 29 | .foregroundColor(.darkGray) 30 | .autocapitalization(.none) 31 | .accessibility(identifier: "\(fieldData.title.withNoSpaces)TextField") 32 | } else { 33 | TextField("", text: $fieldData.value) 34 | .foregroundColor(.darkGray) 35 | .autocapitalization(.none) 36 | .opacity(0.8) 37 | .accessibility(identifier: "\(fieldData.title.withNoSpaces)TextField") 38 | } 39 | } 40 | 41 | Rectangle() 42 | .frame(maxWidth: .infinity, maxHeight: 1) 43 | .foregroundColor( 44 | !fieldData.isEmpty && !fieldData.isValid ? .errorRed : .cadetBlue 45 | ) 46 | .opacity(0.5) 47 | 48 | Text(fieldData.errorMessage) 49 | .frame(maxWidth: .infinity, alignment: .leading) 50 | .offset(CGSize(width: 0, height: -5)) 51 | .font(.footnote) 52 | .foregroundColor(.errorRed) 53 | .opacity(!fieldData.isEmpty && !fieldData.isValid ? 1 : 0) 54 | .animation(.easeOut(duration: 0.2)) 55 | } 56 | .padding(.horizontal, 20) 57 | } 58 | } 59 | 60 | struct TextFieldView_Previews: PreviewProvider { 61 | 62 | struct BindingTestHolder: View { 63 | @State var fieldData = TextFieldData( 64 | title: "Email", 65 | value: "testy@testerson", 66 | validationType: .email, 67 | errorMessage: "Please enter a valid email" 68 | ) 69 | 70 | var body: some View { 71 | TextFieldView(fieldData: $fieldData) 72 | } 73 | } 74 | 75 | static var previews: some View { 76 | BindingTestHolder() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /swift-ui-baseTests/Services/UserServiceUnitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserServiceUnitTests.swift 3 | // ios-baseUnitTests 4 | // 5 | // Created by German on 4/30/20. 6 | // Copyright © 2020 TopTier labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import swift_ui_base 11 | 12 | class UserServiceUnitTests: XCTestCase { 13 | 14 | let userResponse: [String: Any] = [ 15 | "user": [ 16 | "id": 0, 17 | "username": "test", 18 | "email": "test-user@rootstrap.com" 19 | ] 20 | ] 21 | 22 | var testUser: User! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | testUser = User( 27 | id: 0, 28 | username: "test", 29 | email: "test-user@rootstrap.com" 30 | ) 31 | SessionManager.deleteSession() 32 | UserDataManager.deleteUser() 33 | } 34 | 35 | func testUserPersistence() { 36 | AuthenticationServices.saveUserSession(fromResponse: userResponse, headers: [:]) 37 | guard let persistedUser = UserDataManager.currentUser else { 38 | XCTFail("User should NOT be nil") 39 | return 40 | } 41 | XCTAssert(UserDataManager.isUserLogged) 42 | XCTAssert(persistedUser.id == testUser.id) 43 | XCTAssert(persistedUser.username == testUser.username) 44 | XCTAssert(persistedUser.email == testUser.email) 45 | } 46 | 47 | func testGoodSessionPersistence() { 48 | let client = "dummySessionClient" 49 | let token = "dummySessionToken" 50 | let uid = testUser.email 51 | let expiry = "\(Date.timeIntervalSinceReferenceDate)" 52 | let sessionHeaders: [String: Any] = [ 53 | APIClient.HTTPHeader.uid.rawValue: uid, 54 | APIClient.HTTPHeader.client.rawValue: client, 55 | APIClient.HTTPHeader.token.rawValue: token, 56 | APIClient.HTTPHeader.expiry.rawValue: expiry 57 | ] 58 | 59 | AuthenticationServices.saveUserSession( 60 | fromResponse: userResponse, 61 | headers: sessionHeaders 62 | ) 63 | guard let persistedSession = SessionManager.currentSession else { 64 | XCTFail("Session should NOT be nil") 65 | return 66 | } 67 | 68 | XCTAssert(persistedSession.client == client) 69 | XCTAssert(persistedSession.accessToken == token) 70 | XCTAssert(persistedSession.uid == uid) 71 | XCTAssert(persistedSession.accessToken == token) 72 | } 73 | 74 | func testBadSessionPersistence() { 75 | // Testing case where shouldn't be session at all 76 | let unusableHeaders = [APIClient.HTTPHeader.client: "badHeaderKey"] 77 | AuthenticationServices.saveUserSession( 78 | fromResponse: userResponse, 79 | headers: unusableHeaders 80 | ) 81 | XCTAssert(SessionManager.currentSession == nil) 82 | XCTAssertFalse(SessionManager.isValidSession) 83 | 84 | // Testing case where should be session but not valid 85 | let wrongSessionHeaders = [ 86 | "testKey": "testValue", 87 | APIClient.HTTPHeader.uid.rawValue: "", 88 | APIClient.HTTPHeader.client.rawValue: "", 89 | APIClient.HTTPHeader.token.rawValue: "" 90 | ] 91 | AuthenticationServices.saveUserSession( 92 | fromResponse: userResponse, 93 | headers: wrongSessionHeaders 94 | ) 95 | XCTAssert(SessionManager.currentSession != nil) 96 | XCTAssertFalse(SessionManager.isValidSession) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /swift-ui-base/Home/View/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 3/24/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ProfileView: View { 12 | 13 | @State var isShowingImagePicker = false 14 | @State private var image: Image? 15 | 16 | @ObservedObject var viewModel = ProfileViewModel() 17 | 18 | var body: some View { 19 | ZStack { 20 | ActivityIndicatorView(isAnimating: $viewModel.isLoading, style: .medium) 21 | 22 | VStack { 23 | 24 | AvatarView( 25 | image: $image, 26 | isShowingImagePicker: $isShowingImagePicker 27 | ).padding(.top, 70) 28 | 29 | Text("Welcome \(viewModel.username)") 30 | .modifier(MainTitle()) 31 | 32 | Spacer() 33 | 34 | buttons 35 | .padding(.bottom, 60) 36 | 37 | .sheet(isPresented: $isShowingImagePicker, onDismiss: loadImage) { 38 | ImagePicker(image: self.$viewModel.image) 39 | } 40 | 41 | .alert(isPresented: $viewModel.shouldShowAlert) { 42 | if !viewModel.errorDescription.isEmpty { 43 | return Alert(title: Text("Oops"), message: Text(viewModel.errorDescription)) 44 | } else { 45 | return Alert(title: Text("Profile loaded"), message: Text(viewModel.username)) 46 | } 47 | } 48 | } 49 | .disabled(viewModel.isLoading) 50 | .blur(radius: viewModel.isLoading ? 3 : 0) 51 | } 52 | } 53 | 54 | var saveAvatarButtonColor: Color { 55 | let opacity = $image.wrappedValue == nil ? 0.5 : 1 56 | return Color.green.opacity(opacity) 57 | } 58 | 59 | var buttons: some View { 60 | VStack(spacing: 10) { 61 | Button(action: saveAvatar) { 62 | Text("Save Avatar") 63 | .modifier(MainRoundedButton(backgroundColor: saveAvatarButtonColor)) 64 | } 65 | .disabled($image.wrappedValue == nil) 66 | .accessibility(identifier: "SaveAvatarButton") 67 | 68 | Button(action: getMyProfile) { 69 | Text("Get my profile") 70 | .modifier(MainRoundedButton(backgroundColor: Color.blue)) 71 | } 72 | .accessibility(identifier: "GetMyProfileButton") 73 | 74 | Button(action: logoutButtonTapped) { 75 | Text("Log out") 76 | .modifier(MainRoundedButton(backgroundColor: Color.red)) 77 | } 78 | .accessibility(identifier: "LogOutButton") 79 | 80 | Button(action: deleteAccountButtonTapped) { 81 | Text("Delete Account") 82 | .modifier(MainRoundedButton(backgroundColor: Color.purple)) 83 | } 84 | .accessibility(identifier: "DeleteAccountButton") 85 | } 86 | } 87 | 88 | func logoutButtonTapped() { 89 | viewModel.logout() 90 | ViewRouter.shared.currentRoot = .home 91 | } 92 | 93 | func deleteAccountButtonTapped() { 94 | viewModel.deleteAccount() 95 | ViewRouter.shared.currentRoot = .home 96 | } 97 | 98 | func saveAvatar() { 99 | viewModel.saveAvatar() 100 | } 101 | 102 | func getMyProfile() { 103 | viewModel.getMyProfile() 104 | } 105 | 106 | func loadImage() { 107 | guard let inputImage = viewModel.image else { return } 108 | image = Image(uiImage: inputImage) 109 | } 110 | } 111 | 112 | struct ProfileView_Previews: PreviewProvider { 113 | static var previews: some View { 114 | ProfileView() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /swift-ui-baseUITests/swift_ui_baseUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // swift-ui-baseUITests 3 | // 4 | // Created by Germán Stábile on 2/13/20. 5 | // Copyright © 2020 TopTier labs. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | @testable import swift_ui_base 10 | 11 | class ios_baseUITests: XCTestCase { 12 | 13 | var app: XCUIApplication! 14 | let networkMocker = NetworkMocker() 15 | 16 | override func setUp() { 17 | super.setUp() 18 | app = XCUIApplication() 19 | app.launchArguments = ["Automation Test"] 20 | 21 | networkMocker.setUp() 22 | } 23 | 24 | override func tearDown() { 25 | super.tearDown() 26 | networkMocker.tearDown() 27 | } 28 | 29 | func testCreateAccountValidations() { 30 | app.launch() 31 | 32 | app.buttons["GoToSignUpLink"].tap() 33 | 34 | let signUpButton = app.buttons["SignUpButton"] 35 | waitFor(element: signUpButton, timeOut: 2) 36 | 37 | XCTAssertFalse(signUpButton.isEnabled) 38 | 39 | app.type(text: "automation@test", on: "EmailTextField") 40 | 41 | app.dismissKeyboard() 42 | 43 | app.type( 44 | text: "holahola", 45 | on: "PasswordTextField", 46 | isSecure: true 47 | ) 48 | 49 | app.dismissKeyboard() 50 | XCTAssertFalse(signUpButton.isEnabled) 51 | 52 | app.type( 53 | text: "holahola", 54 | on: "ConfirmPasswordTextField", 55 | isSecure: true 56 | ) 57 | XCTAssertFalse(signUpButton.isEnabled) 58 | 59 | app.type(text: ".com", on: "EmailTextField") 60 | XCTAssert(signUpButton.isEnabled) 61 | 62 | app.dismissKeyboard() 63 | 64 | app.type( 65 | text: "holahol", 66 | on: "ConfirmPasswordTextField", 67 | isSecure: true 68 | ) 69 | XCTAssertFalse(signUpButton.isEnabled) 70 | } 71 | 72 | func testAccountCreation() { 73 | app.launch() 74 | 75 | networkMocker.stubSignUp() 76 | 77 | app.attemptSignUp( 78 | in: self, 79 | with: "automation@test.com", 80 | password: "holahola" 81 | ) 82 | 83 | let logOutButton = app.buttons["LogOutButton"] 84 | 85 | waitFor(element: logOutButton, timeOut: 15) 86 | 87 | networkMocker.stubLogOut() 88 | 89 | logOutButton.tap() 90 | 91 | networkMocker.stubLogIn() 92 | 93 | app.attemptSignIn( 94 | in: self, 95 | with: "automation@test.com", 96 | password: "holahola" 97 | ) 98 | 99 | networkMocker.stubGetProfile() 100 | 101 | let getMyProfile = app.buttons["GetMyProfileButton"] 102 | waitFor(element: getMyProfile, timeOut: 10) 103 | getMyProfile.tap() 104 | 105 | sleep(10) 106 | if let alert = app.alerts.allElementsBoundByIndex.first { 107 | waitFor(element: alert, timeOut: 10) 108 | 109 | alert.buttons.allElementsBoundByIndex.first?.tap() 110 | 111 | networkMocker.stubDeleteAccount() 112 | app.deleteAccountIfNeeded(in: self) 113 | } 114 | } 115 | 116 | func testSignInFailure() { 117 | app.launch() 118 | 119 | networkMocker.stubLogIn(shouldSucceed: false) 120 | app.attemptSignIn( 121 | in: self, 122 | with: "automation@test.com", 123 | password: "incorrect password" 124 | ) 125 | 126 | if let alert = app.alerts.allElementsBoundByIndex.first { 127 | waitFor(element: alert, timeOut: 2) 128 | 129 | alert.buttons.allElementsBoundByIndex.first?.tap() 130 | } 131 | 132 | let signInButton = app.buttons["SignInButton"] 133 | waitFor(element: signInButton, timeOut: 2) 134 | } 135 | 136 | func testSignInValidations() { 137 | app.launch() 138 | 139 | app.buttons["GoToLoginLink"].tap() 140 | 141 | let signInButton = app.buttons["SignInButton"] 142 | 143 | waitFor(element: signInButton, timeOut: 2) 144 | 145 | XCTAssertFalse(signInButton.isEnabled) 146 | 147 | app.type(text: "automation@test", on: "EmailTextField") 148 | app.type( 149 | text: "holahola", 150 | on: "PasswordTextField", 151 | isSecure: true 152 | ) 153 | 154 | XCTAssertFalse(signInButton.isEnabled) 155 | 156 | app.type(text: ".com", on: "EmailTextField") 157 | 158 | XCTAssert(signInButton.isEnabled) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Service/AuthenticationServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseService.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class AuthenticationServices { 13 | 14 | fileprivate static let usersUrl = "/users/" 15 | fileprivate static let currentUserUrl = "/user/" 16 | 17 | class func login(_ email: String, 18 | password: String, 19 | success: @escaping () -> Void, 20 | failure: @escaping (_ error: Error) -> Void) { 21 | let url = usersUrl + "sign_in" 22 | let parameters = [ 23 | "user": [ 24 | "email": email, 25 | "password": password 26 | ] 27 | ] 28 | APIClient.request(.post, url: url, params: parameters, success: { response, headers in 29 | AuthenticationServices.saveUserSession(fromResponse: response, headers: headers) 30 | success() 31 | }, failure: { error in 32 | failure(error) 33 | }) 34 | } 35 | 36 | //Multi part upload example 37 | //TODO: rails base backend not supporting multipart uploads yet 38 | class func signup(_ email: String, 39 | password: String, 40 | avatar: UIImage, 41 | success: @escaping (_ user: User?) -> Void, 42 | failure: @escaping (_ error: Error) -> Void) { 43 | let parameters = [ 44 | "user": [ 45 | "email": email, 46 | "password": password, 47 | "password_confirmation": password 48 | ] 49 | ] 50 | 51 | guard let picData = avatar.jpegData(compressionQuality: 0.75) else { 52 | failure(App.error( 53 | domain: .parsing, 54 | code: 1000, 55 | localizedDescription: "Could not parse image" 56 | )) 57 | return 58 | } 59 | let image = MultipartMedia(key: "user[avatar]", data: picData) 60 | //Mixed base64 encoded and multipart images are supported in [MultipartMedia] param: 61 | //Example: let image2 = Base64Media(key: "user[image]", data: picData) Then: media [image, image2] 62 | APIClient.multipartRequest( 63 | url: usersUrl, 64 | params: parameters, 65 | paramsRootKey: "", 66 | media: [image], 67 | success: { response, headers in 68 | AuthenticationServices.saveUserSession(fromResponse: response, headers: headers) 69 | success(UserDataManager.currentUser) 70 | }, 71 | failure: failure 72 | ) 73 | } 74 | 75 | //Example method that uploads base64 encoded image. 76 | class func signup(_ email: String, 77 | password: String, 78 | avatar64: UIImage, 79 | success: @escaping (_ user: User?) -> Void, 80 | failure: @escaping (_ error: Error) -> Void) { 81 | var userParameters: [String: Any] = [ 82 | "email": email, 83 | "password": password, 84 | "password_confirmation": password 85 | ] 86 | 87 | if let picData = avatar64.jpegData(compressionQuality: 0.75) { 88 | userParameters["image"] = picData.asBase64Param() 89 | } 90 | 91 | let parameters = [ 92 | "user": userParameters 93 | ] 94 | 95 | APIClient.request( 96 | .post, 97 | url: usersUrl, 98 | params: parameters, 99 | success: { response, headers in 100 | AuthenticationServices.saveUserSession(fromResponse: response, headers: headers) 101 | success(UserDataManager.currentUser) 102 | }, 103 | failure: failure 104 | ) 105 | } 106 | 107 | class func logout( 108 | success: @escaping () -> Void = {}, 109 | failure: @escaping (_ error: Error) -> Void = { _ in } 110 | ) { 111 | let url = "\(usersUrl)sign_out" 112 | APIClient.request( 113 | .delete, 114 | url: url, 115 | success: { _, _ in 116 | deleteSession() 117 | success() 118 | }, 119 | failure: failure 120 | ) 121 | } 122 | 123 | class func deleteAccount( 124 | success: @escaping () -> Void = {}, 125 | failure: @escaping (_ error: Error) -> Void = { _ in } 126 | ) { 127 | let url = "\(currentUserUrl)delete_account" 128 | APIClient.request( 129 | .delete, 130 | url: url, 131 | success: { _, _ in 132 | deleteSession() 133 | success() 134 | }, 135 | failure: failure 136 | ) 137 | } 138 | 139 | class func deleteSession() { 140 | UserDataManager.deleteUser() 141 | SessionManager.deleteSession() 142 | } 143 | 144 | class func saveUserSession( 145 | fromResponse response: [String: Any], 146 | headers: [AnyHashable: Any] 147 | ) { 148 | UserDataManager.currentUser = User( 149 | dictionary: response["user"] as? [String: Any] ?? [:] 150 | ) 151 | if let headers = headers as? [String: Any] { 152 | SessionManager.currentSession = Session(headers: headers) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /init.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let baseProjectName = "swift-ui-base" 4 | var projectName = "RSDemoProject" 5 | let baseDomain = "com.rootstrap" 6 | var bundleDomain = baseDomain 7 | let baseCompany = "Rootstrap Inc." 8 | var companyName = baseCompany 9 | 10 | let whiteList: [String] = [".DS_Store", "UserInterfaceState.xcuserstate", "init.swift", "init"] 11 | let fileManager = FileManager.default 12 | var currentFolder: String { 13 | return fileManager.currentDirectoryPath 14 | } 15 | 16 | enum SetupStep: Int { 17 | case nameEntry = 1 18 | case bundleDomainEntry 19 | case companyNameEntry 20 | 21 | var question: String { 22 | switch self { 23 | case .nameEntry: return "Enter a name for the project" 24 | case .bundleDomainEntry: return "Enter the reversed domain of your organization" 25 | case .companyNameEntry: return "Enter the Company name to use on file's headers" 26 | } 27 | } 28 | } 29 | 30 | // Helper methods 31 | func prompt(message: String) -> String? { 32 | print("\n" + message) 33 | let answer = readLine() 34 | return answer == nil || answer == "" ? nil : answer! 35 | } 36 | 37 | func setup(step: SetupStep, defaultValue: String) -> String { 38 | let result = prompt(message: "\(step.rawValue). " + step.question + " (leave blank for \(defaultValue)).") 39 | guard let res = result else { 40 | print(defaultValue) 41 | return defaultValue 42 | } 43 | return res 44 | } 45 | 46 | func shell(_ args: String...) -> (output: String, exitCode: Int32) { 47 | let task = Process() 48 | task.launchPath = "/usr/bin/env" 49 | task.arguments = args 50 | task.currentDirectoryPath = currentFolder 51 | let pipe = Pipe() 52 | task.standardOutput = pipe 53 | task.launch() 54 | task.waitUntilExit() 55 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 56 | let output = String(data: data, encoding: .utf8) ?? "" 57 | return (output, task.terminationStatus) 58 | } 59 | 60 | extension URL { 61 | var fileName: String { 62 | let urlValues = try? resourceValues(forKeys: [.nameKey]) 63 | return urlValues?.name ?? "" 64 | } 65 | 66 | var isDirectory: Bool { 67 | let urlValues = try? resourceValues(forKeys: [.isDirectoryKey]) 68 | return urlValues?.isDirectory ?? false 69 | } 70 | 71 | func rename(from oldName: String, to newName: String) { 72 | if fileName.contains(oldName) { 73 | let newName = fileName.replacingOccurrences(of: oldName, with: newName) 74 | try! fileManager.moveItem(at: self, to: URL(fileURLWithPath: newName, relativeTo: deletingLastPathComponent())) 75 | } 76 | } 77 | 78 | func replaceOccurrences(of value: String, with newValue: String) { 79 | guard let fileContent = try? String(contentsOfFile: path, encoding: .utf8) else { 80 | print("Unable to read file at: \(self)") 81 | return 82 | } 83 | let updatedContent = fileContent.replacingOccurrences(of: value, with: newValue) 84 | try! updatedContent.write(to: self, atomically: true, encoding: .utf8) 85 | } 86 | 87 | func setupForNewProject() { 88 | replaceOccurrences(of: baseProjectName, with: projectName) 89 | replaceOccurrences(of: baseDomain, with: bundleDomain) 90 | rename(from: baseProjectName, to: projectName) 91 | } 92 | } 93 | 94 | // Helper functions 95 | func changeOrganizationName() { 96 | let pbxProjectPath = "\(currentFolder)/\(baseProjectName).xcodeproj/project.pbxproj" 97 | guard 98 | fileManager.fileExists(atPath: pbxProjectPath), 99 | companyName != baseCompany 100 | else { return } 101 | 102 | print("\nUpdating company name to '\(companyName)'...") 103 | 104 | let filterKey = "ORGANIZATIONNAME" 105 | let organizationNameFilter = "\(filterKey) = \"\(baseCompany)\"" 106 | let organizationNameReplacement = "\(filterKey) = \"\(companyName)\"" 107 | let fileUrl = URL(fileURLWithPath: pbxProjectPath) 108 | fileUrl.replaceOccurrences(of: organizationNameFilter, with: organizationNameReplacement) 109 | } 110 | 111 | // Project Initialization 112 | print(""" 113 | +-----------------------------------------+ 114 | | | 115 | | < New iOS Project Setup > | 116 | | | 117 | +-----------------------------------------+ 118 | """) 119 | 120 | projectName = setup(step: .nameEntry, defaultValue: projectName) 121 | bundleDomain = setup(step: .bundleDomainEntry, defaultValue: baseDomain) 122 | companyName = setup(step: .companyNameEntry, defaultValue: baseCompany) 123 | 124 | //Remove current git tracking 125 | _ = shell("rm", "-rf", ".git") 126 | 127 | changeOrganizationName() 128 | 129 | print("\nRenaming to '\(projectName)'...") 130 | let enumerator = fileManager.enumerator(at: URL(fileURLWithPath: currentFolder), includingPropertiesForKeys: [.nameKey, .isDirectoryKey])! 131 | var directories: [URL] = [] 132 | while let itemURL = enumerator.nextObject() as? URL { 133 | guard !whiteList.contains(itemURL.fileName) else { continue } 134 | if itemURL.isDirectory { 135 | directories.append(itemURL) 136 | } else { 137 | itemURL.setupForNewProject() 138 | } 139 | } 140 | 141 | for dir in directories.reversed() { 142 | dir.rename(from: baseProjectName, to: projectName) 143 | } 144 | //TODO: Rename current dir 145 | let currentURL = URL(fileURLWithPath: currentFolder) 146 | currentURL.rename(from: baseProjectName, to: projectName) 147 | 148 | print("Installing pods...") 149 | _ = shell("pod", "install") 150 | print("Opening new project...") 151 | _ = shell("open", "\(projectName).xcworkspace") 152 | // Initialization Done! 153 | print("************** ALL SET! *******************") 154 | -------------------------------------------------------------------------------- /swift-ui-base.xcodeproj/xcshareddata/xcschemes/swift-ui-base.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 70 | 76 | 77 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 100 | 102 | 108 | 109 | 110 | 111 | 117 | 119 | 125 | 126 | 127 | 128 | 130 | 131 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maintainability](https://api.codeclimate.com/v1/badges/a9aaea084a3759dc5596/maintainability)](https://codeclimate.com/repos/5eda90fd7beea953360073d0/maintainability) 2 | [![Test Coverage](https://api.codeclimate.com/v1/badges/a9aaea084a3759dc5596/test_coverage)](https://codeclimate.com/repos/5eda90fd7beea953360073d0/test_coverage) 3 | [![License](https://img.shields.io/github/license/rootstrap/swift-ui-base.svg)](https://github.com/rootstrap/swift-ui-base/blob/master/LICENSE.md) 4 | 5 | # SwiftUI Base Template 6 | **SwiftUI base** is a boilerplate project created by Rootstrap for new projects using SwiftUI. The main objective is helping any new projects jump start into feature development by providing a handful of functionalities and helpers. 7 | 8 | ## Features 9 | This template comes with: 10 | #### Main 11 | - Complete **API service** class to easily communicate with **REST services**. 12 | - Examples for **account creation** and **Login**. 13 | - Useful classes to **manage User and Session data**. 14 | - **Secure** way to store keys of your **third party integrations**. 15 | - Generic implementation of **navigation** between views. 16 | - Handy **helpers** and **extensions** to make your coding experience faster and easier. 17 | 18 | ## How to use 19 | 1. Clone repo. 20 | 2. Run `./init` from the recently created folder. 21 | 3. Initialize a new git repo and add your remote url. 22 | 4. Done! 23 | 24 | To manage user and session persistence after the original sign in/up we store that information in the native UserDefaults. The parameters that we save are due to the usage of [Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth) for authentication on the server side. Suffice to say that this can be modified to be on par with the server authentication of your choice. 25 | 26 | ## Pods 27 | 28 | - [Alamofire](https://github.com/Alamofire/Alamofire) for easy and elegant connection with an API. 29 | 30 | ## Optional configuration 31 | 32 | ## Code Quality Standards 33 | In order to meet the required code quality standards, this project runs [SwiftLint](https://github.com/realm/SwiftLint ) 34 | during the build phase and reports warnings/errors directly through XCode. 35 | The current SwiftLint rule configuration is based on [Rootstrap's Swift style guides](https://rootstrap.github.io/swift) and is synced with 36 | the CodeCliemate's configuration file. 37 | 38 | **NOTE:** Make sure you have SwiftLint version 0.35.0 or greater installed to avoid known false-positives with some of the rules. 39 | 40 | ## Security recommendations 41 | #### Third Party Keys 42 | 43 | We strongly recommend that all private keys be added to a `.plist` file that will remain locally and not be committed to your project repo. An example file is already provided, these are the final steps to set it up: 44 | 45 | 1. Rename the `ThirdPartyKeys.example.plist` file on your project so that it is called `ThirdPartyKeys.plist`. 46 | To add a set of keys simply add a dictionary with the name you want the key to have and add the corresponding **Debug**, **Staging** and **Release** keys as items. 47 | 2. Remove the reference of `ThirdPartyKeys.plist` from XCode but do not delete the file. This way, you will keep the file locally(it is already in the .gitignore list) in the project directory. 48 | **Note: Do NOT move the file from the current location, the script uses the $(PROJECT_DIR) directory.** 49 | 3. Go to **Product** -> **Scheme** -> **Edit scheme**. Then select **Pre-actions** for the Build stage and make sure that the `Provided build setting` is set to your current target. 50 | **Repeat this step for the Post-actions script.** 51 | 4. Done :) 52 | 53 | 54 | ## Automated Build and Deployment using Fastlane //TODO! 55 | 56 | We use [Fastlane](https://docs.fastlane.tools) to automate code signing, building and deployment. 57 | 58 | 59 | ### New Project Setup 60 | 61 | Several steps need to be executed before building a project for the first time. 62 | 63 | 1. Install latest Xcode command line tools 64 | ``` 65 | xcode-select --install 66 | ``` 67 | 68 | 2. Install latest Fastlane 69 | ``` 70 | # Using RubyGems 71 | sudo gem install fastlane -NV 72 | # Alternatively using Homebrew 73 | brew install fastlane 74 | ``` 75 | 76 | 3. Create required App Ids in the Apple Developer Portal and App Store Connect 77 | This can be done on the Portal itself or using [fastlane produce](https://docs.fastlane.tools/actions/produce/) 78 | ``` 79 | fastlane produce -u {apple_id} --app-name {app_name} --team-id {team_id} --app-identifier {app_id} 80 | ``` 81 | 4. Create private empty repository to store signing certificates, eg. `git@github.com:rootstrap/{app_name}-certificates.git` 82 | 83 | 5. Generate Matchfile with [fastlane match](https://docs.fastlane.tools/actions/match/) 84 | ``` 85 | fastlane match init 86 | ``` 87 | select `git` as storage mode and specify the URL of the certificates git repo 88 | 89 | 6. **optional** If the Developer account has old/invalid certificates which are not shared, it is recommended to use the `nuke` action to clear the existing certificates (*use with caution*): 90 | ``` 91 | fastlane match nuke development 92 | fastlane match nuke distribution 93 | ``` 94 | 95 | 7. **optional** If wanting to reuse existing distribution certificates, these can be imported into the certificates repository using match with the `import` action (fastlane will prompt for location of the `.cer` and `.p12` files): 96 | ``` 97 | fastlane match import \ 98 | --username {{username}} \ 99 | --git_url {{certificates_git_url}} \ 100 | --team_id {{team_id}} \ 101 | --type appstore \ 102 | --app_identifier com.{{company}}.{{app_name}} \ 103 | ``` 104 | 105 | 8. Check the `fastlane/Appfile` and `fastlane/Fastfile`; set and/or validate the following values before use: 106 | - `app_name` # this will match with application name in App Store and target schemes in the project 107 | - `username` # The apple id used to manage the certificates 108 | - `certificates_git_url` # The repo to store and sync certs and provisioning profiles 109 | - `team_id` # The organization's team id 110 | - `itc_team_id` # App Store Connect team id 111 | - `devices` # List of devices to launch simulators for running tests on 112 | 113 | 114 | ### Fastlane usage 115 | Lanes for each deployment target are provided with some basic behavior, which can be modified as needed: 116 | 117 | - Each target has two options: `build_x` and `release_x`. 118 | - The `build` lane will build the application signed with an **Ad Hoc** certificate and keep the `.ipa` in the local folder for upload. 119 | - The `release` lane will: 120 | - Check the repo status (it has to be clean, with no pending changes) 121 | - Increment the build number. 122 | - Tag the new release and push it to the set branch (dev and staging push to develop and production to master by default, but it's configurable). 123 | - Build the app signed with an **App Store** certificate 124 | - Generate a changelog from the commit diff between this new version and the previous. 125 | - Upload to testflight and wait until it's processed. 126 | 127 | - Additionally, lane `test_develop` can be used by the CI job to only run the unit test against simulators (specified by `devices` variable in Fastfile) 128 | 129 | 130 | ## License 131 | 132 | SwiftUI base is available under the MIT license. See the LICENSE file for more info. 133 | 134 | **NOTE:** Remove the free LICENSE file for private projects or replace it with the corresponding license. 135 | 136 | ## Credits 137 | 138 | **SwiftUI Base** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/swift-ui-base/graphs/contributors). 139 | 140 | [](http://www.rootstrap.com) 141 | -------------------------------------------------------------------------------- /swift-ui-base/Networking/Service/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // swift-ui-base 4 | // 5 | // Created by Germán Stábile on 4/1/20. 6 | // Copyright © 2020 Rootstrap. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public enum SwiftBaseErrorCode: Int { 13 | case inputStreamReadFailed = -6000 14 | case outputStreamWriteFailed = -6001 15 | case contentTypeValidationFailed = -6002 16 | case statusCodeValidationFailed = -6003 17 | case dataSerializationFailed = -6004 18 | case stringSerializationFailed = -6005 19 | case jsonSerializationFailed = -6006 20 | case propertyListSerializationFailed = -6007 21 | } 22 | 23 | class BaseURLConvertible: URLConvertible { 24 | 25 | let path: String 26 | let baseUrl: String 27 | 28 | init(path: String, baseUrl: String = APIClient.getBaseUrl()) { 29 | self.path = path 30 | self.baseUrl = baseUrl 31 | } 32 | 33 | func asURL() throws -> URL { 34 | return URL(string: "\(baseUrl)\(path)")! 35 | } 36 | } 37 | 38 | class BaseURLRequestConvertible: URLRequestConvertible { 39 | let url: URLConvertible 40 | let method: HTTPMethod 41 | let headers: HTTPHeaders 42 | let params: [String: Any]? 43 | let encoding: ParameterEncoding? 44 | 45 | func asURLRequest() throws -> URLRequest { 46 | let request = try URLRequest(url: url, 47 | method: method, 48 | headers: headers) 49 | if let params = params, let encoding = encoding { 50 | return try encoding.encode(request, with: params) 51 | } 52 | 53 | return request 54 | } 55 | 56 | init(path: String, 57 | baseUrl: String = APIClient.getBaseUrl(), 58 | method: HTTPMethod, 59 | encoding: ParameterEncoding? = nil, 60 | params: [String: Any]? = nil, 61 | headers: [String: String] = [:]) { 62 | url = BaseURLConvertible(path: path, baseUrl: baseUrl) 63 | self.method = method 64 | self.headers = HTTPHeaders(headers) 65 | self.params = params 66 | self.encoding = encoding 67 | } 68 | } 69 | 70 | public typealias SuccessCallback = ( 71 | _ responseObject: [String: Any], 72 | _ responseHeaders: [AnyHashable: Any] 73 | ) -> Void 74 | public typealias FailureCallback = (_ error: Error) -> Void 75 | 76 | class APIClient { 77 | 78 | enum HTTPHeader: String { 79 | case uid = "uid" 80 | case client = "client" 81 | case token = "access-token" 82 | case expiry = "expiry" 83 | case accept = "Accept" 84 | case contentType = "Content-Type" 85 | } 86 | 87 | private static let emptyDataStatusCodes: Set = [204, 205] 88 | 89 | //Mandatory headers for Rails 5 API 90 | static let baseHeaders: [String: String] = [ 91 | HTTPHeader.accept.rawValue: "application/json", 92 | HTTPHeader.contentType.rawValue: "application/json" 93 | ] 94 | 95 | fileprivate class func getHeaders() -> [String: String] { 96 | if let session = SessionManager.currentSession { 97 | return baseHeaders + [ 98 | HTTPHeader.uid.rawValue: session.uid ?? "", 99 | HTTPHeader.client.rawValue: session.client ?? "", 100 | HTTPHeader.token.rawValue: session.accessToken ?? "" 101 | ] 102 | } 103 | return baseHeaders 104 | } 105 | 106 | fileprivate class func getBaseUrl() -> String { 107 | return Bundle.main.object(forInfoDictionaryKey: "Base URL") as? String ?? "" 108 | } 109 | 110 | //Recursively build multipart params to send along with media in upload requests. 111 | //If params includes the desired root key, call this method with an empty String for rootKey param. 112 | class func multipartFormData( 113 | _ multipartForm: MultipartFormData, 114 | params: Any, rootKey: String 115 | ) { 116 | switch params.self { 117 | case let array as [Any]: 118 | for val in array { 119 | let forwardRootKey = rootKey.isEmpty ? "array[]" : rootKey + "[]" 120 | multipartFormData(multipartForm, params: val, rootKey: forwardRootKey) 121 | } 122 | case let dict as [String: Any]: 123 | for (k, v) in dict { 124 | let forwardRootKey = rootKey.isEmpty ? k : rootKey + "[\(k)]" 125 | multipartFormData(multipartForm, params: v, rootKey: forwardRootKey) 126 | } 127 | default: 128 | if let uploadData = "\(params)".data( 129 | using: String.Encoding.utf8, allowLossyConversion: false 130 | ) { 131 | let forwardRootKey = rootKey.isEmpty ? 132 | "\(type(of: params))".lowercased() : rootKey 133 | multipartForm.append(uploadData, withName: forwardRootKey) 134 | } 135 | } 136 | } 137 | 138 | //Multipart-form base request. Used to upload media along with desired params. 139 | //Note: Multipart request does not support Content-Type = application/json. 140 | //If your API requires this header do not use this method or change backend to skip this validation. 141 | class func multipartRequest(_ method: HTTPMethod = .post, 142 | url: String, 143 | headers: [String: String] = APIClient.getHeaders(), 144 | params: [String: Any]?, 145 | paramsRootKey: String, 146 | media: [MultipartMedia], 147 | success: @escaping SuccessCallback, 148 | failure: @escaping FailureCallback) { 149 | 150 | let requestConvertible = BaseURLRequestConvertible( 151 | path: url, 152 | method: method, 153 | headers: headers 154 | ) 155 | 156 | AF.upload( 157 | multipartFormData: { (multipartForm) -> Void in 158 | if let parameters = params { 159 | multipartFormData(multipartForm, params: parameters, rootKey: paramsRootKey) 160 | } 161 | for elem in media { 162 | elem.embed(inForm: multipartForm) 163 | } 164 | }, 165 | with: requestConvertible) 166 | .responseJSON(completionHandler: { result in 167 | validateResult(result: result, success: success, failure: failure) 168 | }) 169 | } 170 | 171 | class func defaultEncoding(forMethod method: HTTPMethod) -> ParameterEncoding { 172 | switch method { 173 | case .post, .put, .patch: 174 | return JSONEncoding.default 175 | default: 176 | return URLEncoding.default 177 | } 178 | } 179 | 180 | class func request(_ method: HTTPMethod, 181 | url: String, 182 | params: [String: Any]? = nil, 183 | paramsEncoding: ParameterEncoding? = nil, 184 | success: @escaping SuccessCallback, 185 | failure: @escaping FailureCallback) { 186 | let encoding = paramsEncoding ?? defaultEncoding(forMethod: method) 187 | let headers = APIClient.getHeaders() 188 | let requestConvertible = BaseURLRequestConvertible( 189 | path: url, 190 | method: method, 191 | encoding: encoding, 192 | params: params, 193 | headers: headers 194 | ) 195 | 196 | let request = AF.request(requestConvertible) 197 | 198 | request.responseJSON( 199 | completionHandler: { result in 200 | validateResult(result: result, success: success, failure: failure) 201 | }) 202 | } 203 | 204 | //Handle rails-API-base errors if any 205 | class func handleCustomError(_ code: Int?, dictionary: [String: Any]) -> NSError? { 206 | if let messageDict = dictionary["errors"] as? [String: [String]] { 207 | let errorsList = messageDict[messageDict.keys.first!]! 208 | return NSError( 209 | domain: "\(messageDict.keys.first!) \(errorsList.first!)", 210 | code: code ?? 500, userInfo: nil 211 | ) 212 | } else if let error = dictionary["error"] as? String { 213 | return NSError(domain: error, code: code ?? 500, userInfo: nil) 214 | } else if let errors = dictionary["errors"] as? [String: Any] { 215 | let errorDesc = errors[errors.keys.first!]! 216 | return NSError( 217 | domain: "\(errors.keys.first!) " + "\(errorDesc)", 218 | code: code ?? 500, userInfo: nil 219 | ) 220 | } else if dictionary["errors"] != nil || dictionary["error"] != nil { 221 | return NSError( 222 | domain: "Something went wrong. Try again later.", 223 | code: code ?? 500, userInfo: nil 224 | ) 225 | } 226 | return nil 227 | } 228 | 229 | fileprivate class func validateResult(result: AFDataResponse, 230 | success: @escaping SuccessCallback, 231 | failure: @escaping FailureCallback) { 232 | let defaultError = App.error( 233 | domain: .generic, 234 | localizedDescription: "Error parsing response".localized 235 | ) 236 | guard let response = result.response else { 237 | failure(defaultError) 238 | return 239 | } 240 | 241 | guard 242 | let data = result.data, 243 | !data.isEmpty 244 | else { 245 | let emptyResponseAllowed = emptyDataStatusCodes.contains( 246 | result.response?.statusCode ?? 0 247 | ) 248 | emptyResponseAllowed ? 249 | success([:], response.allHeaderFields) : failure(defaultError) 250 | return 251 | } 252 | 253 | var dictionary: [String: Any]? 254 | var serializationError: NSError? 255 | do { 256 | dictionary = try JSONSerialization.jsonObject( 257 | with: data, options: .allowFragments 258 | ) as? [String: Any] 259 | } catch let exceptionError as NSError { 260 | serializationError = exceptionError 261 | } 262 | //Check for errors in validate() or API 263 | if let errorOcurred = APIClient.handleCustomError( 264 | response.statusCode, dictionary: dictionary ?? [:] 265 | ) ?? result.error as NSError? { 266 | failure(errorOcurred) 267 | return 268 | } 269 | //Check for JSON serialization errors if any data received 270 | if let serializationError = serializationError { 271 | if (serializationError as NSError).code == 401 { 272 | AppDelegate.shared.unexpectedLogout() 273 | } 274 | failure(serializationError) 275 | } else { 276 | success(dictionary ?? [:], response.allHeaderFields) 277 | } 278 | } 279 | } 280 | 281 | //Helper to retrieve the right string value for base64 API uploaders 282 | extension Data { 283 | func asBase64Param(withType type: MimeType = .jpeg) -> String { 284 | return "data:\(type.rawValue);base64,\(self.base64EncodedString())" 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /swift-ui-base.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 071D6B682419576500571DE9 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071D6B672419576500571DE9 /* ColorExtension.swift */; }; 11 | 071D6B6C241976C200571DE9 /* TitleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071D6B6B241976C200571DE9 /* TitleModifier.swift */; }; 12 | 073CF2A324AB9CD300885A4D /* NetworkMocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073CF2A224AB9CD300885A4D /* NetworkMocker.swift */; }; 13 | 073CF2A624AB9FA000885A4D /* SignUpSuccessfully.json in Resources */ = {isa = PBXBuildFile; fileRef = 073CF2A524AB9FA000885A4D /* SignUpSuccessfully.json */; }; 14 | 073CF2A824ABA11D00885A4D /* AuthenticationError.json in Resources */ = {isa = PBXBuildFile; fileRef = 073CF2A724ABA11D00885A4D /* AuthenticationError.json */; }; 15 | 073CF2AC24ABA62C00885A4D /* LogOutSuccessfully.json in Resources */ = {isa = PBXBuildFile; fileRef = 073CF2AB24ABA62C00885A4D /* LogOutSuccessfully.json */; }; 16 | 073CF2AE24ABA6B800885A4D /* LoginSuccessfully.json in Resources */ = {isa = PBXBuildFile; fileRef = 073CF2AD24ABA6B800885A4D /* LoginSuccessfully.json */; }; 17 | 073CF2B424ABD09300885A4D /* NetworkMockerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073CF2B324ABD09300885A4D /* NetworkMockerExtension.swift */; }; 18 | 073CF2B624ABD33900885A4D /* GetProfileSuccessfully.json in Resources */ = {isa = PBXBuildFile; fileRef = 073CF2B524ABD33900885A4D /* GetProfileSuccessfully.json */; }; 19 | 07415FE524181D8800486885 /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07415FE424181D8800486885 /* SignUpView.swift */; }; 20 | 07415FE9241832E700486885 /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07415FE8241832E700486885 /* TextFieldView.swift */; }; 21 | 07415FEC24183A7E00486885 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07415FEB24183A7E00486885 /* StringExtension.swift */; }; 22 | 07442D0B241C08E700A235E7 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07442D0A241C08E700A235E7 /* ActivityIndicatorView.swift */; }; 23 | 07442D11241C186100A235E7 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07442D10241C186100A235E7 /* RootView.swift */; }; 24 | 07442D13241C189300A235E7 /* ViewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07442D12241C189300A235E7 /* ViewRouter.swift */; }; 25 | 0748BD9C249BE7F900D5DAF9 /* UserServiceUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0748BD9B249BE7F900D5DAF9 /* UserServiceUnitTests.swift */; }; 26 | 0748BD9F249BE87C00D5DAF9 /* StringExtensionUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0748BD9E249BE87C00D5DAF9 /* StringExtensionUnitTests.swift */; }; 27 | 0748BDA1249BEBA400D5DAF9 /* XCUIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0748BDA0249BEBA300D5DAF9 /* XCUIApplicationExtension.swift */; }; 28 | 0748BDA3249BEBE500D5DAF9 /* XCUIElementExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0748BDA2249BEBE500D5DAF9 /* XCUIElementExtension.swift */; }; 29 | 0748BDA5249BEC1F00D5DAF9 /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0748BDA4249BEC1F00D5DAF9 /* XCTestCaseExtension.swift */; }; 30 | 0749E1B0242A743300340F8B /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0749E1AF242A743300340F8B /* ProfileView.swift */; }; 31 | 074AADB924192895002C6F85 /* TextFieldData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074AADB824192895002C6F85 /* TextFieldData.swift */; }; 32 | 074B1440241AC6ED000CFFA0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B143F241AC6ED000CFFA0 /* LoginViewModel.swift */; }; 33 | 074F38DD245B6AA00042984E /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074F38DC245B6AA00042984E /* ProfileViewModel.swift */; }; 34 | 0750F701248FE225004B1D32 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0750F700248FE225004B1D32 /* AvatarView.swift */; }; 35 | 0750F703248FEE59004B1D32 /* UserServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0750F702248FEE59004B1D32 /* UserServices.swift */; }; 36 | 0750F70524900A9E004B1D32 /* RoundedButtonModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0750F70424900A9E004B1D32 /* RoundedButtonModifier.swift */; }; 37 | 0768431F2434D9B40052FD64 /* AuthenticationServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0768431E2434D9B40052FD64 /* AuthenticationServices.swift */; }; 38 | 076843222434DF8C0052FD64 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843212434DF8C0052FD64 /* Session.swift */; }; 39 | 076843242434DFBC0052FD64 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843232434DFBC0052FD64 /* DictionaryExtension.swift */; }; 40 | 076843262434DFD30052FD64 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843252434DFD30052FD64 /* HTTPHeader.swift */; }; 41 | 076843292434E03B0052FD64 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843282434E03B0052FD64 /* SessionManager.swift */; }; 42 | 0768432E2434E5170052FD64 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0768432D2434E5170052FD64 /* APIClient.swift */; }; 43 | 076843302434E6660052FD64 /* MultipartMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0768432F2434E6660052FD64 /* MultipartMedia.swift */; }; 44 | 076843322434E6B70052FD64 /* Base64Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843312434E6B70052FD64 /* Base64Media.swift */; }; 45 | 076843372434FFC70052FD64 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076843362434FFC70052FD64 /* App.swift */; }; 46 | 07684339243523740052FD64 /* UserDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07684338243523740052FD64 /* UserDataManager.swift */; }; 47 | 0768433B243523AF0052FD64 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0768433A243523AF0052FD64 /* User.swift */; }; 48 | 0778F5C1247436BC00EFCF10 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0778F5C0247436BC00EFCF10 /* ImagePicker.swift */; }; 49 | 07849A77243783CE00321FB9 /* SignUpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07849A76243783CE00321FB9 /* SignUpViewModel.swift */; }; 50 | 07849A7924378E4B00321FB9 /* ImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07849A7824378E4B00321FB9 /* ImageExtension.swift */; }; 51 | 079BE4DC241816F700241627 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 079BE4DB241816F700241627 /* LoginView.swift */; }; 52 | 07A649FC2417E137003C6753 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A649FB2417E137003C6753 /* AppDelegate.swift */; }; 53 | 07A649FE2417E137003C6753 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A649FD2417E137003C6753 /* SceneDelegate.swift */; }; 54 | 07A64A002417E137003C6753 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A649FF2417E137003C6753 /* HomeView.swift */; }; 55 | 07A64A022417E13A003C6753 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07A64A012417E13A003C6753 /* Assets.xcassets */; }; 56 | 07A64A052417E13A003C6753 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07A64A042417E13A003C6753 /* Preview Assets.xcassets */; }; 57 | 07A64A082417E13A003C6753 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07A64A062417E13A003C6753 /* LaunchScreen.storyboard */; }; 58 | 07A64A1E2417E13B003C6753 /* swift_ui_baseUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A64A1D2417E13B003C6753 /* swift_ui_baseUITests.swift */; }; 59 | 6E032E0D24D347FC00952748 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E032E0C24D347FC00952748 /* ConfigurationManager.swift */; }; 60 | B1CFA0536E99A1673CB5E72C /* Pods_swift_ui_base.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 600B3D530407C25A0816E505 /* Pods_swift_ui_base.framework */; }; 61 | C7605DADCC1160F192626202 /* Pods_swift_ui_base_swift_ui_baseUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50BDE825033233BE4D2E9001 /* Pods_swift_ui_base_swift_ui_baseUITests.framework */; }; 62 | D6B7714F4A3569620113FDA6 /* Pods_swift_ui_base_swift_ui_baseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD29B33DB909BD08DE4E0B34 /* Pods_swift_ui_base_swift_ui_baseTests.framework */; }; 63 | /* End PBXBuildFile section */ 64 | 65 | /* Begin PBXContainerItemProxy section */ 66 | 07A64A0F2417E13B003C6753 /* PBXContainerItemProxy */ = { 67 | isa = PBXContainerItemProxy; 68 | containerPortal = 07A649F02417E137003C6753 /* Project object */; 69 | proxyType = 1; 70 | remoteGlobalIDString = 07A649F72417E137003C6753; 71 | remoteInfo = "swift-ui-base"; 72 | }; 73 | 07A64A1A2417E13B003C6753 /* PBXContainerItemProxy */ = { 74 | isa = PBXContainerItemProxy; 75 | containerPortal = 07A649F02417E137003C6753 /* Project object */; 76 | proxyType = 1; 77 | remoteGlobalIDString = 07A649F72417E137003C6753; 78 | remoteInfo = "swift-ui-base"; 79 | }; 80 | /* End PBXContainerItemProxy section */ 81 | 82 | /* Begin PBXFileReference section */ 83 | 071D6B672419576500571DE9 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; 84 | 071D6B6B241976C200571DE9 /* TitleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleModifier.swift; sourceTree = ""; }; 85 | 073CF2A224AB9CD300885A4D /* NetworkMocker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocker.swift; sourceTree = ""; }; 86 | 073CF2A524AB9FA000885A4D /* SignUpSuccessfully.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SignUpSuccessfully.json; sourceTree = ""; }; 87 | 073CF2A724ABA11D00885A4D /* AuthenticationError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = AuthenticationError.json; sourceTree = ""; }; 88 | 073CF2AB24ABA62C00885A4D /* LogOutSuccessfully.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = LogOutSuccessfully.json; sourceTree = ""; }; 89 | 073CF2AD24ABA6B800885A4D /* LoginSuccessfully.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = LoginSuccessfully.json; sourceTree = ""; }; 90 | 073CF2B324ABD09300885A4D /* NetworkMockerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockerExtension.swift; sourceTree = ""; }; 91 | 073CF2B524ABD33900885A4D /* GetProfileSuccessfully.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = GetProfileSuccessfully.json; sourceTree = ""; }; 92 | 07415FE424181D8800486885 /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; 93 | 07415FE8241832E700486885 /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; 94 | 07415FEB24183A7E00486885 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 95 | 07442D0A241C08E700A235E7 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; 96 | 07442D10241C186100A235E7 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 97 | 07442D12241C189300A235E7 /* ViewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewRouter.swift; sourceTree = ""; }; 98 | 0748BD9B249BE7F900D5DAF9 /* UserServiceUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserServiceUnitTests.swift; sourceTree = ""; }; 99 | 0748BD9E249BE87C00D5DAF9 /* StringExtensionUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionUnitTests.swift; sourceTree = ""; }; 100 | 0748BDA0249BEBA300D5DAF9 /* XCUIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIApplicationExtension.swift; sourceTree = ""; }; 101 | 0748BDA2249BEBE500D5DAF9 /* XCUIElementExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElementExtension.swift; sourceTree = ""; }; 102 | 0748BDA4249BEC1F00D5DAF9 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; 103 | 0749E1AF242A743300340F8B /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 104 | 074AADB824192895002C6F85 /* TextFieldData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldData.swift; sourceTree = ""; }; 105 | 074B143F241AC6ED000CFFA0 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 106 | 074F38DC245B6AA00042984E /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 107 | 0750F700248FE225004B1D32 /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 108 | 0750F702248FEE59004B1D32 /* UserServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserServices.swift; sourceTree = ""; }; 109 | 0750F70424900A9E004B1D32 /* RoundedButtonModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedButtonModifier.swift; sourceTree = ""; }; 110 | 0768431E2434D9B40052FD64 /* AuthenticationServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServices.swift; sourceTree = ""; }; 111 | 076843212434DF8C0052FD64 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 112 | 076843232434DFBC0052FD64 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = ""; }; 113 | 076843252434DFD30052FD64 /* HTTPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = ""; }; 114 | 076843282434E03B0052FD64 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 115 | 0768432D2434E5170052FD64 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; 116 | 0768432F2434E6660052FD64 /* MultipartMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartMedia.swift; sourceTree = ""; }; 117 | 076843312434E6B70052FD64 /* Base64Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64Media.swift; sourceTree = ""; }; 118 | 076843362434FFC70052FD64 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 119 | 07684338243523740052FD64 /* UserDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataManager.swift; sourceTree = ""; }; 120 | 0768433A243523AF0052FD64 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 121 | 0778F5C0247436BC00EFCF10 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 122 | 07849A76243783CE00321FB9 /* SignUpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewModel.swift; sourceTree = ""; }; 123 | 07849A7824378E4B00321FB9 /* ImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtension.swift; sourceTree = ""; }; 124 | 079BE4DB241816F700241627 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 125 | 07A649F82417E137003C6753 /* swift-ui-base.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "swift-ui-base.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 126 | 07A649FB2417E137003C6753 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 127 | 07A649FD2417E137003C6753 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 128 | 07A649FF2417E137003C6753 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 129 | 07A64A012417E13A003C6753 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 130 | 07A64A042417E13A003C6753 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 131 | 07A64A072417E13A003C6753 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 132 | 07A64A092417E13A003C6753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 133 | 07A64A0E2417E13B003C6753 /* swift-ui-baseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "swift-ui-baseTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 134 | 07A64A142417E13B003C6753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 135 | 07A64A192417E13B003C6753 /* swift-ui-baseUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "swift-ui-baseUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 136 | 07A64A1D2417E13B003C6753 /* swift_ui_baseUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = swift_ui_baseUITests.swift; sourceTree = ""; }; 137 | 07A64A1F2417E13B003C6753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 138 | 1D5A66BD2D392E3008B86AE5 /* Pods-swift-ui-base.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base.debug.xcconfig"; path = "Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base.debug.xcconfig"; sourceTree = ""; }; 139 | 50BDE825033233BE4D2E9001 /* Pods_swift_ui_base_swift_ui_baseUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_swift_ui_base_swift_ui_baseUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 140 | 600B3D530407C25A0816E505 /* Pods_swift_ui_base.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_swift_ui_base.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 141 | 6E032E0B24D347BE00952748 /* ThirdPartyKeys.example.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ThirdPartyKeys.example.plist; sourceTree = ""; }; 142 | 6E032E0C24D347FC00952748 /* ConfigurationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; 143 | 7E03CB48557E6A834D246B24 /* Pods-swift-ui-base.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base.release.xcconfig"; path = "Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base.release.xcconfig"; sourceTree = ""; }; 144 | 7EC313E43672C416F121D319 /* Pods-swift-ui-base-swift-ui-baseUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseUITests.debug.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests.debug.xcconfig"; sourceTree = ""; }; 145 | 9A0F24E413CA6A32C483EDA7 /* Pods-swift-ui-base-swift-ui-baseTests.uitest.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseTests.uitest.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests.uitest.xcconfig"; sourceTree = ""; }; 146 | A44E706D7F79796D17992D5E /* Pods-swift-ui-base.uitest.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base.uitest.xcconfig"; path = "Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base.uitest.xcconfig"; sourceTree = ""; }; 147 | B06E4CC4A34CD8B26B3128AF /* Pods-swift-ui-base-swift-ui-baseTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseTests.debug.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests.debug.xcconfig"; sourceTree = ""; }; 148 | B1D57A97908D6E25EB01EAF7 /* Pods-swift-ui-base-swift-ui-baseTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseTests.release.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests.release.xcconfig"; sourceTree = ""; }; 149 | D77EFC245B3FA3AAB543A6A3 /* Pods-swift-ui-base-swift-ui-baseUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseUITests.release.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests.release.xcconfig"; sourceTree = ""; }; 150 | DD29B33DB909BD08DE4E0B34 /* Pods_swift_ui_base_swift_ui_baseTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_swift_ui_base_swift_ui_baseTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 151 | FFF599804C76164B16B725B4 /* Pods-swift-ui-base-swift-ui-baseUITests.uitest.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swift-ui-base-swift-ui-baseUITests.uitest.xcconfig"; path = "Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests.uitest.xcconfig"; sourceTree = ""; }; 152 | /* End PBXFileReference section */ 153 | 154 | /* Begin PBXFrameworksBuildPhase section */ 155 | 07A649F52417E137003C6753 /* Frameworks */ = { 156 | isa = PBXFrameworksBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | B1CFA0536E99A1673CB5E72C /* Pods_swift_ui_base.framework in Frameworks */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | 07A64A0B2417E13B003C6753 /* Frameworks */ = { 164 | isa = PBXFrameworksBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | D6B7714F4A3569620113FDA6 /* Pods_swift_ui_base_swift_ui_baseTests.framework in Frameworks */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | 07A64A162417E13B003C6753 /* Frameworks */ = { 172 | isa = PBXFrameworksBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | C7605DADCC1160F192626202 /* Pods_swift_ui_base_swift_ui_baseUITests.framework in Frameworks */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXFrameworksBuildPhase section */ 180 | 181 | /* Begin PBXGroup section */ 182 | 071D6B692419765600571DE9 /* View */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 07442D09241C08D700A235E7 /* ActivityIndicator */, 186 | 07415FE7241832D000486885 /* TextField */, 187 | ); 188 | path = View; 189 | sourceTree = ""; 190 | }; 191 | 071D6B6A2419765B00571DE9 /* Modifier */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 071D6B6B241976C200571DE9 /* TitleModifier.swift */, 195 | 0750F70424900A9E004B1D32 /* RoundedButtonModifier.swift */, 196 | ); 197 | path = Modifier; 198 | sourceTree = ""; 199 | }; 200 | 073CF2A424AB9F8200885A4D /* Resources */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | 073CF2A524AB9FA000885A4D /* SignUpSuccessfully.json */, 204 | 073CF2A724ABA11D00885A4D /* AuthenticationError.json */, 205 | 073CF2AB24ABA62C00885A4D /* LogOutSuccessfully.json */, 206 | 073CF2AD24ABA6B800885A4D /* LoginSuccessfully.json */, 207 | 073CF2B524ABD33900885A4D /* GetProfileSuccessfully.json */, 208 | ); 209 | path = Resources; 210 | sourceTree = ""; 211 | }; 212 | 07415FE62418325A00486885 /* Common */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 076843352434FFBD0052FD64 /* Model */, 216 | 071D6B6A2419765B00571DE9 /* Modifier */, 217 | 071D6B692419765600571DE9 /* View */, 218 | ); 219 | path = Common; 220 | sourceTree = ""; 221 | }; 222 | 07415FE7241832D000486885 /* TextField */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 074AADB72419286F002C6F85 /* View */, 226 | 074AADB62419286A002C6F85 /* Model */, 227 | ); 228 | path = TextField; 229 | sourceTree = ""; 230 | }; 231 | 07415FEA24183A2700486885 /* Extensions */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 07415FEB24183A7E00486885 /* StringExtension.swift */, 235 | 071D6B672419576500571DE9 /* ColorExtension.swift */, 236 | 076843232434DFBC0052FD64 /* DictionaryExtension.swift */, 237 | 07849A7824378E4B00321FB9 /* ImageExtension.swift */, 238 | ); 239 | path = Extensions; 240 | sourceTree = ""; 241 | }; 242 | 07442D09241C08D700A235E7 /* ActivityIndicator */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 07442D0A241C08E700A235E7 /* ActivityIndicatorView.swift */, 246 | ); 247 | path = ActivityIndicator; 248 | sourceTree = ""; 249 | }; 250 | 07442D0D241C169F00A235E7 /* Navigation */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | 07442D0F241C184700A235E7 /* Model */, 254 | 07442D0E241C184100A235E7 /* View */, 255 | ); 256 | path = Navigation; 257 | sourceTree = ""; 258 | }; 259 | 07442D0E241C184100A235E7 /* View */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | 07442D10241C186100A235E7 /* RootView.swift */, 263 | ); 264 | path = View; 265 | sourceTree = ""; 266 | }; 267 | 07442D0F241C184700A235E7 /* Model */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | 07442D12241C189300A235E7 /* ViewRouter.swift */, 271 | ); 272 | path = Model; 273 | sourceTree = ""; 274 | }; 275 | 0748BD9A249BE7AF00D5DAF9 /* Services */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 0748BD9B249BE7F900D5DAF9 /* UserServiceUnitTests.swift */, 279 | ); 280 | path = Services; 281 | sourceTree = ""; 282 | }; 283 | 0748BD9D249BE84F00D5DAF9 /* Extensions */ = { 284 | isa = PBXGroup; 285 | children = ( 286 | 0748BD9E249BE87C00D5DAF9 /* StringExtensionUnitTests.swift */, 287 | ); 288 | path = Extensions; 289 | sourceTree = ""; 290 | }; 291 | 074AADB62419286A002C6F85 /* Model */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 074AADB824192895002C6F85 /* TextFieldData.swift */, 295 | ); 296 | path = Model; 297 | sourceTree = ""; 298 | }; 299 | 074AADB72419286F002C6F85 /* View */ = { 300 | isa = PBXGroup; 301 | children = ( 302 | 07415FE8241832E700486885 /* TextFieldView.swift */, 303 | ); 304 | path = View; 305 | sourceTree = ""; 306 | }; 307 | 074B143E241AC6D7000CFFA0 /* ViewModel */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | 074B143F241AC6ED000CFFA0 /* LoginViewModel.swift */, 311 | 07849A76243783CE00321FB9 /* SignUpViewModel.swift */, 312 | ); 313 | path = ViewModel; 314 | sourceTree = ""; 315 | }; 316 | 074F38DB245B6A8A0042984E /* ViewModel */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | 074F38DC245B6AA00042984E /* ProfileViewModel.swift */, 320 | ); 321 | path = ViewModel; 322 | sourceTree = ""; 323 | }; 324 | 0768431B2434D9520052FD64 /* Networking */ = { 325 | isa = PBXGroup; 326 | children = ( 327 | 0768431D2434D9960052FD64 /* Service */, 328 | 0768431C2434D98B0052FD64 /* Model */, 329 | ); 330 | path = Networking; 331 | sourceTree = ""; 332 | }; 333 | 0768431C2434D98B0052FD64 /* Model */ = { 334 | isa = PBXGroup; 335 | children = ( 336 | 076843212434DF8C0052FD64 /* Session.swift */, 337 | 076843252434DFD30052FD64 /* HTTPHeader.swift */, 338 | 0768432F2434E6660052FD64 /* MultipartMedia.swift */, 339 | 076843312434E6B70052FD64 /* Base64Media.swift */, 340 | ); 341 | path = Model; 342 | sourceTree = ""; 343 | }; 344 | 0768431D2434D9960052FD64 /* Service */ = { 345 | isa = PBXGroup; 346 | children = ( 347 | 0768431E2434D9B40052FD64 /* AuthenticationServices.swift */, 348 | 0768432D2434E5170052FD64 /* APIClient.swift */, 349 | 0750F702248FEE59004B1D32 /* UserServices.swift */, 350 | ); 351 | path = Service; 352 | sourceTree = ""; 353 | }; 354 | 076843272434E01C0052FD64 /* Managers */ = { 355 | isa = PBXGroup; 356 | children = ( 357 | 07684338243523740052FD64 /* UserDataManager.swift */, 358 | 6E032E0C24D347FC00952748 /* ConfigurationManager.swift */, 359 | 076843282434E03B0052FD64 /* SessionManager.swift */, 360 | ); 361 | path = Managers; 362 | sourceTree = ""; 363 | }; 364 | 076843352434FFBD0052FD64 /* Model */ = { 365 | isa = PBXGroup; 366 | children = ( 367 | 076843362434FFC70052FD64 /* App.swift */, 368 | 0768433A243523AF0052FD64 /* User.swift */, 369 | ); 370 | path = Model; 371 | sourceTree = ""; 372 | }; 373 | 0778F5BE2474369F00EFCF10 /* ImagePicker */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | 0778F5BF247436AE00EFCF10 /* View */, 377 | ); 378 | path = ImagePicker; 379 | sourceTree = ""; 380 | }; 381 | 0778F5BF247436AE00EFCF10 /* View */ = { 382 | isa = PBXGroup; 383 | children = ( 384 | 0778F5C0247436BC00EFCF10 /* ImagePicker.swift */, 385 | ); 386 | path = View; 387 | sourceTree = ""; 388 | }; 389 | 079BE4D7241816B100241627 /* Home */ = { 390 | isa = PBXGroup; 391 | children = ( 392 | 074F38DB245B6A8A0042984E /* ViewModel */, 393 | 079BE4D8241816B700241627 /* View */, 394 | ); 395 | path = Home; 396 | sourceTree = ""; 397 | }; 398 | 079BE4D8241816B700241627 /* View */ = { 399 | isa = PBXGroup; 400 | children = ( 401 | 07A649FF2417E137003C6753 /* HomeView.swift */, 402 | 0749E1AF242A743300340F8B /* ProfileView.swift */, 403 | 0750F700248FE225004B1D32 /* AvatarView.swift */, 404 | ); 405 | path = View; 406 | sourceTree = ""; 407 | }; 408 | 079BE4D9241816C800241627 /* Authentication */ = { 409 | isa = PBXGroup; 410 | children = ( 411 | 074B143E241AC6D7000CFFA0 /* ViewModel */, 412 | 079BE4DA241816DE00241627 /* View */, 413 | ); 414 | path = Authentication; 415 | sourceTree = ""; 416 | }; 417 | 079BE4DA241816DE00241627 /* View */ = { 418 | isa = PBXGroup; 419 | children = ( 420 | 079BE4DB241816F700241627 /* LoginView.swift */, 421 | 07415FE424181D8800486885 /* SignUpView.swift */, 422 | ); 423 | path = View; 424 | sourceTree = ""; 425 | }; 426 | 07A649EF2417E137003C6753 = { 427 | isa = PBXGroup; 428 | children = ( 429 | 6E032E0B24D347BE00952748 /* ThirdPartyKeys.example.plist */, 430 | 07A649FA2417E137003C6753 /* swift-ui-base */, 431 | 07A64A112417E13B003C6753 /* swift-ui-baseTests */, 432 | 07A64A1C2417E13B003C6753 /* swift-ui-baseUITests */, 433 | 07A649F92417E137003C6753 /* Products */, 434 | 6CB79ED3831C28F2FA96610D /* Pods */, 435 | 76C04F8605B8EFC077D4B5FB /* Frameworks */, 436 | ); 437 | sourceTree = ""; 438 | }; 439 | 07A649F92417E137003C6753 /* Products */ = { 440 | isa = PBXGroup; 441 | children = ( 442 | 07A649F82417E137003C6753 /* swift-ui-base.app */, 443 | 07A64A0E2417E13B003C6753 /* swift-ui-baseTests.xctest */, 444 | 07A64A192417E13B003C6753 /* swift-ui-baseUITests.xctest */, 445 | ); 446 | name = Products; 447 | sourceTree = ""; 448 | }; 449 | 07A649FA2417E137003C6753 /* swift-ui-base */ = { 450 | isa = PBXGroup; 451 | children = ( 452 | 07A649FB2417E137003C6753 /* AppDelegate.swift */, 453 | 07A649FD2417E137003C6753 /* SceneDelegate.swift */, 454 | 07415FEA24183A2700486885 /* Extensions */, 455 | 07415FE62418325A00486885 /* Common */, 456 | 079BE4D9241816C800241627 /* Authentication */, 457 | 079BE4D7241816B100241627 /* Home */, 458 | 0768431B2434D9520052FD64 /* Networking */, 459 | 0778F5BE2474369F00EFCF10 /* ImagePicker */, 460 | 07442D0D241C169F00A235E7 /* Navigation */, 461 | 076843272434E01C0052FD64 /* Managers */, 462 | 07A64A012417E13A003C6753 /* Assets.xcassets */, 463 | 07A64A062417E13A003C6753 /* LaunchScreen.storyboard */, 464 | 07A64A092417E13A003C6753 /* Info.plist */, 465 | 07A64A032417E13A003C6753 /* Preview Content */, 466 | ); 467 | path = "swift-ui-base"; 468 | sourceTree = ""; 469 | }; 470 | 07A64A032417E13A003C6753 /* Preview Content */ = { 471 | isa = PBXGroup; 472 | children = ( 473 | 07A64A042417E13A003C6753 /* Preview Assets.xcassets */, 474 | ); 475 | path = "Preview Content"; 476 | sourceTree = ""; 477 | }; 478 | 07A64A112417E13B003C6753 /* swift-ui-baseTests */ = { 479 | isa = PBXGroup; 480 | children = ( 481 | 0748BD9D249BE84F00D5DAF9 /* Extensions */, 482 | 0748BD9A249BE7AF00D5DAF9 /* Services */, 483 | 07A64A142417E13B003C6753 /* Info.plist */, 484 | ); 485 | path = "swift-ui-baseTests"; 486 | sourceTree = ""; 487 | }; 488 | 07A64A1C2417E13B003C6753 /* swift-ui-baseUITests */ = { 489 | isa = PBXGroup; 490 | children = ( 491 | 073CF2A424AB9F8200885A4D /* Resources */, 492 | 07A64A1D2417E13B003C6753 /* swift_ui_baseUITests.swift */, 493 | 07A64A1F2417E13B003C6753 /* Info.plist */, 494 | 0748BDA0249BEBA300D5DAF9 /* XCUIApplicationExtension.swift */, 495 | 0748BDA2249BEBE500D5DAF9 /* XCUIElementExtension.swift */, 496 | 0748BDA4249BEC1F00D5DAF9 /* XCTestCaseExtension.swift */, 497 | 073CF2A224AB9CD300885A4D /* NetworkMocker.swift */, 498 | 073CF2B324ABD09300885A4D /* NetworkMockerExtension.swift */, 499 | ); 500 | path = "swift-ui-baseUITests"; 501 | sourceTree = ""; 502 | }; 503 | 6CB79ED3831C28F2FA96610D /* Pods */ = { 504 | isa = PBXGroup; 505 | children = ( 506 | 1D5A66BD2D392E3008B86AE5 /* Pods-swift-ui-base.debug.xcconfig */, 507 | A44E706D7F79796D17992D5E /* Pods-swift-ui-base.uitest.xcconfig */, 508 | 7E03CB48557E6A834D246B24 /* Pods-swift-ui-base.release.xcconfig */, 509 | B06E4CC4A34CD8B26B3128AF /* Pods-swift-ui-base-swift-ui-baseTests.debug.xcconfig */, 510 | 9A0F24E413CA6A32C483EDA7 /* Pods-swift-ui-base-swift-ui-baseTests.uitest.xcconfig */, 511 | B1D57A97908D6E25EB01EAF7 /* Pods-swift-ui-base-swift-ui-baseTests.release.xcconfig */, 512 | 7EC313E43672C416F121D319 /* Pods-swift-ui-base-swift-ui-baseUITests.debug.xcconfig */, 513 | FFF599804C76164B16B725B4 /* Pods-swift-ui-base-swift-ui-baseUITests.uitest.xcconfig */, 514 | D77EFC245B3FA3AAB543A6A3 /* Pods-swift-ui-base-swift-ui-baseUITests.release.xcconfig */, 515 | ); 516 | path = Pods; 517 | sourceTree = ""; 518 | }; 519 | 76C04F8605B8EFC077D4B5FB /* Frameworks */ = { 520 | isa = PBXGroup; 521 | children = ( 522 | 600B3D530407C25A0816E505 /* Pods_swift_ui_base.framework */, 523 | DD29B33DB909BD08DE4E0B34 /* Pods_swift_ui_base_swift_ui_baseTests.framework */, 524 | 50BDE825033233BE4D2E9001 /* Pods_swift_ui_base_swift_ui_baseUITests.framework */, 525 | ); 526 | name = Frameworks; 527 | sourceTree = ""; 528 | }; 529 | /* End PBXGroup section */ 530 | 531 | /* Begin PBXNativeTarget section */ 532 | 07A649F72417E137003C6753 /* swift-ui-base */ = { 533 | isa = PBXNativeTarget; 534 | buildConfigurationList = 07A64A222417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-base" */; 535 | buildPhases = ( 536 | 4B075428A0F5713362B4C139 /* [CP] Check Pods Manifest.lock */, 537 | 07A649F42417E137003C6753 /* Sources */, 538 | 07A649F52417E137003C6753 /* Frameworks */, 539 | 07A649F62417E137003C6753 /* Resources */, 540 | 186470753BA1BD174DEB8FBC /* [CP] Embed Pods Frameworks */, 541 | ); 542 | buildRules = ( 543 | ); 544 | dependencies = ( 545 | ); 546 | name = "swift-ui-base"; 547 | productName = "swift-ui-base"; 548 | productReference = 07A649F82417E137003C6753 /* swift-ui-base.app */; 549 | productType = "com.apple.product-type.application"; 550 | }; 551 | 07A64A0D2417E13B003C6753 /* swift-ui-baseTests */ = { 552 | isa = PBXNativeTarget; 553 | buildConfigurationList = 07A64A252417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-baseTests" */; 554 | buildPhases = ( 555 | 223F65BAAF9D5EE795660241 /* [CP] Check Pods Manifest.lock */, 556 | 07A64A0A2417E13B003C6753 /* Sources */, 557 | 07A64A0B2417E13B003C6753 /* Frameworks */, 558 | 07A64A0C2417E13B003C6753 /* Resources */, 559 | 3F79AD3E3E8D2FD2A6B5D10A /* [CP] Embed Pods Frameworks */, 560 | ); 561 | buildRules = ( 562 | ); 563 | dependencies = ( 564 | 07A64A102417E13B003C6753 /* PBXTargetDependency */, 565 | ); 566 | name = "swift-ui-baseTests"; 567 | productName = "swift-ui-baseTests"; 568 | productReference = 07A64A0E2417E13B003C6753 /* swift-ui-baseTests.xctest */; 569 | productType = "com.apple.product-type.bundle.unit-test"; 570 | }; 571 | 07A64A182417E13B003C6753 /* swift-ui-baseUITests */ = { 572 | isa = PBXNativeTarget; 573 | buildConfigurationList = 07A64A282417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-baseUITests" */; 574 | buildPhases = ( 575 | 53F4300D7B90091BCE83CDCB /* [CP] Check Pods Manifest.lock */, 576 | 07A64A152417E13B003C6753 /* Sources */, 577 | 07A64A162417E13B003C6753 /* Frameworks */, 578 | 07A64A172417E13B003C6753 /* Resources */, 579 | 6DA6DE36880E1CB9D374C91C /* [CP] Embed Pods Frameworks */, 580 | ); 581 | buildRules = ( 582 | ); 583 | dependencies = ( 584 | 07A64A1B2417E13B003C6753 /* PBXTargetDependency */, 585 | ); 586 | name = "swift-ui-baseUITests"; 587 | productName = "swift-ui-baseUITests"; 588 | productReference = 07A64A192417E13B003C6753 /* swift-ui-baseUITests.xctest */; 589 | productType = "com.apple.product-type.bundle.ui-testing"; 590 | }; 591 | /* End PBXNativeTarget section */ 592 | 593 | /* Begin PBXProject section */ 594 | 07A649F02417E137003C6753 /* Project object */ = { 595 | isa = PBXProject; 596 | attributes = { 597 | LastSwiftUpdateCheck = 1120; 598 | LastUpgradeCheck = 1120; 599 | ORGANIZATIONNAME = Rootstrap; 600 | TargetAttributes = { 601 | 07A649F72417E137003C6753 = { 602 | CreatedOnToolsVersion = 11.2.1; 603 | }; 604 | 07A64A0D2417E13B003C6753 = { 605 | CreatedOnToolsVersion = 11.2.1; 606 | LastSwiftMigration = 1140; 607 | TestTargetID = 07A649F72417E137003C6753; 608 | }; 609 | 07A64A182417E13B003C6753 = { 610 | CreatedOnToolsVersion = 11.2.1; 611 | TestTargetID = 07A649F72417E137003C6753; 612 | }; 613 | }; 614 | }; 615 | buildConfigurationList = 07A649F32417E137003C6753 /* Build configuration list for PBXProject "swift-ui-base" */; 616 | compatibilityVersion = "Xcode 9.3"; 617 | developmentRegion = en; 618 | hasScannedForEncodings = 0; 619 | knownRegions = ( 620 | en, 621 | Base, 622 | ); 623 | mainGroup = 07A649EF2417E137003C6753; 624 | productRefGroup = 07A649F92417E137003C6753 /* Products */; 625 | projectDirPath = ""; 626 | projectRoot = ""; 627 | targets = ( 628 | 07A649F72417E137003C6753 /* swift-ui-base */, 629 | 07A64A0D2417E13B003C6753 /* swift-ui-baseTests */, 630 | 07A64A182417E13B003C6753 /* swift-ui-baseUITests */, 631 | ); 632 | }; 633 | /* End PBXProject section */ 634 | 635 | /* Begin PBXResourcesBuildPhase section */ 636 | 07A649F62417E137003C6753 /* Resources */ = { 637 | isa = PBXResourcesBuildPhase; 638 | buildActionMask = 2147483647; 639 | files = ( 640 | 07A64A082417E13A003C6753 /* LaunchScreen.storyboard in Resources */, 641 | 07A64A052417E13A003C6753 /* Preview Assets.xcassets in Resources */, 642 | 07A64A022417E13A003C6753 /* Assets.xcassets in Resources */, 643 | ); 644 | runOnlyForDeploymentPostprocessing = 0; 645 | }; 646 | 07A64A0C2417E13B003C6753 /* Resources */ = { 647 | isa = PBXResourcesBuildPhase; 648 | buildActionMask = 2147483647; 649 | files = ( 650 | ); 651 | runOnlyForDeploymentPostprocessing = 0; 652 | }; 653 | 07A64A172417E13B003C6753 /* Resources */ = { 654 | isa = PBXResourcesBuildPhase; 655 | buildActionMask = 2147483647; 656 | files = ( 657 | 073CF2B624ABD33900885A4D /* GetProfileSuccessfully.json in Resources */, 658 | 073CF2AE24ABA6B800885A4D /* LoginSuccessfully.json in Resources */, 659 | 073CF2AC24ABA62C00885A4D /* LogOutSuccessfully.json in Resources */, 660 | 073CF2A824ABA11D00885A4D /* AuthenticationError.json in Resources */, 661 | 073CF2A624AB9FA000885A4D /* SignUpSuccessfully.json in Resources */, 662 | ); 663 | runOnlyForDeploymentPostprocessing = 0; 664 | }; 665 | /* End PBXResourcesBuildPhase section */ 666 | 667 | /* Begin PBXShellScriptBuildPhase section */ 668 | 186470753BA1BD174DEB8FBC /* [CP] Embed Pods Frameworks */ = { 669 | isa = PBXShellScriptBuildPhase; 670 | buildActionMask = 2147483647; 671 | files = ( 672 | ); 673 | inputFileListPaths = ( 674 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base-frameworks-${CONFIGURATION}-input-files.xcfilelist", 675 | ); 676 | name = "[CP] Embed Pods Frameworks"; 677 | outputFileListPaths = ( 678 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base-frameworks-${CONFIGURATION}-output-files.xcfilelist", 679 | ); 680 | runOnlyForDeploymentPostprocessing = 0; 681 | shellPath = /bin/sh; 682 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-swift-ui-base/Pods-swift-ui-base-frameworks.sh\"\n"; 683 | showEnvVarsInLog = 0; 684 | }; 685 | 223F65BAAF9D5EE795660241 /* [CP] Check Pods Manifest.lock */ = { 686 | isa = PBXShellScriptBuildPhase; 687 | buildActionMask = 2147483647; 688 | files = ( 689 | ); 690 | inputFileListPaths = ( 691 | ); 692 | inputPaths = ( 693 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 694 | "${PODS_ROOT}/Manifest.lock", 695 | ); 696 | name = "[CP] Check Pods Manifest.lock"; 697 | outputFileListPaths = ( 698 | ); 699 | outputPaths = ( 700 | "$(DERIVED_FILE_DIR)/Pods-swift-ui-base-swift-ui-baseTests-checkManifestLockResult.txt", 701 | ); 702 | runOnlyForDeploymentPostprocessing = 0; 703 | shellPath = /bin/sh; 704 | 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"; 705 | showEnvVarsInLog = 0; 706 | }; 707 | 3F79AD3E3E8D2FD2A6B5D10A /* [CP] Embed Pods Frameworks */ = { 708 | isa = PBXShellScriptBuildPhase; 709 | buildActionMask = 2147483647; 710 | files = ( 711 | ); 712 | inputFileListPaths = ( 713 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", 714 | ); 715 | name = "[CP] Embed Pods Frameworks"; 716 | outputFileListPaths = ( 717 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", 718 | ); 719 | runOnlyForDeploymentPostprocessing = 0; 720 | shellPath = /bin/sh; 721 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseTests/Pods-swift-ui-base-swift-ui-baseTests-frameworks.sh\"\n"; 722 | showEnvVarsInLog = 0; 723 | }; 724 | 4B075428A0F5713362B4C139 /* [CP] Check Pods Manifest.lock */ = { 725 | isa = PBXShellScriptBuildPhase; 726 | buildActionMask = 2147483647; 727 | files = ( 728 | ); 729 | inputFileListPaths = ( 730 | ); 731 | inputPaths = ( 732 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 733 | "${PODS_ROOT}/Manifest.lock", 734 | ); 735 | name = "[CP] Check Pods Manifest.lock"; 736 | outputFileListPaths = ( 737 | ); 738 | outputPaths = ( 739 | "$(DERIVED_FILE_DIR)/Pods-swift-ui-base-checkManifestLockResult.txt", 740 | ); 741 | runOnlyForDeploymentPostprocessing = 0; 742 | shellPath = /bin/sh; 743 | 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"; 744 | showEnvVarsInLog = 0; 745 | }; 746 | 53F4300D7B90091BCE83CDCB /* [CP] Check Pods Manifest.lock */ = { 747 | isa = PBXShellScriptBuildPhase; 748 | buildActionMask = 2147483647; 749 | files = ( 750 | ); 751 | inputFileListPaths = ( 752 | ); 753 | inputPaths = ( 754 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 755 | "${PODS_ROOT}/Manifest.lock", 756 | ); 757 | name = "[CP] Check Pods Manifest.lock"; 758 | outputFileListPaths = ( 759 | ); 760 | outputPaths = ( 761 | "$(DERIVED_FILE_DIR)/Pods-swift-ui-base-swift-ui-baseUITests-checkManifestLockResult.txt", 762 | ); 763 | runOnlyForDeploymentPostprocessing = 0; 764 | shellPath = /bin/sh; 765 | 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"; 766 | showEnvVarsInLog = 0; 767 | }; 768 | 6DA6DE36880E1CB9D374C91C /* [CP] Embed Pods Frameworks */ = { 769 | isa = PBXShellScriptBuildPhase; 770 | buildActionMask = 2147483647; 771 | files = ( 772 | ); 773 | inputFileListPaths = ( 774 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", 775 | ); 776 | name = "[CP] Embed Pods Frameworks"; 777 | outputFileListPaths = ( 778 | "${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", 779 | ); 780 | runOnlyForDeploymentPostprocessing = 0; 781 | shellPath = /bin/sh; 782 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-swift-ui-base-swift-ui-baseUITests/Pods-swift-ui-base-swift-ui-baseUITests-frameworks.sh\"\n"; 783 | showEnvVarsInLog = 0; 784 | }; 785 | /* End PBXShellScriptBuildPhase section */ 786 | 787 | /* Begin PBXSourcesBuildPhase section */ 788 | 07A649F42417E137003C6753 /* Sources */ = { 789 | isa = PBXSourcesBuildPhase; 790 | buildActionMask = 2147483647; 791 | files = ( 792 | 07A649FC2417E137003C6753 /* AppDelegate.swift in Sources */, 793 | 0750F703248FEE59004B1D32 /* UserServices.swift in Sources */, 794 | 076843222434DF8C0052FD64 /* Session.swift in Sources */, 795 | 07442D13241C189300A235E7 /* ViewRouter.swift in Sources */, 796 | 074AADB924192895002C6F85 /* TextFieldData.swift in Sources */, 797 | 074B1440241AC6ED000CFFA0 /* LoginViewModel.swift in Sources */, 798 | 0750F701248FE225004B1D32 /* AvatarView.swift in Sources */, 799 | 0768433B243523AF0052FD64 /* User.swift in Sources */, 800 | 07849A77243783CE00321FB9 /* SignUpViewModel.swift in Sources */, 801 | 076843302434E6660052FD64 /* MultipartMedia.swift in Sources */, 802 | 6E032E0D24D347FC00952748 /* ConfigurationManager.swift in Sources */, 803 | 0768432E2434E5170052FD64 /* APIClient.swift in Sources */, 804 | 07442D0B241C08E700A235E7 /* ActivityIndicatorView.swift in Sources */, 805 | 07415FEC24183A7E00486885 /* StringExtension.swift in Sources */, 806 | 071D6B6C241976C200571DE9 /* TitleModifier.swift in Sources */, 807 | 076843322434E6B70052FD64 /* Base64Media.swift in Sources */, 808 | 0749E1B0242A743300340F8B /* ProfileView.swift in Sources */, 809 | 076843372434FFC70052FD64 /* App.swift in Sources */, 810 | 07849A7924378E4B00321FB9 /* ImageExtension.swift in Sources */, 811 | 076843242434DFBC0052FD64 /* DictionaryExtension.swift in Sources */, 812 | 0768431F2434D9B40052FD64 /* AuthenticationServices.swift in Sources */, 813 | 071D6B682419576500571DE9 /* ColorExtension.swift in Sources */, 814 | 07415FE524181D8800486885 /* SignUpView.swift in Sources */, 815 | 07415FE9241832E700486885 /* TextFieldView.swift in Sources */, 816 | 07442D11241C186100A235E7 /* RootView.swift in Sources */, 817 | 0750F70524900A9E004B1D32 /* RoundedButtonModifier.swift in Sources */, 818 | 07A649FE2417E137003C6753 /* SceneDelegate.swift in Sources */, 819 | 0778F5C1247436BC00EFCF10 /* ImagePicker.swift in Sources */, 820 | 07684339243523740052FD64 /* UserDataManager.swift in Sources */, 821 | 074F38DD245B6AA00042984E /* ProfileViewModel.swift in Sources */, 822 | 079BE4DC241816F700241627 /* LoginView.swift in Sources */, 823 | 076843262434DFD30052FD64 /* HTTPHeader.swift in Sources */, 824 | 076843292434E03B0052FD64 /* SessionManager.swift in Sources */, 825 | 07A64A002417E137003C6753 /* HomeView.swift in Sources */, 826 | ); 827 | runOnlyForDeploymentPostprocessing = 0; 828 | }; 829 | 07A64A0A2417E13B003C6753 /* Sources */ = { 830 | isa = PBXSourcesBuildPhase; 831 | buildActionMask = 2147483647; 832 | files = ( 833 | 0748BD9F249BE87C00D5DAF9 /* StringExtensionUnitTests.swift in Sources */, 834 | 0748BD9C249BE7F900D5DAF9 /* UserServiceUnitTests.swift in Sources */, 835 | ); 836 | runOnlyForDeploymentPostprocessing = 0; 837 | }; 838 | 07A64A152417E13B003C6753 /* Sources */ = { 839 | isa = PBXSourcesBuildPhase; 840 | buildActionMask = 2147483647; 841 | files = ( 842 | 0748BDA5249BEC1F00D5DAF9 /* XCTestCaseExtension.swift in Sources */, 843 | 07A64A1E2417E13B003C6753 /* swift_ui_baseUITests.swift in Sources */, 844 | 0748BDA1249BEBA400D5DAF9 /* XCUIApplicationExtension.swift in Sources */, 845 | 073CF2B424ABD09300885A4D /* NetworkMockerExtension.swift in Sources */, 846 | 073CF2A324AB9CD300885A4D /* NetworkMocker.swift in Sources */, 847 | 0748BDA3249BEBE500D5DAF9 /* XCUIElementExtension.swift in Sources */, 848 | ); 849 | runOnlyForDeploymentPostprocessing = 0; 850 | }; 851 | /* End PBXSourcesBuildPhase section */ 852 | 853 | /* Begin PBXTargetDependency section */ 854 | 07A64A102417E13B003C6753 /* PBXTargetDependency */ = { 855 | isa = PBXTargetDependency; 856 | target = 07A649F72417E137003C6753 /* swift-ui-base */; 857 | targetProxy = 07A64A0F2417E13B003C6753 /* PBXContainerItemProxy */; 858 | }; 859 | 07A64A1B2417E13B003C6753 /* PBXTargetDependency */ = { 860 | isa = PBXTargetDependency; 861 | target = 07A649F72417E137003C6753 /* swift-ui-base */; 862 | targetProxy = 07A64A1A2417E13B003C6753 /* PBXContainerItemProxy */; 863 | }; 864 | /* End PBXTargetDependency section */ 865 | 866 | /* Begin PBXVariantGroup section */ 867 | 07A64A062417E13A003C6753 /* LaunchScreen.storyboard */ = { 868 | isa = PBXVariantGroup; 869 | children = ( 870 | 07A64A072417E13A003C6753 /* Base */, 871 | ); 872 | name = LaunchScreen.storyboard; 873 | sourceTree = ""; 874 | }; 875 | /* End PBXVariantGroup section */ 876 | 877 | /* Begin XCBuildConfiguration section */ 878 | 073CF2AF24ABB0FF00885A4D /* UITest */ = { 879 | isa = XCBuildConfiguration; 880 | buildSettings = { 881 | ALWAYS_SEARCH_USER_PATHS = NO; 882 | CLANG_ANALYZER_NONNULL = YES; 883 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 884 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 885 | CLANG_CXX_LIBRARY = "libc++"; 886 | CLANG_ENABLE_MODULES = YES; 887 | CLANG_ENABLE_OBJC_ARC = YES; 888 | CLANG_ENABLE_OBJC_WEAK = YES; 889 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 890 | CLANG_WARN_BOOL_CONVERSION = YES; 891 | CLANG_WARN_COMMA = YES; 892 | CLANG_WARN_CONSTANT_CONVERSION = YES; 893 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 894 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 895 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 896 | CLANG_WARN_EMPTY_BODY = YES; 897 | CLANG_WARN_ENUM_CONVERSION = YES; 898 | CLANG_WARN_INFINITE_RECURSION = YES; 899 | CLANG_WARN_INT_CONVERSION = YES; 900 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 901 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 902 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 903 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 904 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 905 | CLANG_WARN_STRICT_PROTOTYPES = YES; 906 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 907 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 908 | CLANG_WARN_UNREACHABLE_CODE = YES; 909 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 910 | COPY_PHASE_STRIP = NO; 911 | DEBUG_INFORMATION_FORMAT = dwarf; 912 | ENABLE_STRICT_OBJC_MSGSEND = YES; 913 | ENABLE_TESTABILITY = YES; 914 | GCC_C_LANGUAGE_STANDARD = gnu11; 915 | GCC_DYNAMIC_NO_PIC = NO; 916 | GCC_NO_COMMON_BLOCKS = YES; 917 | GCC_OPTIMIZATION_LEVEL = 0; 918 | GCC_PREPROCESSOR_DEFINITIONS = ( 919 | "DEBUG=1", 920 | "$(inherited)", 921 | ); 922 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 923 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 924 | GCC_WARN_UNDECLARED_SELECTOR = YES; 925 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 926 | GCC_WARN_UNUSED_FUNCTION = YES; 927 | GCC_WARN_UNUSED_VARIABLE = YES; 928 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 929 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 930 | MTL_FAST_MATH = YES; 931 | ONLY_ACTIVE_ARCH = YES; 932 | SDKROOT = iphoneos; 933 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 934 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 935 | }; 936 | name = UITest; 937 | }; 938 | 073CF2B024ABB0FF00885A4D /* UITest */ = { 939 | isa = XCBuildConfiguration; 940 | baseConfigurationReference = A44E706D7F79796D17992D5E /* Pods-swift-ui-base.uitest.xcconfig */; 941 | buildSettings = { 942 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 943 | BASE_URL = "http://localhost:8080"; 944 | CODE_SIGN_STYLE = Automatic; 945 | DEVELOPMENT_ASSET_PATHS = "\"swift-ui-base/Preview Content\""; 946 | DEVELOPMENT_TEAM = V9839GT2TL; 947 | ENABLE_PREVIEWS = YES; 948 | INFOPLIST_FILE = "swift-ui-base/Info.plist"; 949 | LD_RUNPATH_SEARCH_PATHS = ( 950 | "$(inherited)", 951 | "@executable_path/Frameworks", 952 | ); 953 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-base"; 954 | PRODUCT_NAME = "$(TARGET_NAME)"; 955 | SWIFT_VERSION = 5.0; 956 | TARGETED_DEVICE_FAMILY = "1,2"; 957 | }; 958 | name = UITest; 959 | }; 960 | 073CF2B124ABB0FF00885A4D /* UITest */ = { 961 | isa = XCBuildConfiguration; 962 | baseConfigurationReference = 9A0F24E413CA6A32C483EDA7 /* Pods-swift-ui-base-swift-ui-baseTests.uitest.xcconfig */; 963 | buildSettings = { 964 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 965 | BUNDLE_LOADER = "$(TEST_HOST)"; 966 | CLANG_ENABLE_MODULES = YES; 967 | CODE_SIGN_STYLE = Automatic; 968 | DEVELOPMENT_TEAM = V9839GT2TL; 969 | INFOPLIST_FILE = "swift-ui-baseTests/Info.plist"; 970 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 971 | LD_RUNPATH_SEARCH_PATHS = ( 972 | "$(inherited)", 973 | "@executable_path/Frameworks", 974 | "@loader_path/Frameworks", 975 | ); 976 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseTests"; 977 | PRODUCT_NAME = "$(TARGET_NAME)"; 978 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 979 | SWIFT_VERSION = 5.0; 980 | TARGETED_DEVICE_FAMILY = "1,2"; 981 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/swift-ui-base.app/swift-ui-base"; 982 | }; 983 | name = UITest; 984 | }; 985 | 073CF2B224ABB0FF00885A4D /* UITest */ = { 986 | isa = XCBuildConfiguration; 987 | baseConfigurationReference = FFF599804C76164B16B725B4 /* Pods-swift-ui-base-swift-ui-baseUITests.uitest.xcconfig */; 988 | buildSettings = { 989 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 990 | CODE_SIGN_STYLE = Automatic; 991 | DEVELOPMENT_TEAM = V9839GT2TL; 992 | INFOPLIST_FILE = "swift-ui-baseUITests/Info.plist"; 993 | IPHONEOS_DEPLOYMENT_TARGET = 13.3; 994 | LD_RUNPATH_SEARCH_PATHS = ( 995 | "$(inherited)", 996 | "@executable_path/Frameworks", 997 | "@loader_path/Frameworks", 998 | ); 999 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseUITests"; 1000 | PRODUCT_NAME = "$(TARGET_NAME)"; 1001 | SWIFT_VERSION = 5.0; 1002 | TARGETED_DEVICE_FAMILY = "1,2"; 1003 | TEST_TARGET_NAME = "swift-ui-base"; 1004 | }; 1005 | name = UITest; 1006 | }; 1007 | 07A64A202417E13B003C6753 /* Debug */ = { 1008 | isa = XCBuildConfiguration; 1009 | buildSettings = { 1010 | ALWAYS_SEARCH_USER_PATHS = NO; 1011 | CLANG_ANALYZER_NONNULL = YES; 1012 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1013 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1014 | CLANG_CXX_LIBRARY = "libc++"; 1015 | CLANG_ENABLE_MODULES = YES; 1016 | CLANG_ENABLE_OBJC_ARC = YES; 1017 | CLANG_ENABLE_OBJC_WEAK = YES; 1018 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1019 | CLANG_WARN_BOOL_CONVERSION = YES; 1020 | CLANG_WARN_COMMA = YES; 1021 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1022 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1023 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1024 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1025 | CLANG_WARN_EMPTY_BODY = YES; 1026 | CLANG_WARN_ENUM_CONVERSION = YES; 1027 | CLANG_WARN_INFINITE_RECURSION = YES; 1028 | CLANG_WARN_INT_CONVERSION = YES; 1029 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1030 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1031 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1032 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1033 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1034 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1035 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1036 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1037 | CLANG_WARN_UNREACHABLE_CODE = YES; 1038 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1039 | COPY_PHASE_STRIP = NO; 1040 | DEBUG_INFORMATION_FORMAT = dwarf; 1041 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1042 | ENABLE_TESTABILITY = YES; 1043 | GCC_C_LANGUAGE_STANDARD = gnu11; 1044 | GCC_DYNAMIC_NO_PIC = NO; 1045 | GCC_NO_COMMON_BLOCKS = YES; 1046 | GCC_OPTIMIZATION_LEVEL = 0; 1047 | GCC_PREPROCESSOR_DEFINITIONS = ( 1048 | "DEBUG=1", 1049 | "$(inherited)", 1050 | ); 1051 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1052 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1053 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1054 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1055 | GCC_WARN_UNUSED_FUNCTION = YES; 1056 | GCC_WARN_UNUSED_VARIABLE = YES; 1057 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1058 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 1059 | MTL_FAST_MATH = YES; 1060 | ONLY_ACTIVE_ARCH = YES; 1061 | SDKROOT = iphoneos; 1062 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 1063 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1064 | }; 1065 | name = Debug; 1066 | }; 1067 | 07A64A212417E13B003C6753 /* Release */ = { 1068 | isa = XCBuildConfiguration; 1069 | buildSettings = { 1070 | ALWAYS_SEARCH_USER_PATHS = NO; 1071 | CLANG_ANALYZER_NONNULL = YES; 1072 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 1073 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 1074 | CLANG_CXX_LIBRARY = "libc++"; 1075 | CLANG_ENABLE_MODULES = YES; 1076 | CLANG_ENABLE_OBJC_ARC = YES; 1077 | CLANG_ENABLE_OBJC_WEAK = YES; 1078 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 1079 | CLANG_WARN_BOOL_CONVERSION = YES; 1080 | CLANG_WARN_COMMA = YES; 1081 | CLANG_WARN_CONSTANT_CONVERSION = YES; 1082 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 1083 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 1084 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 1085 | CLANG_WARN_EMPTY_BODY = YES; 1086 | CLANG_WARN_ENUM_CONVERSION = YES; 1087 | CLANG_WARN_INFINITE_RECURSION = YES; 1088 | CLANG_WARN_INT_CONVERSION = YES; 1089 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 1090 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 1091 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 1092 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 1093 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 1094 | CLANG_WARN_STRICT_PROTOTYPES = YES; 1095 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 1096 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 1097 | CLANG_WARN_UNREACHABLE_CODE = YES; 1098 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 1099 | COPY_PHASE_STRIP = NO; 1100 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 1101 | ENABLE_NS_ASSERTIONS = NO; 1102 | ENABLE_STRICT_OBJC_MSGSEND = YES; 1103 | GCC_C_LANGUAGE_STANDARD = gnu11; 1104 | GCC_NO_COMMON_BLOCKS = YES; 1105 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 1106 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 1107 | GCC_WARN_UNDECLARED_SELECTOR = YES; 1108 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 1109 | GCC_WARN_UNUSED_FUNCTION = YES; 1110 | GCC_WARN_UNUSED_VARIABLE = YES; 1111 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1112 | MTL_ENABLE_DEBUG_INFO = NO; 1113 | MTL_FAST_MATH = YES; 1114 | SDKROOT = iphoneos; 1115 | SWIFT_COMPILATION_MODE = wholemodule; 1116 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 1117 | VALIDATE_PRODUCT = YES; 1118 | }; 1119 | name = Release; 1120 | }; 1121 | 07A64A232417E13B003C6753 /* Debug */ = { 1122 | isa = XCBuildConfiguration; 1123 | baseConfigurationReference = 1D5A66BD2D392E3008B86AE5 /* Pods-swift-ui-base.debug.xcconfig */; 1124 | buildSettings = { 1125 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 1126 | BASE_URL = "https://ios-bases-api.herokuapp.com/api/v1"; 1127 | CODE_SIGN_STYLE = Automatic; 1128 | DEVELOPMENT_ASSET_PATHS = "\"swift-ui-base/Preview Content\""; 1129 | DEVELOPMENT_TEAM = V9839GT2TL; 1130 | ENABLE_PREVIEWS = YES; 1131 | INFOPLIST_FILE = "swift-ui-base/Info.plist"; 1132 | LD_RUNPATH_SEARCH_PATHS = ( 1133 | "$(inherited)", 1134 | "@executable_path/Frameworks", 1135 | ); 1136 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-base"; 1137 | PRODUCT_NAME = "$(TARGET_NAME)"; 1138 | SWIFT_VERSION = 5.0; 1139 | TARGETED_DEVICE_FAMILY = "1,2"; 1140 | }; 1141 | name = Debug; 1142 | }; 1143 | 07A64A242417E13B003C6753 /* Release */ = { 1144 | isa = XCBuildConfiguration; 1145 | baseConfigurationReference = 7E03CB48557E6A834D246B24 /* Pods-swift-ui-base.release.xcconfig */; 1146 | buildSettings = { 1147 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 1148 | BASE_URL = "https://ios-bases-api.herokuapp.com/api/v1"; 1149 | CODE_SIGN_STYLE = Automatic; 1150 | DEVELOPMENT_ASSET_PATHS = "\"swift-ui-base/Preview Content\""; 1151 | DEVELOPMENT_TEAM = V9839GT2TL; 1152 | ENABLE_PREVIEWS = YES; 1153 | INFOPLIST_FILE = "swift-ui-base/Info.plist"; 1154 | LD_RUNPATH_SEARCH_PATHS = ( 1155 | "$(inherited)", 1156 | "@executable_path/Frameworks", 1157 | ); 1158 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-base"; 1159 | PRODUCT_NAME = "$(TARGET_NAME)"; 1160 | SWIFT_VERSION = 5.0; 1161 | TARGETED_DEVICE_FAMILY = "1,2"; 1162 | }; 1163 | name = Release; 1164 | }; 1165 | 07A64A262417E13B003C6753 /* Debug */ = { 1166 | isa = XCBuildConfiguration; 1167 | baseConfigurationReference = B06E4CC4A34CD8B26B3128AF /* Pods-swift-ui-base-swift-ui-baseTests.debug.xcconfig */; 1168 | buildSettings = { 1169 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1170 | BUNDLE_LOADER = "$(TEST_HOST)"; 1171 | CLANG_ENABLE_MODULES = YES; 1172 | CODE_SIGN_STYLE = Automatic; 1173 | DEVELOPMENT_TEAM = V9839GT2TL; 1174 | INFOPLIST_FILE = "swift-ui-baseTests/Info.plist"; 1175 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1176 | LD_RUNPATH_SEARCH_PATHS = ( 1177 | "$(inherited)", 1178 | "@executable_path/Frameworks", 1179 | "@loader_path/Frameworks", 1180 | ); 1181 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseTests"; 1182 | PRODUCT_NAME = "$(TARGET_NAME)"; 1183 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1184 | SWIFT_VERSION = 5.0; 1185 | TARGETED_DEVICE_FAMILY = "1,2"; 1186 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/swift-ui-base.app/swift-ui-base"; 1187 | }; 1188 | name = Debug; 1189 | }; 1190 | 07A64A272417E13B003C6753 /* Release */ = { 1191 | isa = XCBuildConfiguration; 1192 | baseConfigurationReference = B1D57A97908D6E25EB01EAF7 /* Pods-swift-ui-base-swift-ui-baseTests.release.xcconfig */; 1193 | buildSettings = { 1194 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1195 | BUNDLE_LOADER = "$(TEST_HOST)"; 1196 | CLANG_ENABLE_MODULES = YES; 1197 | CODE_SIGN_STYLE = Automatic; 1198 | DEVELOPMENT_TEAM = V9839GT2TL; 1199 | INFOPLIST_FILE = "swift-ui-baseTests/Info.plist"; 1200 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 1201 | LD_RUNPATH_SEARCH_PATHS = ( 1202 | "$(inherited)", 1203 | "@executable_path/Frameworks", 1204 | "@loader_path/Frameworks", 1205 | ); 1206 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseTests"; 1207 | PRODUCT_NAME = "$(TARGET_NAME)"; 1208 | SWIFT_VERSION = 5.0; 1209 | TARGETED_DEVICE_FAMILY = "1,2"; 1210 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/swift-ui-base.app/swift-ui-base"; 1211 | }; 1212 | name = Release; 1213 | }; 1214 | 07A64A292417E13B003C6753 /* Debug */ = { 1215 | isa = XCBuildConfiguration; 1216 | baseConfigurationReference = 7EC313E43672C416F121D319 /* Pods-swift-ui-base-swift-ui-baseUITests.debug.xcconfig */; 1217 | buildSettings = { 1218 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1219 | CODE_SIGN_STYLE = Automatic; 1220 | DEVELOPMENT_TEAM = V9839GT2TL; 1221 | INFOPLIST_FILE = "swift-ui-baseUITests/Info.plist"; 1222 | IPHONEOS_DEPLOYMENT_TARGET = 13.3; 1223 | LD_RUNPATH_SEARCH_PATHS = ( 1224 | "$(inherited)", 1225 | "@executable_path/Frameworks", 1226 | "@loader_path/Frameworks", 1227 | ); 1228 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseUITests"; 1229 | PRODUCT_NAME = "$(TARGET_NAME)"; 1230 | SWIFT_VERSION = 5.0; 1231 | TARGETED_DEVICE_FAMILY = "1,2"; 1232 | TEST_TARGET_NAME = "swift-ui-base"; 1233 | }; 1234 | name = Debug; 1235 | }; 1236 | 07A64A2A2417E13B003C6753 /* Release */ = { 1237 | isa = XCBuildConfiguration; 1238 | baseConfigurationReference = D77EFC245B3FA3AAB543A6A3 /* Pods-swift-ui-base-swift-ui-baseUITests.release.xcconfig */; 1239 | buildSettings = { 1240 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1241 | CODE_SIGN_STYLE = Automatic; 1242 | DEVELOPMENT_TEAM = V9839GT2TL; 1243 | INFOPLIST_FILE = "swift-ui-baseUITests/Info.plist"; 1244 | IPHONEOS_DEPLOYMENT_TARGET = 13.3; 1245 | LD_RUNPATH_SEARCH_PATHS = ( 1246 | "$(inherited)", 1247 | "@executable_path/Frameworks", 1248 | "@loader_path/Frameworks", 1249 | ); 1250 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootstrap.swift-ui-baseUITests"; 1251 | PRODUCT_NAME = "$(TARGET_NAME)"; 1252 | SWIFT_VERSION = 5.0; 1253 | TARGETED_DEVICE_FAMILY = "1,2"; 1254 | TEST_TARGET_NAME = "swift-ui-base"; 1255 | }; 1256 | name = Release; 1257 | }; 1258 | /* End XCBuildConfiguration section */ 1259 | 1260 | /* Begin XCConfigurationList section */ 1261 | 07A649F32417E137003C6753 /* Build configuration list for PBXProject "swift-ui-base" */ = { 1262 | isa = XCConfigurationList; 1263 | buildConfigurations = ( 1264 | 07A64A202417E13B003C6753 /* Debug */, 1265 | 073CF2AF24ABB0FF00885A4D /* UITest */, 1266 | 07A64A212417E13B003C6753 /* Release */, 1267 | ); 1268 | defaultConfigurationIsVisible = 0; 1269 | defaultConfigurationName = Release; 1270 | }; 1271 | 07A64A222417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-base" */ = { 1272 | isa = XCConfigurationList; 1273 | buildConfigurations = ( 1274 | 07A64A232417E13B003C6753 /* Debug */, 1275 | 073CF2B024ABB0FF00885A4D /* UITest */, 1276 | 07A64A242417E13B003C6753 /* Release */, 1277 | ); 1278 | defaultConfigurationIsVisible = 0; 1279 | defaultConfigurationName = Release; 1280 | }; 1281 | 07A64A252417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-baseTests" */ = { 1282 | isa = XCConfigurationList; 1283 | buildConfigurations = ( 1284 | 07A64A262417E13B003C6753 /* Debug */, 1285 | 073CF2B124ABB0FF00885A4D /* UITest */, 1286 | 07A64A272417E13B003C6753 /* Release */, 1287 | ); 1288 | defaultConfigurationIsVisible = 0; 1289 | defaultConfigurationName = Release; 1290 | }; 1291 | 07A64A282417E13B003C6753 /* Build configuration list for PBXNativeTarget "swift-ui-baseUITests" */ = { 1292 | isa = XCConfigurationList; 1293 | buildConfigurations = ( 1294 | 07A64A292417E13B003C6753 /* Debug */, 1295 | 073CF2B224ABB0FF00885A4D /* UITest */, 1296 | 07A64A2A2417E13B003C6753 /* Release */, 1297 | ); 1298 | defaultConfigurationIsVisible = 0; 1299 | defaultConfigurationName = Release; 1300 | }; 1301 | /* End XCConfigurationList section */ 1302 | }; 1303 | rootObject = 07A649F02417E137003C6753 /* Project object */; 1304 | } 1305 | --------------------------------------------------------------------------------