├── .gitignore ├── 99_Stocks.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── 99_Stocks ├── 3rdPartyLibs │ └── SwiftChart │ │ ├── Chart.swift │ │ ├── ChartColors.swift │ │ └── ChartSeries.swift ├── 99_Stocks.entitlements ├── Extensions │ ├── Double+Currency.swift │ └── URLSession+Combine.swift ├── GeneratedCode │ └── APIConfigurationSourcery.generated.swift ├── Info.plist ├── Modules │ ├── -App │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── SceneDelegate.swift │ │ └── SceneRouter.swift │ ├── Companies │ │ ├── Company.swift │ │ ├── Models │ │ │ ├── Company.Model.StockName.swift │ │ │ ├── Company.Model.StockState.swift │ │ │ ├── Country.Model.Country.swift │ │ │ ├── DetailItem │ │ │ │ ├── Company.Model.DetailItem.swift │ │ │ │ └── Company.Model.DetailItemResponse.swift │ │ │ └── ItemList │ │ │ │ ├── Company.Model.ItemList.swift │ │ │ │ └── Company.Model.ItemListResponse.swift │ │ ├── Network │ │ │ ├── Company.Network.APIClient.swift │ │ │ └── Company.Network.Endpoint.swift │ │ └── SubModules │ │ │ ├── DetailItem │ │ │ ├── Company.Router.DetailItem.swift │ │ │ ├── Company.View.DetailItem.swift │ │ │ └── Company.ViewModel.DetailItem.swift │ │ │ ├── ItemList │ │ │ ├── Company.Router.ItemList.swift │ │ │ ├── Company.View.ItemList.swift │ │ │ ├── Company.ViewModel.ItemList.swift │ │ │ └── SupportingViews │ │ │ │ └── CompaniesListItemView.swift │ │ │ └── SupportingViews │ │ │ └── ChartView.swift │ └── General │ │ └── SupportingViews │ │ └── ActivityIndicatorView.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon108x108@2x.png │ │ │ ├── AppIcon24x24@2x.png │ │ │ ├── AppIcon27.5x27.5@2x.png │ │ │ ├── AppIcon29x29@2x.png │ │ │ ├── AppIcon29x29@3x.png │ │ │ ├── AppIcon40x40@2x.png │ │ │ ├── AppIcon44x44@2x.png │ │ │ ├── AppIcon50x50@2x.png │ │ │ ├── AppIcon86x86@2x.png │ │ │ ├── AppIcon98x98@2x.png │ │ │ ├── Contents.json │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon-76@2x.png │ │ │ ├── Icon-83.5@2x.png │ │ │ ├── Icon-Notification.png │ │ │ ├── Icon-Notification@2x.png │ │ │ ├── Icon-Notification@3x.png │ │ │ ├── Icon-Small-40.png │ │ │ ├── Icon-Small-40@2x.png │ │ │ ├── Icon-Small-40@3x.png │ │ │ ├── Icon-Small.png │ │ │ ├── Icon-Small@2x.png │ │ │ ├── Icon-Small@3x.png │ │ │ ├── icon.png │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ ├── icon_512x512@2x.png │ │ │ └── watchicon.png │ │ ├── Colors │ │ │ ├── Companies │ │ │ │ ├── Contents.json │ │ │ │ └── Currency │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── down.colorset │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── neutral.colorset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── up.colorset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── Images │ │ │ ├── Companies │ │ │ ├── Contents.json │ │ │ ├── alibaba.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── alibaba.jpg │ │ │ ├── alphabet.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── alphabet.jpg │ │ │ ├── amazon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── amzn.png │ │ │ ├── apple.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── apple.png │ │ │ ├── berkshire.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── berkshire.jpg │ │ │ ├── facebook.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── fb.jpg │ │ │ ├── johnson.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── johnson.jpg │ │ │ ├── jpmorgan.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── jpmorgan.jpg │ │ │ └── microsoft.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── microsoft.png │ │ │ ├── Contents.json │ │ │ └── StocksIcon.imageset │ │ │ ├── Contents.json │ │ │ └── StocksIcon.png │ ├── Colors.swift │ ├── Images.swift │ ├── Localization.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── en.lproj │ │ └── Localizable.strings │ └── es.lproj │ │ └── Localizable.strings └── Utils │ ├── Network │ ├── APIClient.swift │ ├── APIConfiguration.swift │ └── NetworkHelpers.swift │ └── PropertyWrappers │ ├── Asset.swift │ └── SystemImage.swift ├── 99_StocksTests ├── Info.plist └── StocksTests.swift ├── APIRequestsTestPlan.xctestplan ├── Generation ├── .sourcery.yml ├── Localization │ ├── CSV2Localizables │ ├── CSV2Localizables.swift │ ├── Localizable.csv │ └── Localizable.numbers ├── sourcery │ ├── APIConfig │ │ ├── APIConfigurationSourcery.swifttemplate │ │ └── Utils.swifttemplate │ └── swiftgen │ │ └── structured-swift5.stencil └── swiftgen.yml ├── LICENSE ├── README.md ├── StocksUITests ├── Info.plist └── StocksUITests.swift └── github └── screenshots.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | # Edit at https://www.gitignore.io/?templates=swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata/ 24 | 25 | ## Other 26 | *.moved-aside 27 | *.xccheckout 28 | *.xcscmblueprint 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | .build/ 47 | 48 | # CocoaPods 49 | # 50 | # We recommend against adding the Pods directory to your .gitignore. However 51 | # you should judge for yourself, the pros and cons are mentioned at: 52 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 53 | # 54 | # Pods/ 55 | # 56 | # Add this line if you want to avoid checking in source code from the Xcode workspace 57 | # *.xcworkspace 58 | 59 | # Carthage 60 | # 61 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 62 | # Carthage/Checkouts 63 | 64 | Carthage/Build 65 | 66 | # fastlane 67 | # 68 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 69 | # screenshots whenever they are needed. 70 | # For more information about the recommended setup visit: 71 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 72 | 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots/**/*.png 76 | fastlane/test_output 77 | 78 | # Code Injection 79 | # 80 | # After new code Injection tools there's a generated folder /iOSInjectionProject 81 | # https://github.com/johnno1962/injectionforxcode 82 | 83 | iOSInjectionProject/ 84 | 85 | # End of https://www.gitignore.io/api/swift 86 | 87 | .idea 88 | .idea/* -------------------------------------------------------------------------------- /99_Stocks.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6549D0F922AD9A9800E32447 /* APIConfigurationSourcery.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6549D0F822AD9A9800E32447 /* APIConfigurationSourcery.generated.swift */; }; 11 | 65736AD822AB1490009A51F2 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AD722AB1490009A51F2 /* ChartView.swift */; }; 12 | 65736ADB22ABA6A4009A51F2 /* Double+Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736ADA22ABA6A4009A51F2 /* Double+Currency.swift */; }; 13 | 65736ADF22ABAA92009A51F2 /* Asset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736ADE22ABAA92009A51F2 /* Asset.swift */; }; 14 | 65736AE322ABB0CB009A51F2 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AE222ABB0CB009A51F2 /* Colors.swift */; }; 15 | 65736AE522ABB285009A51F2 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AE422ABB285009A51F2 /* Images.swift */; }; 16 | 65736AE822ABB59B009A51F2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 65736AEA22ABB59B009A51F2 /* Localizable.strings */; }; 17 | 65736AED22ABBBDE009A51F2 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AEC22ABBBDE009A51F2 /* Localization.swift */; }; 18 | 65736AF322ABC521009A51F2 /* Company.Model.ItemListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AF222ABC521009A51F2 /* Company.Model.ItemListResponse.swift */; }; 19 | 65736AF522ABC52E009A51F2 /* Company.Model.DetailItemResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AF422ABC52E009A51F2 /* Company.Model.DetailItemResponse.swift */; }; 20 | 65736AF922ABC87E009A51F2 /* Company.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AF822ABC87E009A51F2 /* Company.swift */; }; 21 | 65736AFB22ABC8A9009A51F2 /* Company.Network.APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AFA22ABC8A9009A51F2 /* Company.Network.APIClient.swift */; }; 22 | 65736AFD22ABC8C1009A51F2 /* Company.Network.Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AFC22ABC8C1009A51F2 /* Company.Network.Endpoint.swift */; }; 23 | 65736AFF22ABC91C009A51F2 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736AFE22ABC91C009A51F2 /* APIClient.swift */; }; 24 | 65736B0222ABCA27009A51F2 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736B0122ABCA27009A51F2 /* APIConfiguration.swift */; }; 25 | 65736B0A22ABD645009A51F2 /* URLSession+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736B0622ABD645009A51F2 /* URLSession+Combine.swift */; }; 26 | 65736B0F22AC073F009A51F2 /* Company.ViewModel.ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736B0E22AC073F009A51F2 /* Company.ViewModel.ItemList.swift */; }; 27 | 65736B1122AC1039009A51F2 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65736B1022AC1039009A51F2 /* ActivityIndicatorView.swift */; }; 28 | 658E93AC22AD6A36008CCC32 /* StocksUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658E93AB22AD6A36008CCC32 /* StocksUITests.swift */; }; 29 | 659747B722AC3B79004277E3 /* Company.Router.ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659747B622AC3B79004277E3 /* Company.Router.ItemList.swift */; }; 30 | 659747B922AC3BDD004277E3 /* Company.Router.DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659747B822AC3BDD004277E3 /* Company.Router.DetailItem.swift */; }; 31 | 659747BB22AC4D10004277E3 /* Company.ViewModel.DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659747BA22AC4D0F004277E3 /* Company.ViewModel.DetailItem.swift */; }; 32 | 65AAE53022AAD3C500344B49 /* CompaniesListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE52F22AAD3C500344B49 /* CompaniesListItemView.swift */; }; 33 | 65AAE53322AAD40B00344B49 /* Company.Model.ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53222AAD40B00344B49 /* Company.Model.ItemList.swift */; }; 34 | 65AAE53722AAE02B00344B49 /* Company.View.DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53622AAE02B00344B49 /* Company.View.DetailItem.swift */; }; 35 | 65AAE53922AAE5F100344B49 /* Company.Model.DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53822AAE5F100344B49 /* Company.Model.DetailItem.swift */; }; 36 | 65AAE53E22AB047C00344B49 /* ChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53B22AB047C00344B49 /* ChartSeries.swift */; }; 37 | 65AAE53F22AB047C00344B49 /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53C22AB047C00344B49 /* Chart.swift */; }; 38 | 65AAE54022AB047C00344B49 /* ChartColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAE53D22AB047C00344B49 /* ChartColors.swift */; }; 39 | 65B48AA122AAC72900BE9832 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B48AA022AAC72900BE9832 /* AppDelegate.swift */; }; 40 | 65B48AA322AAC72900BE9832 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B48AA222AAC72900BE9832 /* SceneDelegate.swift */; }; 41 | 65B48AA522AAC72900BE9832 /* Company.View.ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B48AA422AAC72900BE9832 /* Company.View.ItemList.swift */; }; 42 | 65B48AA722AAC72A00BE9832 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65B48AA622AAC72A00BE9832 /* Assets.xcassets */; }; 43 | 65B48AAA22AAC72A00BE9832 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65B48AA922AAC72A00BE9832 /* Preview Assets.xcassets */; }; 44 | 65B48AAD22AAC72A00BE9832 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65B48AAB22AAC72A00BE9832 /* LaunchScreen.storyboard */; }; 45 | 65B48AB822AAC72A00BE9832 /* StocksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B48AB722AAC72A00BE9832 /* StocksTests.swift */; }; 46 | 65B48AC522AAC7E600BE9832 /* SceneRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B48AC422AAC7E600BE9832 /* SceneRouter.swift */; }; 47 | 65E11C4D22AD27D30015BFC5 /* SystemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E11C4C22AD27D30015BFC5 /* SystemImage.swift */; }; 48 | 65E11C5122AD37890015BFC5 /* NetworkHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E11C5022AD37890015BFC5 /* NetworkHelpers.swift */; }; 49 | 65E11C5322AD37FC0015BFC5 /* Company.Model.StockName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E11C5222AD37FC0015BFC5 /* Company.Model.StockName.swift */; }; 50 | 65E11C5522AD381B0015BFC5 /* Country.Model.Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E11C5422AD381B0015BFC5 /* Country.Model.Country.swift */; }; 51 | 65E11C5822AD3A4A0015BFC5 /* Company.Model.StockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E11C5722AD3A4A0015BFC5 /* Company.Model.StockState.swift */; }; 52 | /* End PBXBuildFile section */ 53 | 54 | /* Begin PBXContainerItemProxy section */ 55 | 658E93AE22AD6A36008CCC32 /* PBXContainerItemProxy */ = { 56 | isa = PBXContainerItemProxy; 57 | containerPortal = 65B48A9522AAC72900BE9832 /* Project object */; 58 | proxyType = 1; 59 | remoteGlobalIDString = 65B48A9C22AAC72900BE9832; 60 | remoteInfo = 99_Stocks; 61 | }; 62 | 65B48AB422AAC72A00BE9832 /* PBXContainerItemProxy */ = { 63 | isa = PBXContainerItemProxy; 64 | containerPortal = 65B48A9522AAC72900BE9832 /* Project object */; 65 | proxyType = 1; 66 | remoteGlobalIDString = 65B48A9C22AAC72900BE9832; 67 | remoteInfo = 99_Companies; 68 | }; 69 | /* End PBXContainerItemProxy section */ 70 | 71 | /* Begin PBXFileReference section */ 72 | 6549D0F822AD9A9800E32447 /* APIConfigurationSourcery.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIConfigurationSourcery.generated.swift; sourceTree = ""; }; 73 | 65736AD722AB1490009A51F2 /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 74 | 65736ADA22ABA6A4009A51F2 /* Double+Currency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Currency.swift"; sourceTree = ""; }; 75 | 65736ADE22ABAA92009A51F2 /* Asset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Asset.swift; sourceTree = ""; }; 76 | 65736AE222ABB0CB009A51F2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 77 | 65736AE422ABB285009A51F2 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; 78 | 65736AE922ABB59B009A51F2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 79 | 65736AEB22ABB5A5009A51F2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 80 | 65736AEC22ABBBDE009A51F2 /* Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 81 | 65736AF222ABC521009A51F2 /* Company.Model.ItemListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.ItemListResponse.swift; sourceTree = ""; }; 82 | 65736AF422ABC52E009A51F2 /* Company.Model.DetailItemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.DetailItemResponse.swift; sourceTree = ""; }; 83 | 65736AF822ABC87E009A51F2 /* Company.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.swift; sourceTree = ""; }; 84 | 65736AFA22ABC8A9009A51F2 /* Company.Network.APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Network.APIClient.swift; sourceTree = ""; }; 85 | 65736AFC22ABC8C1009A51F2 /* Company.Network.Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Network.Endpoint.swift; sourceTree = ""; }; 86 | 65736AFE22ABC91C009A51F2 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; 87 | 65736B0122ABCA27009A51F2 /* APIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfiguration.swift; sourceTree = ""; }; 88 | 65736B0622ABD645009A51F2 /* URLSession+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLSession+Combine.swift"; sourceTree = ""; }; 89 | 65736B0E22AC073F009A51F2 /* Company.ViewModel.ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.ViewModel.ItemList.swift; sourceTree = ""; }; 90 | 65736B1022AC1039009A51F2 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; 91 | 658E93A022AD6850008CCC32 /* APIRequestsTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = APIRequestsTestPlan.xctestplan; sourceTree = ""; }; 92 | 658E93A922AD6A36008CCC32 /* StocksUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StocksUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 93 | 658E93AB22AD6A36008CCC32 /* StocksUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StocksUITests.swift; sourceTree = ""; }; 94 | 658E93AD22AD6A36008CCC32 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 659747B622AC3B79004277E3 /* Company.Router.ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Router.ItemList.swift; sourceTree = ""; }; 96 | 659747B822AC3BDD004277E3 /* Company.Router.DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Router.DetailItem.swift; sourceTree = ""; }; 97 | 659747BA22AC4D0F004277E3 /* Company.ViewModel.DetailItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Company.ViewModel.DetailItem.swift; sourceTree = ""; }; 98 | 659BA007234BFBFF00D1CDE5 /* 99_Stocks.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = 99_Stocks.entitlements; sourceTree = ""; }; 99 | 65AAE52F22AAD3C500344B49 /* CompaniesListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompaniesListItemView.swift; sourceTree = ""; }; 100 | 65AAE53222AAD40B00344B49 /* Company.Model.ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.ItemList.swift; sourceTree = ""; }; 101 | 65AAE53622AAE02B00344B49 /* Company.View.DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.View.DetailItem.swift; sourceTree = ""; }; 102 | 65AAE53822AAE5F100344B49 /* Company.Model.DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.DetailItem.swift; sourceTree = ""; }; 103 | 65AAE53B22AB047C00344B49 /* ChartSeries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartSeries.swift; sourceTree = ""; }; 104 | 65AAE53C22AB047C00344B49 /* Chart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; 105 | 65AAE53D22AB047C00344B49 /* ChartColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartColors.swift; sourceTree = ""; }; 106 | 65B48A9D22AAC72900BE9832 /* 99_Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 99_Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; }; 107 | 65B48AA022AAC72900BE9832 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 108 | 65B48AA222AAC72900BE9832 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 109 | 65B48AA422AAC72900BE9832 /* Company.View.ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.View.ItemList.swift; sourceTree = ""; }; 110 | 65B48AA622AAC72A00BE9832 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 111 | 65B48AA922AAC72A00BE9832 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 112 | 65B48AAC22AAC72A00BE9832 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 113 | 65B48AAE22AAC72A00BE9832 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 114 | 65B48AB322AAC72A00BE9832 /* 99_StocksTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = 99_StocksTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 115 | 65B48AB722AAC72A00BE9832 /* StocksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StocksTests.swift; sourceTree = ""; }; 116 | 65B48AB922AAC72A00BE9832 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 117 | 65B48AC422AAC7E600BE9832 /* SceneRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneRouter.swift; sourceTree = ""; }; 118 | 65E11C4C22AD27D30015BFC5 /* SystemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImage.swift; sourceTree = ""; }; 119 | 65E11C5022AD37890015BFC5 /* NetworkHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHelpers.swift; sourceTree = ""; }; 120 | 65E11C5222AD37FC0015BFC5 /* Company.Model.StockName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.StockName.swift; sourceTree = ""; }; 121 | 65E11C5422AD381B0015BFC5 /* Country.Model.Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.Model.Country.swift; sourceTree = ""; }; 122 | 65E11C5722AD3A4A0015BFC5 /* Company.Model.StockState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.Model.StockState.swift; sourceTree = ""; }; 123 | /* End PBXFileReference section */ 124 | 125 | /* Begin PBXFrameworksBuildPhase section */ 126 | 658E93A622AD6A36008CCC32 /* Frameworks */ = { 127 | isa = PBXFrameworksBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | 65B48A9A22AAC72900BE9832 /* Frameworks */ = { 134 | isa = PBXFrameworksBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | 65B48AB022AAC72A00BE9832 /* Frameworks */ = { 141 | isa = PBXFrameworksBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXFrameworksBuildPhase section */ 148 | 149 | /* Begin PBXGroup section */ 150 | 6549D0F722AD984000E32447 /* GeneratedCode */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 6549D0F822AD9A9800E32447 /* APIConfigurationSourcery.generated.swift */, 154 | ); 155 | path = GeneratedCode; 156 | sourceTree = ""; 157 | }; 158 | 65736AD622AB1482009A51F2 /* SupportingViews */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 65736AD722AB1490009A51F2 /* ChartView.swift */, 162 | ); 163 | path = SupportingViews; 164 | sourceTree = ""; 165 | }; 166 | 65736AD922ABA682009A51F2 /* Extensions */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 65736B0622ABD645009A51F2 /* URLSession+Combine.swift */, 170 | 65736ADA22ABA6A4009A51F2 /* Double+Currency.swift */, 171 | ); 172 | path = Extensions; 173 | sourceTree = ""; 174 | }; 175 | 65736ADC22ABA934009A51F2 /* Utils */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 65736AF622ABC7DB009A51F2 /* Network */, 179 | 65736ADD22ABA93A009A51F2 /* PropertyWrappers */, 180 | ); 181 | path = Utils; 182 | sourceTree = ""; 183 | }; 184 | 65736ADD22ABA93A009A51F2 /* PropertyWrappers */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 65736ADE22ABAA92009A51F2 /* Asset.swift */, 188 | 65E11C4C22AD27D30015BFC5 /* SystemImage.swift */, 189 | ); 190 | path = PropertyWrappers; 191 | sourceTree = ""; 192 | }; 193 | 65736AE122ABB0BD009A51F2 /* Resources */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 65B48AA822AAC72A00BE9832 /* Preview Content */, 197 | 65B48AA622AAC72A00BE9832 /* Assets.xcassets */, 198 | 65736AE222ABB0CB009A51F2 /* Colors.swift */, 199 | 65736AE422ABB285009A51F2 /* Images.swift */, 200 | 65736AEC22ABBBDE009A51F2 /* Localization.swift */, 201 | 65736AEA22ABB59B009A51F2 /* Localizable.strings */, 202 | ); 203 | path = Resources; 204 | sourceTree = ""; 205 | }; 206 | 65736AEF22ABC4C6009A51F2 /* Models */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 65736AF022ABC509009A51F2 /* ItemList */, 210 | 65736AF122ABC50E009A51F2 /* DetailItem */, 211 | 65E11C5222AD37FC0015BFC5 /* Company.Model.StockName.swift */, 212 | 65E11C5422AD381B0015BFC5 /* Country.Model.Country.swift */, 213 | 65E11C5722AD3A4A0015BFC5 /* Company.Model.StockState.swift */, 214 | ); 215 | path = Models; 216 | sourceTree = ""; 217 | }; 218 | 65736AF022ABC509009A51F2 /* ItemList */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 65736AF222ABC521009A51F2 /* Company.Model.ItemListResponse.swift */, 222 | 65AAE53222AAD40B00344B49 /* Company.Model.ItemList.swift */, 223 | ); 224 | path = ItemList; 225 | sourceTree = ""; 226 | }; 227 | 65736AF122ABC50E009A51F2 /* DetailItem */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 65736AF422ABC52E009A51F2 /* Company.Model.DetailItemResponse.swift */, 231 | 65AAE53822AAE5F100344B49 /* Company.Model.DetailItem.swift */, 232 | ); 233 | path = DetailItem; 234 | sourceTree = ""; 235 | }; 236 | 65736AF622ABC7DB009A51F2 /* Network */ = { 237 | isa = PBXGroup; 238 | children = ( 239 | 65736AFE22ABC91C009A51F2 /* APIClient.swift */, 240 | 65736B0122ABCA27009A51F2 /* APIConfiguration.swift */, 241 | 65E11C5022AD37890015BFC5 /* NetworkHelpers.swift */, 242 | ); 243 | path = Network; 244 | sourceTree = ""; 245 | }; 246 | 65736AF722ABC871009A51F2 /* Network */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 65736AFC22ABC8C1009A51F2 /* Company.Network.Endpoint.swift */, 250 | 65736AFA22ABC8A9009A51F2 /* Company.Network.APIClient.swift */, 251 | ); 252 | path = Network; 253 | sourceTree = ""; 254 | }; 255 | 658E93AA22AD6A36008CCC32 /* StocksUITests */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | 658E93AB22AD6A36008CCC32 /* StocksUITests.swift */, 259 | 658E93AD22AD6A36008CCC32 /* Info.plist */, 260 | ); 261 | path = StocksUITests; 262 | sourceTree = ""; 263 | }; 264 | 659747B222AC3615004277E3 /* ItemList */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | 65D5084222AD550F00949493 /* SupportingViews */, 268 | 659747B622AC3B79004277E3 /* Company.Router.ItemList.swift */, 269 | 65736B0E22AC073F009A51F2 /* Company.ViewModel.ItemList.swift */, 270 | 65B48AA422AAC72900BE9832 /* Company.View.ItemList.swift */, 271 | ); 272 | path = ItemList; 273 | sourceTree = ""; 274 | }; 275 | 659747B322AC362D004277E3 /* DetailItem */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 659747B822AC3BDD004277E3 /* Company.Router.DetailItem.swift */, 279 | 659747BA22AC4D0F004277E3 /* Company.ViewModel.DetailItem.swift */, 280 | 65AAE53622AAE02B00344B49 /* Company.View.DetailItem.swift */, 281 | ); 282 | path = DetailItem; 283 | sourceTree = ""; 284 | }; 285 | 65AAE53122AAD40000344B49 /* Models */ = { 286 | isa = PBXGroup; 287 | children = ( 288 | ); 289 | path = Models; 290 | sourceTree = ""; 291 | }; 292 | 65AAE53A22AB047C00344B49 /* SwiftChart */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | 65AAE53B22AB047C00344B49 /* ChartSeries.swift */, 296 | 65AAE53C22AB047C00344B49 /* Chart.swift */, 297 | 65AAE53D22AB047C00344B49 /* ChartColors.swift */, 298 | ); 299 | path = SwiftChart; 300 | sourceTree = ""; 301 | }; 302 | 65AAE54122AB048600344B49 /* 3rdPartyLibs */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | 65AAE53A22AB047C00344B49 /* SwiftChart */, 306 | ); 307 | path = 3rdPartyLibs; 308 | sourceTree = ""; 309 | }; 310 | 65B48A9422AAC72900BE9832 = { 311 | isa = PBXGroup; 312 | children = ( 313 | 65B48A9F22AAC72900BE9832 /* 99_Stocks */, 314 | 658E93A022AD6850008CCC32 /* APIRequestsTestPlan.xctestplan */, 315 | 65B48AB622AAC72A00BE9832 /* 99_StocksTests */, 316 | 658E93AA22AD6A36008CCC32 /* StocksUITests */, 317 | 65B48A9E22AAC72900BE9832 /* Products */, 318 | ); 319 | sourceTree = ""; 320 | }; 321 | 65B48A9E22AAC72900BE9832 /* Products */ = { 322 | isa = PBXGroup; 323 | children = ( 324 | 65B48A9D22AAC72900BE9832 /* 99_Stocks.app */, 325 | 65B48AB322AAC72A00BE9832 /* 99_StocksTests.xctest */, 326 | 658E93A922AD6A36008CCC32 /* StocksUITests.xctest */, 327 | ); 328 | name = Products; 329 | sourceTree = ""; 330 | }; 331 | 65B48A9F22AAC72900BE9832 /* 99_Stocks */ = { 332 | isa = PBXGroup; 333 | children = ( 334 | 659BA007234BFBFF00D1CDE5 /* 99_Stocks.entitlements */, 335 | 65AAE54122AB048600344B49 /* 3rdPartyLibs */, 336 | 6549D0F722AD984000E32447 /* GeneratedCode */, 337 | 65736ADC22ABA934009A51F2 /* Utils */, 338 | 65736AE122ABB0BD009A51F2 /* Resources */, 339 | 65736AD922ABA682009A51F2 /* Extensions */, 340 | 65B48AC222AAC76D00BE9832 /* Modules */, 341 | 65B48AAE22AAC72A00BE9832 /* Info.plist */, 342 | ); 343 | path = 99_Stocks; 344 | sourceTree = ""; 345 | }; 346 | 65B48AA822AAC72A00BE9832 /* Preview Content */ = { 347 | isa = PBXGroup; 348 | children = ( 349 | 65B48AA922AAC72A00BE9832 /* Preview Assets.xcassets */, 350 | ); 351 | path = "Preview Content"; 352 | sourceTree = ""; 353 | }; 354 | 65B48AB622AAC72A00BE9832 /* 99_StocksTests */ = { 355 | isa = PBXGroup; 356 | children = ( 357 | 65B48AB722AAC72A00BE9832 /* StocksTests.swift */, 358 | 65B48AB922AAC72A00BE9832 /* Info.plist */, 359 | ); 360 | path = 99_StocksTests; 361 | sourceTree = ""; 362 | }; 363 | 65B48AC222AAC76D00BE9832 /* Modules */ = { 364 | isa = PBXGroup; 365 | children = ( 366 | 65E11C5C22AD4D770015BFC5 /* -App */, 367 | 65E11C5A22AD4CBA0015BFC5 /* General */, 368 | 65B48AC322AAC79300BE9832 /* Companies */, 369 | ); 370 | path = Modules; 371 | sourceTree = ""; 372 | }; 373 | 65B48AC322AAC79300BE9832 /* Companies */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | 65736AF722ABC871009A51F2 /* Network */, 377 | 65736AEF22ABC4C6009A51F2 /* Models */, 378 | 65E11C5922AD4C470015BFC5 /* SubModules */, 379 | 65736AF822ABC87E009A51F2 /* Company.swift */, 380 | ); 381 | path = Companies; 382 | sourceTree = ""; 383 | }; 384 | 65D5084222AD550F00949493 /* SupportingViews */ = { 385 | isa = PBXGroup; 386 | children = ( 387 | 65AAE52F22AAD3C500344B49 /* CompaniesListItemView.swift */, 388 | ); 389 | path = SupportingViews; 390 | sourceTree = ""; 391 | }; 392 | 65E11C5622AD39EB0015BFC5 /* SupportingViews */ = { 393 | isa = PBXGroup; 394 | children = ( 395 | 65736B1022AC1039009A51F2 /* ActivityIndicatorView.swift */, 396 | ); 397 | path = SupportingViews; 398 | sourceTree = ""; 399 | }; 400 | 65E11C5922AD4C470015BFC5 /* SubModules */ = { 401 | isa = PBXGroup; 402 | children = ( 403 | 65736AD622AB1482009A51F2 /* SupportingViews */, 404 | 659747B222AC3615004277E3 /* ItemList */, 405 | 659747B322AC362D004277E3 /* DetailItem */, 406 | ); 407 | path = SubModules; 408 | sourceTree = ""; 409 | }; 410 | 65E11C5A22AD4CBA0015BFC5 /* General */ = { 411 | isa = PBXGroup; 412 | children = ( 413 | 65E11C5B22AD4CD00015BFC5 /* Network */, 414 | 65AAE53122AAD40000344B49 /* Models */, 415 | 65E11C5622AD39EB0015BFC5 /* SupportingViews */, 416 | ); 417 | path = General; 418 | sourceTree = ""; 419 | }; 420 | 65E11C5B22AD4CD00015BFC5 /* Network */ = { 421 | isa = PBXGroup; 422 | children = ( 423 | ); 424 | path = Network; 425 | sourceTree = ""; 426 | }; 427 | 65E11C5C22AD4D770015BFC5 /* -App */ = { 428 | isa = PBXGroup; 429 | children = ( 430 | 65B48AA022AAC72900BE9832 /* AppDelegate.swift */, 431 | 65B48AA222AAC72900BE9832 /* SceneDelegate.swift */, 432 | 65B48AC422AAC7E600BE9832 /* SceneRouter.swift */, 433 | 65B48AAB22AAC72A00BE9832 /* LaunchScreen.storyboard */, 434 | ); 435 | path = "-App"; 436 | sourceTree = ""; 437 | }; 438 | /* End PBXGroup section */ 439 | 440 | /* Begin PBXNativeTarget section */ 441 | 658E93A822AD6A36008CCC32 /* StocksUITests */ = { 442 | isa = PBXNativeTarget; 443 | buildConfigurationList = 658E93B022AD6A36008CCC32 /* Build configuration list for PBXNativeTarget "StocksUITests" */; 444 | buildPhases = ( 445 | 658E93A522AD6A36008CCC32 /* Sources */, 446 | 658E93A622AD6A36008CCC32 /* Frameworks */, 447 | 658E93A722AD6A36008CCC32 /* Resources */, 448 | ); 449 | buildRules = ( 450 | ); 451 | dependencies = ( 452 | 658E93AF22AD6A36008CCC32 /* PBXTargetDependency */, 453 | ); 454 | name = StocksUITests; 455 | productName = StocksUITests; 456 | productReference = 658E93A922AD6A36008CCC32 /* StocksUITests.xctest */; 457 | productType = "com.apple.product-type.bundle.ui-testing"; 458 | }; 459 | 65B48A9C22AAC72900BE9832 /* 99_Stocks */ = { 460 | isa = PBXNativeTarget; 461 | buildConfigurationList = 65B48ABC22AAC72A00BE9832 /* Build configuration list for PBXNativeTarget "99_Stocks" */; 462 | buildPhases = ( 463 | 65B48A9922AAC72900BE9832 /* Sources */, 464 | 65B48A9A22AAC72900BE9832 /* Frameworks */, 465 | 65B48A9B22AAC72900BE9832 /* Resources */, 466 | ); 467 | buildRules = ( 468 | ); 469 | dependencies = ( 470 | ); 471 | name = 99_Stocks; 472 | productName = 99_Companies; 473 | productReference = 65B48A9D22AAC72900BE9832 /* 99_Stocks.app */; 474 | productType = "com.apple.product-type.application"; 475 | }; 476 | 65B48AB222AAC72A00BE9832 /* 99_StocksTests */ = { 477 | isa = PBXNativeTarget; 478 | buildConfigurationList = 65B48ABF22AAC72A00BE9832 /* Build configuration list for PBXNativeTarget "99_StocksTests" */; 479 | buildPhases = ( 480 | 65B48AAF22AAC72A00BE9832 /* Sources */, 481 | 65B48AB022AAC72A00BE9832 /* Frameworks */, 482 | 65B48AB122AAC72A00BE9832 /* Resources */, 483 | ); 484 | buildRules = ( 485 | ); 486 | dependencies = ( 487 | 65B48AB522AAC72A00BE9832 /* PBXTargetDependency */, 488 | ); 489 | name = 99_StocksTests; 490 | productName = 99_CompaniesTests; 491 | productReference = 65B48AB322AAC72A00BE9832 /* 99_StocksTests.xctest */; 492 | productType = "com.apple.product-type.bundle.unit-test"; 493 | }; 494 | /* End PBXNativeTarget section */ 495 | 496 | /* Begin PBXProject section */ 497 | 65B48A9522AAC72900BE9832 /* Project object */ = { 498 | isa = PBXProject; 499 | attributes = { 500 | LastSwiftUpdateCheck = 1100; 501 | LastUpgradeCheck = 1100; 502 | ORGANIZATIONNAME = "Daniel Illescas Romero"; 503 | TargetAttributes = { 504 | 658E93A822AD6A36008CCC32 = { 505 | CreatedOnToolsVersion = 11.0; 506 | TestTargetID = 65B48A9C22AAC72900BE9832; 507 | }; 508 | 65B48A9C22AAC72900BE9832 = { 509 | CreatedOnToolsVersion = 11.0; 510 | }; 511 | 65B48AB222AAC72A00BE9832 = { 512 | CreatedOnToolsVersion = 11.0; 513 | TestTargetID = 65B48A9C22AAC72900BE9832; 514 | }; 515 | }; 516 | }; 517 | buildConfigurationList = 65B48A9822AAC72900BE9832 /* Build configuration list for PBXProject "99_Stocks" */; 518 | compatibilityVersion = "Xcode 9.3"; 519 | developmentRegion = en; 520 | hasScannedForEncodings = 0; 521 | knownRegions = ( 522 | en, 523 | Base, 524 | es, 525 | ); 526 | mainGroup = 65B48A9422AAC72900BE9832; 527 | productRefGroup = 65B48A9E22AAC72900BE9832 /* Products */; 528 | projectDirPath = ""; 529 | projectRoot = ""; 530 | targets = ( 531 | 65B48A9C22AAC72900BE9832 /* 99_Stocks */, 532 | 65B48AB222AAC72A00BE9832 /* 99_StocksTests */, 533 | 658E93A822AD6A36008CCC32 /* StocksUITests */, 534 | ); 535 | }; 536 | /* End PBXProject section */ 537 | 538 | /* Begin PBXResourcesBuildPhase section */ 539 | 658E93A722AD6A36008CCC32 /* Resources */ = { 540 | isa = PBXResourcesBuildPhase; 541 | buildActionMask = 2147483647; 542 | files = ( 543 | ); 544 | runOnlyForDeploymentPostprocessing = 0; 545 | }; 546 | 65B48A9B22AAC72900BE9832 /* Resources */ = { 547 | isa = PBXResourcesBuildPhase; 548 | buildActionMask = 2147483647; 549 | files = ( 550 | 65B48AAD22AAC72A00BE9832 /* LaunchScreen.storyboard in Resources */, 551 | 65736AE822ABB59B009A51F2 /* Localizable.strings in Resources */, 552 | 65B48AAA22AAC72A00BE9832 /* Preview Assets.xcassets in Resources */, 553 | 65B48AA722AAC72A00BE9832 /* Assets.xcassets in Resources */, 554 | ); 555 | runOnlyForDeploymentPostprocessing = 0; 556 | }; 557 | 65B48AB122AAC72A00BE9832 /* Resources */ = { 558 | isa = PBXResourcesBuildPhase; 559 | buildActionMask = 2147483647; 560 | files = ( 561 | ); 562 | runOnlyForDeploymentPostprocessing = 0; 563 | }; 564 | /* End PBXResourcesBuildPhase section */ 565 | 566 | /* Begin PBXSourcesBuildPhase section */ 567 | 658E93A522AD6A36008CCC32 /* Sources */ = { 568 | isa = PBXSourcesBuildPhase; 569 | buildActionMask = 2147483647; 570 | files = ( 571 | 658E93AC22AD6A36008CCC32 /* StocksUITests.swift in Sources */, 572 | ); 573 | runOnlyForDeploymentPostprocessing = 0; 574 | }; 575 | 65B48A9922AAC72900BE9832 /* Sources */ = { 576 | isa = PBXSourcesBuildPhase; 577 | buildActionMask = 2147483647; 578 | files = ( 579 | 65736AFD22ABC8C1009A51F2 /* Company.Network.Endpoint.swift in Sources */, 580 | 65AAE53F22AB047C00344B49 /* Chart.swift in Sources */, 581 | 65736AF522ABC52E009A51F2 /* Company.Model.DetailItemResponse.swift in Sources */, 582 | 65E11C5322AD37FC0015BFC5 /* Company.Model.StockName.swift in Sources */, 583 | 65736AFB22ABC8A9009A51F2 /* Company.Network.APIClient.swift in Sources */, 584 | 65E11C5122AD37890015BFC5 /* NetworkHelpers.swift in Sources */, 585 | 65736AED22ABBBDE009A51F2 /* Localization.swift in Sources */, 586 | 65736AF922ABC87E009A51F2 /* Company.swift in Sources */, 587 | 65736AE322ABB0CB009A51F2 /* Colors.swift in Sources */, 588 | 65AAE53922AAE5F100344B49 /* Company.Model.DetailItem.swift in Sources */, 589 | 65AAE53322AAD40B00344B49 /* Company.Model.ItemList.swift in Sources */, 590 | 65736AF322ABC521009A51F2 /* Company.Model.ItemListResponse.swift in Sources */, 591 | 65AAE53722AAE02B00344B49 /* Company.View.DetailItem.swift in Sources */, 592 | 65AAE53E22AB047C00344B49 /* ChartSeries.swift in Sources */, 593 | 65B48AA122AAC72900BE9832 /* AppDelegate.swift in Sources */, 594 | 65736B1122AC1039009A51F2 /* ActivityIndicatorView.swift in Sources */, 595 | 65B48AA322AAC72900BE9832 /* SceneDelegate.swift in Sources */, 596 | 65B48AA522AAC72900BE9832 /* Company.View.ItemList.swift in Sources */, 597 | 659747BB22AC4D10004277E3 /* Company.ViewModel.DetailItem.swift in Sources */, 598 | 65736ADF22ABAA92009A51F2 /* Asset.swift in Sources */, 599 | 65736B0F22AC073F009A51F2 /* Company.ViewModel.ItemList.swift in Sources */, 600 | 6549D0F922AD9A9800E32447 /* APIConfigurationSourcery.generated.swift in Sources */, 601 | 65E11C4D22AD27D30015BFC5 /* SystemImage.swift in Sources */, 602 | 65AAE53022AAD3C500344B49 /* CompaniesListItemView.swift in Sources */, 603 | 65AAE54022AB047C00344B49 /* ChartColors.swift in Sources */, 604 | 65736B0A22ABD645009A51F2 /* URLSession+Combine.swift in Sources */, 605 | 65B48AC522AAC7E600BE9832 /* SceneRouter.swift in Sources */, 606 | 65736B0222ABCA27009A51F2 /* APIConfiguration.swift in Sources */, 607 | 65E11C5522AD381B0015BFC5 /* Country.Model.Country.swift in Sources */, 608 | 65736AE522ABB285009A51F2 /* Images.swift in Sources */, 609 | 659747B922AC3BDD004277E3 /* Company.Router.DetailItem.swift in Sources */, 610 | 65736AD822AB1490009A51F2 /* ChartView.swift in Sources */, 611 | 65736AFF22ABC91C009A51F2 /* APIClient.swift in Sources */, 612 | 65736ADB22ABA6A4009A51F2 /* Double+Currency.swift in Sources */, 613 | 65E11C5822AD3A4A0015BFC5 /* Company.Model.StockState.swift in Sources */, 614 | 659747B722AC3B79004277E3 /* Company.Router.ItemList.swift in Sources */, 615 | ); 616 | runOnlyForDeploymentPostprocessing = 0; 617 | }; 618 | 65B48AAF22AAC72A00BE9832 /* Sources */ = { 619 | isa = PBXSourcesBuildPhase; 620 | buildActionMask = 2147483647; 621 | files = ( 622 | 65B48AB822AAC72A00BE9832 /* StocksTests.swift in Sources */, 623 | ); 624 | runOnlyForDeploymentPostprocessing = 0; 625 | }; 626 | /* End PBXSourcesBuildPhase section */ 627 | 628 | /* Begin PBXTargetDependency section */ 629 | 658E93AF22AD6A36008CCC32 /* PBXTargetDependency */ = { 630 | isa = PBXTargetDependency; 631 | target = 65B48A9C22AAC72900BE9832 /* 99_Stocks */; 632 | targetProxy = 658E93AE22AD6A36008CCC32 /* PBXContainerItemProxy */; 633 | }; 634 | 65B48AB522AAC72A00BE9832 /* PBXTargetDependency */ = { 635 | isa = PBXTargetDependency; 636 | target = 65B48A9C22AAC72900BE9832 /* 99_Stocks */; 637 | targetProxy = 65B48AB422AAC72A00BE9832 /* PBXContainerItemProxy */; 638 | }; 639 | /* End PBXTargetDependency section */ 640 | 641 | /* Begin PBXVariantGroup section */ 642 | 65736AEA22ABB59B009A51F2 /* Localizable.strings */ = { 643 | isa = PBXVariantGroup; 644 | children = ( 645 | 65736AE922ABB59B009A51F2 /* en */, 646 | 65736AEB22ABB5A5009A51F2 /* es */, 647 | ); 648 | name = Localizable.strings; 649 | sourceTree = ""; 650 | }; 651 | 65B48AAB22AAC72A00BE9832 /* LaunchScreen.storyboard */ = { 652 | isa = PBXVariantGroup; 653 | children = ( 654 | 65B48AAC22AAC72A00BE9832 /* Base */, 655 | ); 656 | name = LaunchScreen.storyboard; 657 | sourceTree = ""; 658 | }; 659 | /* End PBXVariantGroup section */ 660 | 661 | /* Begin XCBuildConfiguration section */ 662 | 658E93B122AD6A36008CCC32 /* Debug */ = { 663 | isa = XCBuildConfiguration; 664 | buildSettings = { 665 | CODE_SIGN_STYLE = Automatic; 666 | DEVELOPMENT_TEAM = PRK6268SLD; 667 | INFOPLIST_FILE = StocksUITests/Info.plist; 668 | LD_RUNPATH_SEARCH_PATHS = ( 669 | "$(inherited)", 670 | "@executable_path/Frameworks", 671 | "@loader_path/Frameworks", 672 | ); 673 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.StocksUITests"; 674 | PRODUCT_NAME = "$(TARGET_NAME)"; 675 | SWIFT_VERSION = 5.0; 676 | TARGETED_DEVICE_FAMILY = "1,2"; 677 | TEST_TARGET_NAME = 99_Stocks; 678 | }; 679 | name = Debug; 680 | }; 681 | 658E93B222AD6A36008CCC32 /* Release */ = { 682 | isa = XCBuildConfiguration; 683 | buildSettings = { 684 | CODE_SIGN_STYLE = Automatic; 685 | DEVELOPMENT_TEAM = PRK6268SLD; 686 | INFOPLIST_FILE = StocksUITests/Info.plist; 687 | LD_RUNPATH_SEARCH_PATHS = ( 688 | "$(inherited)", 689 | "@executable_path/Frameworks", 690 | "@loader_path/Frameworks", 691 | ); 692 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.StocksUITests"; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SWIFT_VERSION = 5.0; 695 | TARGETED_DEVICE_FAMILY = "1,2"; 696 | TEST_TARGET_NAME = 99_Stocks; 697 | }; 698 | name = Release; 699 | }; 700 | 65B48ABA22AAC72A00BE9832 /* Debug */ = { 701 | isa = XCBuildConfiguration; 702 | buildSettings = { 703 | ALWAYS_SEARCH_USER_PATHS = NO; 704 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 705 | CLANG_ANALYZER_NONNULL = YES; 706 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 707 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 708 | CLANG_CXX_LIBRARY = "libc++"; 709 | CLANG_ENABLE_MODULES = YES; 710 | CLANG_ENABLE_OBJC_ARC = YES; 711 | CLANG_ENABLE_OBJC_WEAK = YES; 712 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 713 | CLANG_WARN_BOOL_CONVERSION = YES; 714 | CLANG_WARN_COMMA = YES; 715 | CLANG_WARN_CONSTANT_CONVERSION = YES; 716 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 717 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 718 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 719 | CLANG_WARN_EMPTY_BODY = YES; 720 | CLANG_WARN_ENUM_CONVERSION = YES; 721 | CLANG_WARN_INFINITE_RECURSION = YES; 722 | CLANG_WARN_INT_CONVERSION = YES; 723 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 724 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 725 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 726 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 727 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 728 | CLANG_WARN_STRICT_PROTOTYPES = YES; 729 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 730 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 731 | CLANG_WARN_UNREACHABLE_CODE = YES; 732 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 733 | COPY_PHASE_STRIP = NO; 734 | DEBUG_INFORMATION_FORMAT = dwarf; 735 | ENABLE_STRICT_OBJC_MSGSEND = YES; 736 | ENABLE_TESTABILITY = YES; 737 | GCC_C_LANGUAGE_STANDARD = gnu11; 738 | GCC_DYNAMIC_NO_PIC = NO; 739 | GCC_NO_COMMON_BLOCKS = YES; 740 | GCC_OPTIMIZATION_LEVEL = 0; 741 | GCC_PREPROCESSOR_DEFINITIONS = ( 742 | "DEBUG=1", 743 | "$(inherited)", 744 | ); 745 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 746 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 747 | GCC_WARN_UNDECLARED_SELECTOR = YES; 748 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 749 | GCC_WARN_UNUSED_FUNCTION = YES; 750 | GCC_WARN_UNUSED_VARIABLE = YES; 751 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 752 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 753 | MTL_FAST_MATH = YES; 754 | ONLY_ACTIVE_ARCH = YES; 755 | SDKROOT = iphoneos; 756 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 757 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 758 | }; 759 | name = Debug; 760 | }; 761 | 65B48ABB22AAC72A00BE9832 /* Release */ = { 762 | isa = XCBuildConfiguration; 763 | buildSettings = { 764 | ALWAYS_SEARCH_USER_PATHS = NO; 765 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 766 | CLANG_ANALYZER_NONNULL = YES; 767 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 768 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 769 | CLANG_CXX_LIBRARY = "libc++"; 770 | CLANG_ENABLE_MODULES = YES; 771 | CLANG_ENABLE_OBJC_ARC = YES; 772 | CLANG_ENABLE_OBJC_WEAK = YES; 773 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 774 | CLANG_WARN_BOOL_CONVERSION = YES; 775 | CLANG_WARN_COMMA = YES; 776 | CLANG_WARN_CONSTANT_CONVERSION = YES; 777 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 778 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 779 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 780 | CLANG_WARN_EMPTY_BODY = YES; 781 | CLANG_WARN_ENUM_CONVERSION = YES; 782 | CLANG_WARN_INFINITE_RECURSION = YES; 783 | CLANG_WARN_INT_CONVERSION = YES; 784 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 785 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 786 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 787 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 788 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 789 | CLANG_WARN_STRICT_PROTOTYPES = YES; 790 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 791 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 792 | CLANG_WARN_UNREACHABLE_CODE = YES; 793 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 794 | COPY_PHASE_STRIP = NO; 795 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 796 | ENABLE_NS_ASSERTIONS = NO; 797 | ENABLE_STRICT_OBJC_MSGSEND = YES; 798 | GCC_C_LANGUAGE_STANDARD = gnu11; 799 | GCC_NO_COMMON_BLOCKS = YES; 800 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 801 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 802 | GCC_WARN_UNDECLARED_SELECTOR = YES; 803 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 804 | GCC_WARN_UNUSED_FUNCTION = YES; 805 | GCC_WARN_UNUSED_VARIABLE = YES; 806 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 807 | MTL_ENABLE_DEBUG_INFO = NO; 808 | MTL_FAST_MATH = YES; 809 | SDKROOT = iphoneos; 810 | SWIFT_COMPILATION_MODE = wholemodule; 811 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 812 | VALIDATE_PRODUCT = YES; 813 | }; 814 | name = Release; 815 | }; 816 | 65B48ABD22AAC72A00BE9832 /* Debug */ = { 817 | isa = XCBuildConfiguration; 818 | buildSettings = { 819 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 820 | CODE_SIGN_ENTITLEMENTS = 99_Stocks/99_Stocks.entitlements; 821 | CODE_SIGN_STYLE = Automatic; 822 | DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; 823 | DEVELOPMENT_ASSET_PATHS = "99_Stocks/Resources/Preview\\ Content"; 824 | DEVELOPMENT_TEAM = PRK6268SLD; 825 | ENABLE_PREVIEWS = YES; 826 | INFOPLIST_FILE = 99_Stocks/Info.plist; 827 | LD_RUNPATH_SEARCH_PATHS = ( 828 | "$(inherited)", 829 | "@executable_path/Frameworks", 830 | ); 831 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.-99-Stocks"; 832 | PRODUCT_NAME = "$(TARGET_NAME)"; 833 | SUPPORTS_MACCATALYST = YES; 834 | SWIFT_VERSION = 5.0; 835 | TARGETED_DEVICE_FAMILY = "1,2"; 836 | }; 837 | name = Debug; 838 | }; 839 | 65B48ABE22AAC72A00BE9832 /* Release */ = { 840 | isa = XCBuildConfiguration; 841 | buildSettings = { 842 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 843 | CODE_SIGN_ENTITLEMENTS = 99_Stocks/99_Stocks.entitlements; 844 | CODE_SIGN_STYLE = Automatic; 845 | DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; 846 | DEVELOPMENT_ASSET_PATHS = "99_Stocks/Resources/Preview\\ Content"; 847 | DEVELOPMENT_TEAM = PRK6268SLD; 848 | ENABLE_PREVIEWS = YES; 849 | INFOPLIST_FILE = 99_Stocks/Info.plist; 850 | LD_RUNPATH_SEARCH_PATHS = ( 851 | "$(inherited)", 852 | "@executable_path/Frameworks", 853 | ); 854 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.-99-Stocks"; 855 | PRODUCT_NAME = "$(TARGET_NAME)"; 856 | SUPPORTS_MACCATALYST = YES; 857 | SWIFT_VERSION = 5.0; 858 | TARGETED_DEVICE_FAMILY = "1,2"; 859 | }; 860 | name = Release; 861 | }; 862 | 65B48AC022AAC72A00BE9832 /* Debug */ = { 863 | isa = XCBuildConfiguration; 864 | buildSettings = { 865 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 866 | BUNDLE_LOADER = "$(TEST_HOST)"; 867 | CODE_SIGN_STYLE = Automatic; 868 | DEVELOPMENT_TEAM = PRK6268SLD; 869 | INFOPLIST_FILE = 99_StocksTests/Info.plist; 870 | LD_RUNPATH_SEARCH_PATHS = ( 871 | "$(inherited)", 872 | "@executable_path/Frameworks", 873 | "@loader_path/Frameworks", 874 | ); 875 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.-9-CompaniesTests"; 876 | PRODUCT_NAME = "$(TARGET_NAME)"; 877 | SWIFT_VERSION = 5.0; 878 | TARGETED_DEVICE_FAMILY = "1,2"; 879 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/99_Stocks.app/99_Stocks"; 880 | }; 881 | name = Debug; 882 | }; 883 | 65B48AC122AAC72A00BE9832 /* Release */ = { 884 | isa = XCBuildConfiguration; 885 | buildSettings = { 886 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 887 | BUNDLE_LOADER = "$(TEST_HOST)"; 888 | CODE_SIGN_STYLE = Automatic; 889 | DEVELOPMENT_TEAM = PRK6268SLD; 890 | INFOPLIST_FILE = 99_StocksTests/Info.plist; 891 | LD_RUNPATH_SEARCH_PATHS = ( 892 | "$(inherited)", 893 | "@executable_path/Frameworks", 894 | "@loader_path/Frameworks", 895 | ); 896 | PRODUCT_BUNDLE_IDENTIFIER = "es.illescas-daniel.-9-CompaniesTests"; 897 | PRODUCT_NAME = "$(TARGET_NAME)"; 898 | SWIFT_VERSION = 5.0; 899 | TARGETED_DEVICE_FAMILY = "1,2"; 900 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/99_Stocks.app/99_Stocks"; 901 | }; 902 | name = Release; 903 | }; 904 | /* End XCBuildConfiguration section */ 905 | 906 | /* Begin XCConfigurationList section */ 907 | 658E93B022AD6A36008CCC32 /* Build configuration list for PBXNativeTarget "StocksUITests" */ = { 908 | isa = XCConfigurationList; 909 | buildConfigurations = ( 910 | 658E93B122AD6A36008CCC32 /* Debug */, 911 | 658E93B222AD6A36008CCC32 /* Release */, 912 | ); 913 | defaultConfigurationIsVisible = 0; 914 | defaultConfigurationName = Release; 915 | }; 916 | 65B48A9822AAC72900BE9832 /* Build configuration list for PBXProject "99_Stocks" */ = { 917 | isa = XCConfigurationList; 918 | buildConfigurations = ( 919 | 65B48ABA22AAC72A00BE9832 /* Debug */, 920 | 65B48ABB22AAC72A00BE9832 /* Release */, 921 | ); 922 | defaultConfigurationIsVisible = 0; 923 | defaultConfigurationName = Release; 924 | }; 925 | 65B48ABC22AAC72A00BE9832 /* Build configuration list for PBXNativeTarget "99_Stocks" */ = { 926 | isa = XCConfigurationList; 927 | buildConfigurations = ( 928 | 65B48ABD22AAC72A00BE9832 /* Debug */, 929 | 65B48ABE22AAC72A00BE9832 /* Release */, 930 | ); 931 | defaultConfigurationIsVisible = 0; 932 | defaultConfigurationName = Release; 933 | }; 934 | 65B48ABF22AAC72A00BE9832 /* Build configuration list for PBXNativeTarget "99_StocksTests" */ = { 935 | isa = XCConfigurationList; 936 | buildConfigurations = ( 937 | 65B48AC022AAC72A00BE9832 /* Debug */, 938 | 65B48AC122AAC72A00BE9832 /* Release */, 939 | ); 940 | defaultConfigurationIsVisible = 0; 941 | defaultConfigurationName = Release; 942 | }; 943 | /* End XCConfigurationList section */ 944 | }; 945 | rootObject = 65B48A9522AAC72900BE9832 /* Project object */; 946 | } 947 | -------------------------------------------------------------------------------- /99_Stocks.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /99_Stocks.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /99_Stocks/3rdPartyLibs/SwiftChart/Chart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chart.swift 3 | // 4 | // Created by Giampaolo Bellavite on 07/11/14. 5 | // Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol ChartDelegate: class { 11 | 12 | /** 13 | Tells the delegate that the specified chart has been touched. 14 | 15 | - parameter chart: The chart that has been touched. 16 | - parameter indexes: Each element of this array contains the index of the data that has been touched, one for each 17 | series. If the series hasn't been touched, its index will be nil. 18 | - parameter x: The value on the x-axis that has been touched. 19 | - parameter left: The distance from the left side of the chart. 20 | 21 | */ 22 | func didTouchChart(_ chart: Chart, indexes: [Int?], x: Double, left: CGFloat) 23 | 24 | /** 25 | Tells the delegate that the user finished touching the chart. The user will 26 | "finish" touching the chart only swiping left/right outside the chart. 27 | 28 | - parameter chart: The chart that has been touched. 29 | 30 | */ 31 | func didFinishTouchingChart(_ chart: Chart) 32 | /** 33 | Tells the delegate that the user ended touching the chart. The user 34 | will "end" touching the chart whenever the touchesDidEnd method is 35 | being called. 36 | 37 | - parameter chart: The chart that has been touched. 38 | 39 | */ 40 | func didEndTouchingChart(_ chart: Chart) 41 | } 42 | 43 | /** 44 | Represent the x- and the y-axis values for each point in a chart series. 45 | */ 46 | typealias ChartPoint = (x: Double, y: Double) 47 | 48 | /** 49 | Set the a x-label orientation. 50 | */ 51 | public enum ChartLabelOrientation { 52 | case horizontal 53 | case vertical 54 | } 55 | 56 | @IBDesignable 57 | open class Chart: UIControl { 58 | 59 | // MARK: Options 60 | 61 | @IBInspectable 62 | open var identifier: String? 63 | 64 | /** 65 | Series to display in the chart. 66 | */ 67 | open var series: [ChartSeries] = [] { 68 | didSet { 69 | setNeedsDisplay() 70 | } 71 | } 72 | 73 | /** 74 | The values to display as labels on the x-axis. You can format these values with the `xLabelFormatter` attribute. 75 | As default, it will display the values of the series which has the most data. 76 | */ 77 | open var xLabels: [Double]? 78 | 79 | /** 80 | Formatter for the labels on the x-axis. `index` represents the `xLabels` index, `value` its value. 81 | */ 82 | open var xLabelsFormatter = { (labelIndex: Int, labelValue: Double) -> String in 83 | String(Int(labelValue)) 84 | } 85 | 86 | /** 87 | Text alignment for the x-labels. 88 | */ 89 | open var xLabelsTextAlignment: NSTextAlignment = .left 90 | 91 | /** 92 | Orientation for the x-labels. 93 | */ 94 | open var xLabelsOrientation: ChartLabelOrientation = .horizontal 95 | 96 | /** 97 | Skip the last x-label. Setting this to false may make the label overflow the frame width. 98 | */ 99 | open var xLabelsSkipLast: Bool = true 100 | 101 | /** 102 | Values to display as labels of the y-axis. If not specified, will display the lowest, the middle and the highest 103 | values. 104 | */ 105 | open var yLabels: [Double]? 106 | 107 | /** 108 | Formatter for the labels on the y-axis. 109 | */ 110 | open var yLabelsFormatter = { (labelIndex: Int, labelValue: Double) -> String in 111 | String(Int(labelValue)) 112 | } 113 | 114 | /** 115 | Displays the y-axis labels on the right side of the chart. 116 | */ 117 | open var yLabelsOnRightSide: Bool = false 118 | 119 | /** 120 | Font used for the labels. 121 | */ 122 | open var labelFont: UIFont? = UIFont.systemFont(ofSize: 12) 123 | 124 | /** 125 | The color used for the labels. 126 | */ 127 | @IBInspectable 128 | open var labelColor: UIColor = UIColor.black 129 | 130 | /** 131 | Color for the axes. 132 | */ 133 | @IBInspectable 134 | open var axesColor: UIColor = UIColor.gray.withAlphaComponent(0.3) 135 | 136 | /** 137 | Color for the grid. 138 | */ 139 | @IBInspectable 140 | open var gridColor: UIColor = UIColor.gray.withAlphaComponent(0.3) 141 | /** 142 | Enable the lines for the labels on the x-axis 143 | */ 144 | open var showXLabelsAndGrid: Bool = true 145 | /** 146 | Enable the lines for the labels on the y-axis 147 | */ 148 | open var showYLabelsAndGrid: Bool = true 149 | 150 | /** 151 | Height of the area at the bottom of the chart, containing the labels for the x-axis. 152 | */ 153 | open var bottomInset: CGFloat = 20 154 | 155 | /** 156 | Height of the area at the top of the chart, acting a padding to make place for the top y-axis label. 157 | */ 158 | open var topInset: CGFloat = 20 159 | 160 | /** 161 | Width of the chart's lines. 162 | */ 163 | @IBInspectable 164 | open var lineWidth: CGFloat = 2 165 | 166 | /** 167 | Delegate for listening to Chart touch events. 168 | */ 169 | weak open var delegate: ChartDelegate? 170 | 171 | /** 172 | Custom minimum value for the x-axis. 173 | */ 174 | open var minX: Double? 175 | 176 | /** 177 | Custom minimum value for the y-axis. 178 | */ 179 | open var minY: Double? 180 | 181 | /** 182 | Custom maximum value for the x-axis. 183 | */ 184 | open var maxX: Double? 185 | 186 | /** 187 | Custom maximum value for the y-axis. 188 | */ 189 | open var maxY: Double? 190 | 191 | /** 192 | Color for the highlight line. 193 | */ 194 | open var highlightLineColor = UIColor.gray 195 | 196 | /** 197 | Width for the highlight line. 198 | */ 199 | open var highlightLineWidth: CGFloat = 0.5 200 | 201 | /** 202 | Hide the highlight line when touch event ends, e.g. when stop swiping over the chart 203 | */ 204 | open var hideHighlightLineOnTouchEnd = false 205 | 206 | /** 207 | Alpha component for the area color. 208 | */ 209 | open var areaAlphaComponent: CGFloat = 0.1 210 | 211 | // MARK: Private variables 212 | 213 | fileprivate var highlightShapeLayer: CAShapeLayer! 214 | fileprivate var layerStore: [CAShapeLayer] = [] 215 | 216 | fileprivate var drawingHeight: CGFloat! 217 | fileprivate var drawingWidth: CGFloat! 218 | 219 | // Minimum and maximum values represented in the chart 220 | fileprivate var min: ChartPoint! 221 | fileprivate var max: ChartPoint! 222 | 223 | // Represent a set of points corresponding to a segment line on the chart. 224 | typealias ChartLineSegment = [ChartPoint] 225 | 226 | // MARK: initializations 227 | 228 | override public init(frame: CGRect) { 229 | super.init(frame: frame) 230 | commonInit() 231 | } 232 | 233 | required public init?(coder aDecoder: NSCoder) { 234 | super.init(coder: aDecoder) 235 | commonInit() 236 | } 237 | 238 | convenience public init() { 239 | self.init(frame: .zero) 240 | commonInit() 241 | } 242 | 243 | private func commonInit() { 244 | backgroundColor = UIColor.clear 245 | contentMode = .redraw // redraw rects on bounds change 246 | } 247 | 248 | override open func draw(_ rect: CGRect) { 249 | #if TARGET_INTERFACE_BUILDER 250 | drawIBPlaceholder() 251 | #else 252 | drawChart() 253 | #endif 254 | } 255 | 256 | /** 257 | Adds a chart series. 258 | */ 259 | open func add(_ series: ChartSeries) { 260 | self.series.append(series) 261 | } 262 | 263 | /** 264 | Adds multiple chart series. 265 | */ 266 | open func add(_ series: [ChartSeries]) { 267 | for s in series { 268 | add(s) 269 | } 270 | } 271 | 272 | /** 273 | Remove the series at the specified index. 274 | */ 275 | open func removeSeriesAt(_ index: Int) { 276 | series.remove(at: index) 277 | } 278 | 279 | /** 280 | Remove all the series. 281 | */ 282 | open func removeAllSeries() { 283 | series = [] 284 | } 285 | 286 | /** 287 | Return the value for the specified series at the given index. 288 | */ 289 | open func valueForSeries(_ seriesIndex: Int, atIndex dataIndex: Int?) -> Double? { 290 | if dataIndex == nil { return nil } 291 | let series = self.series[seriesIndex] as ChartSeries 292 | return series.data[dataIndex!].y 293 | } 294 | 295 | fileprivate func drawIBPlaceholder() { 296 | let placeholder = UIView(frame: self.frame) 297 | placeholder.backgroundColor = UIColor(red: 0.93, green: 0.93, blue: 0.93, alpha: 1) 298 | let label = UILabel() 299 | label.text = "Chart" 300 | label.font = UIFont.systemFont(ofSize: 28) 301 | label.textColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2) 302 | label.sizeToFit() 303 | label.frame.origin.x += frame.width/2 - (label.frame.width / 2) 304 | label.frame.origin.y += frame.height/2 - (label.frame.height / 2) 305 | 306 | placeholder.addSubview(label) 307 | addSubview(placeholder) 308 | } 309 | 310 | fileprivate func drawChart() { 311 | 312 | drawingHeight = bounds.height - bottomInset - topInset 313 | drawingWidth = bounds.width 314 | 315 | let minMax = getMinMax() 316 | min = minMax.min 317 | max = minMax.max 318 | 319 | highlightShapeLayer = nil 320 | 321 | // Remove things before drawing, e.g. when changing orientation 322 | 323 | for view in self.subviews { 324 | view.removeFromSuperview() 325 | } 326 | for layer in layerStore { 327 | layer.removeFromSuperlayer() 328 | } 329 | layerStore.removeAll() 330 | 331 | // Draw content 332 | 333 | for (index, series) in self.series.enumerated() { 334 | 335 | // Separate each line in multiple segments over and below the x axis 336 | let segments = Chart.segmentLine(series.data as ChartLineSegment, zeroLevel: series.colors.zeroLevel) 337 | 338 | segments.forEach({ segment in 339 | let scaledXValues = scaleValuesOnXAxis( segment.map { $0.x } ) 340 | let scaledYValues = scaleValuesOnYAxis( segment.map { $0.y } ) 341 | 342 | if series.line { 343 | drawLine(scaledXValues, yValues: scaledYValues, seriesIndex: index) 344 | } 345 | if series.area { 346 | drawArea(scaledXValues, yValues: scaledYValues, seriesIndex: index) 347 | } 348 | }) 349 | } 350 | 351 | drawAxes() 352 | 353 | if showXLabelsAndGrid && (xLabels != nil || series.count > 0) { 354 | drawLabelsAndGridOnXAxis() 355 | } 356 | if showYLabelsAndGrid && (yLabels != nil || series.count > 0) { 357 | drawLabelsAndGridOnYAxis() 358 | } 359 | 360 | } 361 | 362 | // MARK: - Scaling 363 | 364 | fileprivate func getMinMax() -> (min: ChartPoint, max: ChartPoint) { 365 | // Start with user-provided values 366 | 367 | var min = (x: minX, y: minY) 368 | var max = (x: maxX, y: maxY) 369 | 370 | // Check in datasets 371 | 372 | for series in self.series { 373 | let xValues = series.data.map { $0.x } 374 | let yValues = series.data.map { $0.y } 375 | 376 | let newMinX = xValues.minOrZero() 377 | let newMinY = yValues.minOrZero() 378 | let newMaxX = xValues.maxOrZero() 379 | let newMaxY = yValues.maxOrZero() 380 | 381 | if min.x == nil || newMinX < min.x! { min.x = newMinX } 382 | if min.y == nil || newMinY < min.y! { min.y = newMinY } 383 | if max.x == nil || newMaxX > max.x! { max.x = newMaxX } 384 | if max.y == nil || newMaxY > max.y! { max.y = newMaxY } 385 | } 386 | 387 | // Check in labels 388 | 389 | if let xLabels = self.xLabels { 390 | let newMinX = xLabels.minOrZero() 391 | let newMaxX = xLabels.maxOrZero() 392 | if min.x == nil || newMinX < min.x! { min.x = newMinX } 393 | if max.x == nil || newMaxX > max.x! { max.x = newMaxX } 394 | } 395 | 396 | if let yLabels = self.yLabels { 397 | let newMinY = yLabels.minOrZero() 398 | let newMaxY = yLabels.maxOrZero() 399 | if min.y == nil || newMinY < min.y! { min.y = newMinY } 400 | if max.y == nil || newMaxY > max.y! { max.y = newMaxY } 401 | } 402 | 403 | if min.x == nil { min.x = 0 } 404 | if min.y == nil { min.y = 0 } 405 | if max.x == nil { max.x = 0 } 406 | if max.y == nil { max.y = 0 } 407 | 408 | return (min: (x: min.x!, y: min.y!), max: (x: max.x!, max.y!)) 409 | } 410 | 411 | fileprivate func scaleValuesOnXAxis(_ values: [Double]) -> [Double] { 412 | let width = Double(drawingWidth) 413 | 414 | var factor: Double 415 | if max.x - min.x == 0 { 416 | factor = 0 417 | } else { 418 | factor = width / (max.x - min.x) 419 | } 420 | 421 | let scaled = values.map { factor * ($0 - self.min.x) } 422 | return scaled 423 | } 424 | 425 | fileprivate func scaleValuesOnYAxis(_ values: [Double]) -> [Double] { 426 | let height = Double(drawingHeight) 427 | var factor: Double 428 | if max.y - min.y == 0 { 429 | factor = 0 430 | } else { 431 | factor = height / (max.y - min.y) 432 | } 433 | 434 | let scaled = values.map { Double(self.topInset) + height - factor * ($0 - self.min.y) } 435 | 436 | return scaled 437 | } 438 | 439 | fileprivate func scaleValueOnYAxis(_ value: Double) -> Double { 440 | let height = Double(drawingHeight) 441 | var factor: Double 442 | if max.y - min.y == 0 { 443 | factor = 0 444 | } else { 445 | factor = height / (max.y - min.y) 446 | } 447 | 448 | let scaled = Double(self.topInset) + height - factor * (value - min.y) 449 | return scaled 450 | } 451 | 452 | fileprivate func getZeroValueOnYAxis(zeroLevel: Double) -> Double { 453 | if min.y > zeroLevel { 454 | return scaleValueOnYAxis(min.y) 455 | } else { 456 | return scaleValueOnYAxis(zeroLevel) 457 | } 458 | } 459 | 460 | // MARK: - Drawings 461 | 462 | fileprivate func drawLine(_ xValues: [Double], yValues: [Double], seriesIndex: Int) { 463 | // YValues are "reverted" from top to bottom, so 'above' means <= level 464 | let isAboveZeroLine = yValues.max()! <= self.scaleValueOnYAxis(series[seriesIndex].colors.zeroLevel) 465 | let path = CGMutablePath() 466 | path.move(to: CGPoint(x: CGFloat(xValues.first!), y: CGFloat(yValues.first!))) 467 | for i in 1.. 0 { 534 | let y = CGFloat(getZeroValueOnYAxis(zeroLevel: 0)) 535 | context.move(to: CGPoint(x: CGFloat(0), y: y)) 536 | context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: y)) 537 | context.strokePath() 538 | } 539 | 540 | // vertical axis on the left 541 | context.move(to: CGPoint(x: CGFloat(0), y: CGFloat(0))) 542 | context.addLine(to: CGPoint(x: CGFloat(0), y: drawingHeight + topInset)) 543 | context.strokePath() 544 | 545 | // vertical axis on the right 546 | context.move(to: CGPoint(x: CGFloat(drawingWidth), y: CGFloat(0))) 547 | context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: drawingHeight + topInset)) 548 | context.strokePath() 549 | } 550 | 551 | fileprivate func drawLabelsAndGridOnXAxis() { 552 | let context = UIGraphicsGetCurrentContext()! 553 | context.setStrokeColor(gridColor.cgColor) 554 | context.setLineWidth(0.5) 555 | 556 | var labels: [Double] 557 | if xLabels == nil { 558 | // Use labels from the first series 559 | labels = series[0].data.map({ (point: ChartPoint) -> Double in 560 | return point.x }) 561 | } else { 562 | labels = xLabels! 563 | } 564 | 565 | let scaled = scaleValuesOnXAxis(labels) 566 | let padding: CGFloat = 5 567 | scaled.enumerated().forEach { (i, value) in 568 | let x = CGFloat(value) 569 | let isLastLabel = x == drawingWidth 570 | 571 | // Add vertical grid for each label, except axes on the left and right 572 | 573 | if x != 0 && x != drawingWidth { 574 | context.move(to: CGPoint(x: x, y: CGFloat(0))) 575 | context.addLine(to: CGPoint(x: x, y: bounds.height)) 576 | context.strokePath() 577 | } 578 | 579 | if xLabelsSkipLast && isLastLabel { 580 | // Do not add label at the most right position 581 | return 582 | } 583 | 584 | // Add label 585 | let label = UILabel(frame: CGRect(x: x, y: drawingHeight, width: 0, height: 0)) 586 | label.font = labelFont 587 | label.text = xLabelsFormatter(i, labels[i]) 588 | label.textColor = labelColor 589 | 590 | // Set label size 591 | label.sizeToFit() 592 | // Center label vertically 593 | label.frame.origin.y += topInset 594 | if xLabelsOrientation == .horizontal { 595 | // Add left padding 596 | label.frame.origin.y -= (label.frame.height - bottomInset) / 2 597 | label.frame.origin.x += padding 598 | 599 | // Set label's text alignment 600 | label.frame.size.width = (drawingWidth / CGFloat(labels.count)) - padding * 2 601 | label.textAlignment = xLabelsTextAlignment 602 | } else { 603 | label.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2)) 604 | 605 | // Adjust vertical position according to the label's height 606 | label.frame.origin.y += label.frame.size.height / 2 607 | 608 | // Adjust horizontal position as the series line 609 | label.frame.origin.x = x 610 | if xLabelsTextAlignment == .center { 611 | // Align horizontally in series 612 | label.frame.origin.x += ((drawingWidth / CGFloat(labels.count)) / 2) - (label.frame.size.width / 2) 613 | } else { 614 | // Give some space from the vertical line 615 | label.frame.origin.x += padding 616 | } 617 | } 618 | self.addSubview(label) 619 | } 620 | } 621 | 622 | fileprivate func drawLabelsAndGridOnYAxis() { 623 | let context = UIGraphicsGetCurrentContext()! 624 | context.setStrokeColor(gridColor.cgColor) 625 | context.setLineWidth(0.5) 626 | 627 | var labels: [Double] 628 | if yLabels == nil { 629 | labels = [(min.y + max.y) / 2, max.y] 630 | if yLabelsOnRightSide || min.y != 0 { 631 | labels.insert(min.y, at: 0) 632 | } 633 | } else { 634 | labels = yLabels! 635 | } 636 | 637 | let scaled = scaleValuesOnYAxis(labels) 638 | let padding: CGFloat = 5 639 | let zero = CGFloat(getZeroValueOnYAxis(zeroLevel: 0)) 640 | 641 | scaled.enumerated().forEach { (i, value) in 642 | 643 | let y = CGFloat(value) 644 | 645 | // Add horizontal grid for each label, but not over axes 646 | if y != drawingHeight + topInset && y != zero { 647 | 648 | context.move(to: CGPoint(x: CGFloat(0), y: y)) 649 | context.addLine(to: CGPoint(x: self.bounds.width, y: y)) 650 | if labels[i] != 0 { 651 | // Horizontal grid for 0 is not dashed 652 | context.setLineDash(phase: CGFloat(0), lengths: [CGFloat(5)]) 653 | } else { 654 | context.setLineDash(phase: CGFloat(0), lengths: []) 655 | } 656 | context.strokePath() 657 | } 658 | 659 | let label = UILabel(frame: CGRect(x: padding, y: y, width: 0, height: 0)) 660 | label.font = labelFont 661 | label.text = yLabelsFormatter(i, labels[i]) 662 | label.textColor = labelColor 663 | label.sizeToFit() 664 | 665 | if yLabelsOnRightSide { 666 | label.frame.origin.x = drawingWidth 667 | label.frame.origin.x -= label.frame.width + padding 668 | } 669 | 670 | // Labels should be placed above the horizontal grid 671 | label.frame.origin.y -= label.frame.height 672 | 673 | self.addSubview(label) 674 | } 675 | UIGraphicsEndImageContext() 676 | } 677 | 678 | // MARK: - Touch events 679 | 680 | fileprivate func drawHighlightLineFromLeftPosition(_ left: CGFloat) { 681 | if let shapeLayer = highlightShapeLayer { 682 | // Use line already created 683 | let path = CGMutablePath() 684 | 685 | path.move(to: CGPoint(x: left, y: 0)) 686 | path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset)) 687 | shapeLayer.path = path 688 | } else { 689 | // Create the line 690 | let path = CGMutablePath() 691 | 692 | path.move(to: CGPoint(x: left, y: CGFloat(0))) 693 | path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset)) 694 | let shapeLayer = CAShapeLayer() 695 | shapeLayer.frame = self.bounds 696 | shapeLayer.path = path 697 | shapeLayer.strokeColor = highlightLineColor.cgColor 698 | shapeLayer.fillColor = nil 699 | shapeLayer.lineWidth = highlightLineWidth 700 | 701 | highlightShapeLayer = shapeLayer 702 | layer.addSublayer(shapeLayer) 703 | layerStore.append(shapeLayer) 704 | } 705 | } 706 | 707 | func handleTouchEvents(_ touches: Set, event: UIEvent!) { 708 | let point = touches.first! 709 | let left = point.location(in: self).x 710 | let x = valueFromPointAtX(left) 711 | 712 | if left < 0 || left > (drawingWidth as CGFloat) { 713 | // Remove highlight line at the end of the touch event 714 | if let shapeLayer = highlightShapeLayer { 715 | shapeLayer.path = nil 716 | } 717 | delegate?.didFinishTouchingChart(self) 718 | return 719 | } 720 | 721 | drawHighlightLineFromLeftPosition(left) 722 | 723 | if delegate == nil { 724 | return 725 | } 726 | 727 | var indexes: [Int?] = [] 728 | 729 | for series in self.series { 730 | var index: Int? = nil 731 | let xValues = series.data.map({ (point: ChartPoint) -> Double in 732 | return point.x }) 733 | let closest = Chart.findClosestInValues(xValues, forValue: x) 734 | if closest.lowestIndex != nil && closest.highestIndex != nil { 735 | // Consider valid only values on the right 736 | index = closest.lowestIndex 737 | } 738 | indexes.append(index) 739 | } 740 | delegate!.didTouchChart(self, indexes: indexes, x: x, left: left) 741 | } 742 | 743 | override open func touchesBegan(_ touches: Set, with event: UIEvent?) { 744 | handleTouchEvents(touches, event: event) 745 | } 746 | 747 | override open func touchesEnded(_ touches: Set, with event: UIEvent?) { 748 | handleTouchEvents(touches, event: event) 749 | if self.hideHighlightLineOnTouchEnd { 750 | if let shapeLayer = highlightShapeLayer { 751 | shapeLayer.path = nil 752 | } 753 | } 754 | delegate?.didEndTouchingChart(self) 755 | } 756 | 757 | override open func touchesMoved(_ touches: Set, with event: UIEvent?) { 758 | handleTouchEvents(touches, event: event) 759 | } 760 | 761 | // MARK: - Utilities 762 | 763 | fileprivate func valueFromPointAtX(_ x: CGFloat) -> Double { 764 | let value = ((max.x-min.x) / Double(drawingWidth)) * Double(x) + min.x 765 | return value 766 | } 767 | 768 | fileprivate func valueFromPointAtY(_ y: CGFloat) -> Double { 769 | let value = ((max.y - min.y) / Double(drawingHeight)) * Double(y) + min.y 770 | return -value 771 | } 772 | 773 | fileprivate class func findClosestInValues( 774 | _ values: [Double], 775 | forValue value: Double 776 | ) -> ( 777 | lowestValue: Double?, 778 | highestValue: Double?, 779 | lowestIndex: Int?, 780 | highestIndex: Int? 781 | ) { 782 | var lowestValue: Double?, highestValue: Double?, lowestIndex: Int?, highestIndex: Int? 783 | 784 | values.enumerated().forEach { (i, currentValue) in 785 | 786 | if currentValue <= value && (lowestValue == nil || lowestValue! < currentValue) { 787 | lowestValue = currentValue 788 | lowestIndex = i 789 | } 790 | if currentValue >= value && (highestValue == nil || highestValue! > currentValue) { 791 | highestValue = currentValue 792 | highestIndex = i 793 | } 794 | 795 | } 796 | return ( 797 | lowestValue: lowestValue, 798 | highestValue: highestValue, 799 | lowestIndex: lowestIndex, 800 | highestIndex: highestIndex 801 | ) 802 | } 803 | 804 | /** 805 | Segment a line in multiple lines when the line touches the x-axis, i.e. separating 806 | positive from negative values. 807 | */ 808 | fileprivate class func segmentLine(_ line: ChartLineSegment, zeroLevel: Double) -> [ChartLineSegment] { 809 | var segments: [ChartLineSegment] = [] 810 | var segment: ChartLineSegment = [] 811 | 812 | line.enumerated().forEach { (i, point) in 813 | segment.append(point) 814 | if i < line.count - 1 { 815 | let nextPoint = line[i+1] 816 | if point.y >= zeroLevel && nextPoint.y < zeroLevel || point.y < zeroLevel && nextPoint.y >= zeroLevel { 817 | // The segment intersects zeroLevel, close the segment with the intersection point 818 | let closingPoint = Chart.intersectionWithLevel(point, and: nextPoint, level: zeroLevel) 819 | segment.append(closingPoint) 820 | segments.append(segment) 821 | // Start a new segment 822 | segment = [closingPoint] 823 | } 824 | } else { 825 | // End of the line 826 | segments.append(segment) 827 | } 828 | } 829 | return segments 830 | } 831 | 832 | /** 833 | Return the intersection of a line between two points and 'y = level' line 834 | */ 835 | fileprivate class func intersectionWithLevel(_ p1: ChartPoint, and p2: ChartPoint, level: Double) -> ChartPoint { 836 | let dy1 = level - p1.y 837 | let dy2 = level - p2.y 838 | return (x: (p2.x * dy1 - p1.x * dy2) / (dy1 - dy2), y: level) 839 | } 840 | } 841 | 842 | extension Sequence where Element == Double { 843 | func minOrZero() -> Double { 844 | return self.min() ?? 0.0 845 | } 846 | func maxOrZero() -> Double { 847 | return self.max() ?? 0.0 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /99_Stocks/3rdPartyLibs/SwiftChart/ChartColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartColors.swift 3 | // 4 | // Created by Giampaolo Bellavite on 07/11/14. 5 | // Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | Shorthands for various colors to use in the charts. 12 | */ 13 | public struct ChartColors { 14 | 15 | static fileprivate func colorFromHex(_ hex: Int) -> UIColor { 16 | let red = CGFloat((hex & 0xFF0000) >> 16) / 255.0 17 | let green = CGFloat((hex & 0xFF00) >> 8) / 255.0 18 | let blue = CGFloat((hex & 0xFF)) / 255.0 19 | return UIColor(red: red, green: green, blue: blue, alpha: 1) 20 | } 21 | 22 | static public func blueColor() -> UIColor { 23 | return colorFromHex(0x4A90E2) 24 | } 25 | static public func orangeColor() -> UIColor { 26 | return colorFromHex(0xF5A623) 27 | } 28 | static public func greenColor() -> UIColor { 29 | return colorFromHex(0x7ED321) 30 | } 31 | static public func darkGreenColor() -> UIColor { 32 | return colorFromHex(0x417505) 33 | } 34 | static public func redColor() -> UIColor { 35 | return colorFromHex(0xFF3200) 36 | } 37 | static public func darkRedColor() -> UIColor { 38 | return colorFromHex(0xD0021B) 39 | } 40 | static public func purpleColor() -> UIColor { 41 | return colorFromHex(0x9013FE) 42 | } 43 | static public func maroonColor() -> UIColor { 44 | return colorFromHex(0x8B572A) 45 | } 46 | static public func pinkColor() -> UIColor { 47 | return colorFromHex(0xBD10E0) 48 | } 49 | static public func greyColor() -> UIColor { 50 | return colorFromHex(0x7f7f7f) 51 | } 52 | static public func cyanColor() -> UIColor { 53 | return colorFromHex(0x50E3C2) 54 | } 55 | static public func goldColor() -> UIColor { 56 | return colorFromHex(0xbcbd22) 57 | } 58 | static public func yellowColor() -> UIColor { 59 | return colorFromHex(0xF8E71C) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /99_Stocks/3rdPartyLibs/SwiftChart/ChartSeries.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartSeries.swift 3 | // 4 | // Created by Giampaolo Bellavite on 07/11/14. 5 | // Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | The `ChartSeries` class create a chart series and configure its appearance and behavior. 12 | */ 13 | open class ChartSeries { 14 | /** 15 | The data used for the chart series. 16 | */ 17 | open var data: [(x: Double, y: Double)] 18 | 19 | /** 20 | When set to `false`, will hide the series line. Useful for drawing only the area with `area=true`. 21 | */ 22 | open var line: Bool = true 23 | 24 | /** 25 | Draws an area below the series line. 26 | */ 27 | open var area: Bool = false 28 | 29 | /** 30 | The series color. 31 | */ 32 | open var color: UIColor = ChartColors.blueColor() { 33 | didSet { 34 | colors = (above: color, below: color, 0) 35 | } 36 | } 37 | 38 | /** 39 | A tuple to specify the color above or below the zero 40 | */ 41 | open var colors: ( 42 | above: UIColor, 43 | below: UIColor, 44 | zeroLevel: Double 45 | ) = (above: ChartColors.blueColor(), below: ChartColors.redColor(), 0) 46 | 47 | public init(_ data: [Double]) { 48 | self.data = [] 49 | data.enumerated().forEach { (x, y) in 50 | let point: (x: Double, y: Double) = (x: Double(x), y: y) 51 | self.data.append(point) 52 | } 53 | } 54 | 55 | public init(data: [(x: Double, y: Double)]) { 56 | self.data = data 57 | } 58 | 59 | public init(data: [(x: Int, y: Double)]) { 60 | self.data = data.map { (Double($0.x), Double($0.y)) } 61 | } 62 | 63 | public init(data: [(x: Float, y: Float)]) { 64 | self.data = data.map { (Double($0.x), Double($0.y)) } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /99_Stocks/99_Stocks.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /99_Stocks/Extensions/Double+Currency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Money.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | var currencyFormatted: String { 13 | let formatter = NumberFormatter() 14 | formatter.locale = Locale.current 15 | formatter.numberStyle = .currency 16 | return formatter.string(from: NSNumber(value: self)) 17 | ?? String(self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /99_Stocks/Extensions/URLSession+Combine.swift: -------------------------------------------------------------------------------- 1 | // URLSession+Combine.swift 2 | // Based on extensions from GitHubSearchWithSwiftUI, by marty-suzuki 3 | // https://github.com/marty-suzuki/GitHubSearchWithSwiftUI 4 | 5 | import Combine 6 | import Foundation 7 | 8 | struct CombineExtension { 9 | let base: Base 10 | 11 | init(_ base: Base) { 12 | self.base = base 13 | } 14 | } 15 | extension URLSession { 16 | var combine: CombineExtension { 17 | return .init(self) 18 | } 19 | } 20 | 21 | extension CombineExtension where Base == URLSession { 22 | 23 | func send(request: URLRequest) -> Future { 24 | 25 | Future { [base] subscriber in 26 | 27 | let task = base.dataTask(with: request) { data, response, error in 28 | 29 | guard let response = response as? HTTPURLResponse else { 30 | subscriber(.failure(.invalidResponse)) 31 | return 32 | } 33 | 34 | guard 200..<300 ~= response.statusCode else { 35 | let sessionError: URLSessionError 36 | if let data = data { 37 | sessionError = .serverErrorMessage(statusCode: response.statusCode, 38 | data: data) 39 | } else { 40 | sessionError = .serverError(statusCode: response.statusCode, 41 | error: error) 42 | } 43 | subscriber(.failure(sessionError)) 44 | return 45 | } 46 | 47 | guard let data = data else { 48 | subscriber(.failure(.noData)) 49 | return 50 | } 51 | 52 | if let error = error { 53 | subscriber(.failure(.unknown(error))) 54 | } else { 55 | subscriber(.success(data)) 56 | } 57 | } 58 | 59 | task.resume() 60 | } 61 | } 62 | } 63 | 64 | enum URLSessionError: Error { 65 | case invalidResponse 66 | case noData 67 | case serverErrorMessage(statusCode: Int, data: Data) 68 | case serverError(statusCode: Int, error: Error?) 69 | case unknown(Error) 70 | } 71 | -------------------------------------------------------------------------------- /99_Stocks/GeneratedCode/APIConfigurationSourcery.generated.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 0.16.1 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | 5 | 6 | import Foundation 7 | import Combine 8 | 9 | // MARK: - Company.Network.Endpoint 10 | 11 | internal extension Company.Network.Endpoint { 12 | 13 | var baseURL: URLConvertible { 14 | return "https://mobile.ninetynine.com/testapi/1/companies" 15 | } 16 | 17 | var method: HTTPMethod { 18 | switch self { 19 | case .companies: 20 | return .get 21 | case .company: 22 | return .get 23 | } 24 | } 25 | 26 | var path: String { 27 | switch self { 28 | case .companies: 29 | return "/" 30 | case .company(let id): 31 | return "/\(id)" 32 | } 33 | } 34 | 35 | var queryItems: [URLQueryItem] { 36 | switch self { 37 | case .companies: 38 | return [] 39 | case .company: 40 | return [] 41 | } 42 | } 43 | 44 | var bodyParameters: [String : Any]? { 45 | switch self { 46 | case .companies: return nil 47 | case .company: return nil 48 | } 49 | } 50 | } 51 | 52 | // MARK: [API Client] protocol and extension 53 | 54 | // By creating this protocol, we could create a mock APIClient (or just another api client in general) that implements 55 | // these methods 56 | protocol CompanyNetworkApiClientProtocol { 57 | static func companies() -> AnyPublisher<[Company.Model.ItemListResponse], Error> 58 | static func company(id: Int) -> AnyPublisher 59 | } 60 | 61 | extension Company.Network.ApiClient: CompanyNetworkApiClientProtocol { 62 | static func companies() -> AnyPublisher<[Company.Model.ItemListResponse], Error> { 63 | return self.request(.companies) 64 | } 65 | static func company(id: Int) -> AnyPublisher { 66 | return self.request(.company(id: id)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /99_Stocks/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 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /99_Stocks/Modules/-App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillTerminate(_ application: UIApplication) { 22 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 23 | } 24 | 25 | // MARK: UISceneSession Lifecycle 26 | 27 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 31 | } 32 | 33 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 34 | // Called when the user discards a scene session. 35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /99_Stocks/Modules/-App/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /99_Stocks/Modules/-App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | if let windowScene = scene as? UIWindowScene { 23 | let window = UIWindow(windowScene: windowScene) 24 | window.rootViewController = UIHostingController(rootView: SceneRouter.initialView) 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | 30 | func sceneDidDisconnect(_ scene: UIScene) { 31 | // Called as the scene is being released by the system. 32 | // This occurs shortly after the scene enters the background, or when its session is discarded. 33 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 34 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 35 | } 36 | 37 | func sceneDidBecomeActive(_ scene: UIScene) { 38 | // Called when the scene has moved from an inactive state to an active state. 39 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 40 | } 41 | 42 | func sceneWillResignActive(_ scene: UIScene) { 43 | // Called when the scene will move from an active state to an inactive state. 44 | // This may occur due to temporary interruptions (ex. an incoming phone call). 45 | } 46 | 47 | func sceneWillEnterForeground(_ scene: UIScene) { 48 | // Called as the scene transitions from the background to the foreground. 49 | // Use this method to undo the changes made on entering the background. 50 | } 51 | 52 | func sceneDidEnterBackground(_ scene: UIScene) { 53 | // Called as the scene transitions from the foreground to the background. 54 | // Use this method to save data, release shared resources, and store enough scene-specific state information 55 | // to restore the scene back to its current state. 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /99_Stocks/Modules/-App/SceneRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneRouter.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | /// We will use routers for the app and all the modules, to easily instantiate the views 13 | /// and/or provide multiple different values (like a mock view with mock data) 14 | struct SceneRouter { 15 | static var initialView: some View { 16 | Company.Router.ItemList.instance 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Company.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Company.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // We will use `Company` as a "namespace" 12 | enum Company { // MVVM+RN 13 | 14 | enum Model {} 15 | enum View {} 16 | enum ViewModel {} 17 | 18 | enum Router {} 19 | enum Network {} 20 | } 21 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/Company.Model.StockName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StockName.swift 3 | // 99_Stocks 4 | // 5 | // Created by Daniel Illescas Romero on 09/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Model { 12 | 13 | /// This will allow us match them againts predefined images 14 | /// of the companies that are located in the assets folder 15 | enum StockName: String { 16 | 17 | case apple = "APPL" 18 | case amazon = "AMZN" 19 | case microsoft = "MSFT" 20 | case alphabet = "GOOG" 21 | case facebook = "FB" 22 | case berkshire = "BRK.A" 23 | case alibaba = "BABA" 24 | case johnson = "JNJ" 25 | case jpmorgan = "JPM" 26 | 27 | var companyIcon: Image { 28 | typealias I = Images.Companies 29 | switch self { 30 | case .apple: return I.apple 31 | case .amazon: return I.amazon 32 | case .microsoft: return I.microsoft 33 | case .alphabet: return I.alphabet 34 | case .facebook: return I.facebook 35 | case .berkshire: return I.berkshire 36 | case .alibaba: return I.alibaba 37 | case .johnson: return I.johnson 38 | case .jpmorgan: return I.jpmorgan 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/Company.Model.StockState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyStockState.swift 3 | // 99_Stocks 4 | // 5 | // Created by Daniel Illescas Romero on 09/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Model { 12 | 13 | /// For when a price drops, goes up or states the same 14 | enum SharePriceState: String { 15 | case neutral 16 | case up 17 | case down 18 | 19 | var directionImage: Image? { 20 | switch self { 21 | case .neutral: 22 | return nil//Images.Companies.SharePriceState.neutral 23 | case .up: 24 | return Images.Companies.SharePriceState.up 25 | case .down: 26 | return Images.Companies.SharePriceState.down 27 | } 28 | } 29 | var color: Color { 30 | switch self { 31 | case .neutral: 32 | return Colors.Companies.Currency.neutral 33 | case .up: 34 | return Colors.Companies.Currency.up 35 | case .down: 36 | return Colors.Companies.Currency.down 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/Country.Model.Country.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Country.swift 3 | // 99_Stocks 4 | // 5 | // Created by Daniel Illescas Romero on 09/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Company.Model { 12 | 13 | /// If we match the country, we can put their flag 14 | enum Country: String { 15 | case usa = "United States of America" 16 | case spain = "Spain" 17 | case italy = "Italy" 18 | 19 | var emojiFlag: String { 20 | switch self { 21 | case .usa: return "🇺🇸" 22 | case .spain: return "🇪🇸" 23 | case .italy: return "🇮🇹" 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/DetailItem/Company.Model.DetailItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyDetail.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Model { 12 | struct DetailItem: Hashable, Identifiable { 13 | let id: Int 14 | let name: String 15 | let stockName: String 16 | let sharePrice: Double 17 | let description: String 18 | let country: String 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/DetailItem/Company.Model.DetailItemResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyDetailResponse.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Company.Model { 12 | /* 13 | - EXAMPLE 14 | { 15 | "id": 8, 16 | "name": "Johnson & Johnson", 17 | "ric": "JNJ", 18 | "sharePrice": 155.817, 19 | "description": "Lorem ipsum is your friend ;)", 20 | "country": "United States of America" 21 | } 22 | */ 23 | /// We asume no fields will be nil and every field will be present, else the field should be Optional 24 | /// On a real world app, as we have the real full API specification we would already know which fields are nullable or not 25 | struct DetailItemResponse: Decodable { 26 | 27 | let id: Int 28 | let name: String 29 | let stockName: String 30 | let sharePrice: Double 31 | let description: String 32 | let country: String 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case id, name 36 | case stockName = "ric" 37 | case sharePrice, description, country 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/ItemList/Company.Model.ItemList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyEntry.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Model { 12 | struct ItemList: Hashable, Identifiable { 13 | let id: Int 14 | let name: String 15 | let stockName: String 16 | let sharePrice: Double 17 | } 18 | } 19 | extension Company.Model.ItemList: Comparable { 20 | static func < (lhs: Company.Model.ItemList, rhs: Company.Model.ItemList) -> Bool { 21 | return lhs.sharePrice < rhs.sharePrice 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Models/ItemList/Company.Model.ItemListResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyEntryResponse.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Company.Model { 12 | /* 13 | - EXAMPLE 14 | { 15 | "id": 1, 16 | "name": "Apple Inc.", 17 | "ric": "APPL", 18 | "sharePrice": 226.304 19 | } 20 | */ 21 | /// We asume no fields will be nil and every field will be present, else the field should be Optional 22 | /// On a real world app, as we have the real full API specification we would already know which fields are nullable or not 23 | struct ItemListResponse: Decodable { 24 | 25 | let id: Int 26 | let name: String 27 | let stockName: String 28 | let sharePrice: Double 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case id, name 32 | case stockName = "ric" 33 | case sharePrice 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Network/Company.Network.APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyAPIClient.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | extension Company.Network { 13 | // The implementation was automatically generated, 14 | // you can find the generated code under the "GeneratedCode" folder 15 | enum ApiClient: APIClient { 16 | typealias APIConfigType = Company.Network.Endpoint 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/Network/Company.Network.Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyEndpoint.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Company.Network { 12 | // The `APIConfiguration` implementation was automatically generated, 13 | // you can find the generated code under the "GeneratedCode" folder 14 | // sourcery: url = "https://mobile.ninetynine.com/testapi/1/companies" 15 | enum Endpoint: APIConfiguration { 16 | // sourcery: response = [Company.Model.ItemListResponse] 17 | case companies 18 | // sourcery: path = "/\(id)", response = Company.Model.DetailItemResponse 19 | case company(id: Int) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/DetailItem/Company.Router.DetailItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyDetilRouter.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Router { 12 | enum DetailItem { 13 | static func instance(companyId: Company.Model.ItemList.ID) -> some View { 14 | Company.View.DetailItem().environmentObject( 15 | Company.ViewModel.DetailItem(companyId: companyId, scheduleToReloadEvery: 3) 16 | ) 17 | } 18 | #if DEBUG 19 | static var mockInstance: some View { 20 | Company.View.DetailItem().environmentObject( 21 | Company.ViewModel.DetailItem(companyId: 1) 22 | ) 23 | } 24 | #endif 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/DetailItem/Company.View.DetailItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompaniesDetailView.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.View { 12 | 13 | struct DetailItem: View { 14 | 15 | @EnvironmentObject 16 | var companyDetailsVM: Company.ViewModel.DetailItem 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .center, spacing: 40) { 21 | self.companyDetailsVM.image? 22 | .resizable() 23 | .aspectRatio(contentMode: .fit) 24 | .frame(width: 120, height: 120) 25 | .clipShape(Circle()) 26 | .shadow(radius: 2.5) 27 | .padding(.top, 20) 28 | VStack(alignment: .center, spacing: 8) { 29 | Text(self.companyDetailsVM.stockNameAndPossiblyItsFlag()) 30 | .font(.system(.title, design: .monospaced)) 31 | .fontWeight(.bold) 32 | .padding(.vertical, self.companyDetailsVM.image == nil ? 16 : 0) 33 | HStack { 34 | self.companyDetailsVM.stockState.directionImage? 35 | .resizable() 36 | .aspectRatio(contentMode: .fit) 37 | .frame(width: 10, height: 10) 38 | .foregroundColor(self.companyDetailsVM.stockState.color) 39 | Text(self.companyDetailsVM.company.sharePrice.currencyFormatted) 40 | .font(.system(.title, design: .monospaced)) 41 | .fontWeight(.bold) 42 | .foregroundColor(self.companyDetailsVM.stockState.color) 43 | } 44 | Text(self.companyDetailsVM.company.description) 45 | .font(.body) 46 | .padding(.vertical, 16) 47 | .lineLimit(5) 48 | .allowsTightening(true) 49 | Spacer(minLength: 20) 50 | ChartView(data: self.companyDetailsVM.sharePrices) 51 | .frame(minHeight: 215) 52 | .padding(.all, 30) 53 | } 54 | Spacer() 55 | } 56 | .frame(width: min(UIScreen.main.bounds.width, UIScreen.main.bounds.height), alignment: .center) 57 | } 58 | .navigationBarTitle( 59 | Text(self.companyDetailsVM.company.name) 60 | ) 61 | .navigationBarItems(trailing: 62 | Button(action: { 63 | self.companyDetailsVM.reload() 64 | }, label: { 65 | HStack { 66 | Text(L10n.Sections.Companies.Button.reload) 67 | Images.System.reloadIcon 68 | } 69 | }) 70 | ) 71 | } 72 | } 73 | } 74 | 75 | #if DEBUG 76 | struct CompanyDetailView_Previews : PreviewProvider { 77 | static var previews: some View { 78 | Company.Router.DetailItem.mockInstance 79 | } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/DetailItem/Company.ViewModel.DetailItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyDetails.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | extension Company.ViewModel { 14 | 15 | final class DetailItem: ObservableObject { 16 | 17 | internal let didChange = PassthroughSubject() 18 | 19 | private let companyId: Company.Model.ItemList.ID 20 | private(set) var stockState: Company.Model.SharePriceState = .neutral { 21 | didSet { 22 | didChange.send(self) 23 | } 24 | } 25 | 26 | private var previousStock: (state: Company.Model.SharePriceState, price: Double)? 27 | private(set) var sharePrices: [(x: Int, y: Double)] = [] { 28 | didSet { 29 | didChange.send(self) 30 | } 31 | } 32 | 33 | var data: Company.Model.DetailItem? { 34 | didSet { 35 | didChange.send(self) 36 | } 37 | } 38 | 39 | init(companyId: Company.Model.ItemList.ID, initialData: Company.Model.DetailItem? = nil, scheduleToReloadEvery timeInterval: TimeInterval? = 15) { 40 | self.companyId = companyId 41 | self.data = initialData 42 | fetchCompanyById() 43 | if let timeInterval = timeInterval { 44 | reloadEvery(timeInterval) 45 | } 46 | } 47 | 48 | func reload() { 49 | self.fetchCompanyById() 50 | } 51 | 52 | var image: Image? { 53 | guard let stockName = Company.Model.StockName(rawValue: self.company.stockName) else { return nil } 54 | return stockName.companyIcon 55 | } 56 | 57 | // MARK: - Convenience 58 | 59 | private func fetchCompanyById() { 60 | _ = Company.Network.ApiClient.company(id: self.companyId).map { 61 | Company.Model.DetailItem(id: $0.id, name: $0.name, stockName: $0.stockName, sharePrice: $0.sharePrice, description: $0.description, country: $0.country) 62 | }.sink(receiveCompletion: { _ in }, receiveValue: { result in 63 | DispatchQueue.main.async { 64 | self.data = result 65 | if let previousStock = self.previousStock { 66 | if result.sharePrice == previousStock.price && previousStock.state != .neutral { 67 | self.previousStock = (.neutral, result.sharePrice) 68 | self.stockState = .neutral 69 | } else if result.sharePrice > previousStock.price && previousStock.state != .up { 70 | self.previousStock = (.up, result.sharePrice) 71 | self.stockState = .up 72 | } else if result.sharePrice < previousStock.price && previousStock.state != .down { 73 | self.previousStock = (.down, result.sharePrice) 74 | self.stockState = .down 75 | } 76 | } else { 77 | self.previousStock = (.neutral, result.sharePrice) 78 | } 79 | let previousCount = self.sharePrices.count 80 | self.sharePrices.append((x: previousCount, y: result.sharePrice)) 81 | } 82 | }) 83 | } 84 | 85 | private func reloadEvery(_ timeInterval: TimeInterval) { 86 | Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) {_ in 87 | self.fetchCompanyById() 88 | } 89 | } 90 | } 91 | 92 | } 93 | 94 | extension Company.ViewModel.DetailItem { 95 | var company: Company.Model.DetailItem { 96 | if let companyDetail = self.data { 97 | return companyDetail 98 | } 99 | return .init(id: 0, name: "-", stockName: "-", sharePrice: 0, description: "", country: "") 100 | } 101 | func stockNameAndPossiblyItsFlag() -> String { 102 | if let countryFlag = Company.Model.Country(rawValue: self.company.country)?.emojiFlag { 103 | return self.company.stockName + " " + countryFlag 104 | } else { 105 | return self.company.stockName 106 | } 107 | } 108 | } 109 | #if DEBUG 110 | /// Only used in the preview and maybe when the user doesn't have internet (not implemented) 111 | extension Company.ViewModel.DetailItem { 112 | static var mockData: Company.Model.DetailItem { 113 | .init(id: 1, 114 | name: "Apple", 115 | stockName: "APPL", 116 | sharePrice: 1023.1, 117 | description: "Lorem ipsum is your friend ;)", 118 | country: "United States of America") 119 | } 120 | } 121 | #endif 122 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/ItemList/Company.Router.ItemList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompaniesViewRouter.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.Router { 12 | enum ItemList { 13 | static var instance: some View { 14 | Company.View.ItemList().environmentObject( 15 | Company.ViewModel.ItemList(initialData: [], scheduleToReloadEvery: 3) 16 | ) 17 | } 18 | #if DEBUG 19 | static var mockInstance: some View { 20 | Company.View.ItemList().environmentObject( 21 | Company.ViewModel.ItemList(initialData: Company.ViewModel.ItemList.mockData, 22 | scheduleToReloadEvery: 3) 23 | ) 24 | } 25 | #endif 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/ItemList/Company.View.ItemList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.View { 12 | 13 | struct ItemList : View { 14 | 15 | @EnvironmentObject 16 | var companies: Company.ViewModel.ItemList 17 | 18 | var body: some View { 19 | NavigationView { 20 | VStack(spacing: 16) { 21 | TextField(L10n.Sections.Companies.TextField.search, text: $companies.searchText, onCommit: onTextFieldCommit) 22 | .frame(height: 40) 23 | .padding(.horizontal, 16) 24 | .border(Color.gray) 25 | .cornerRadius(12) 26 | .padding(.horizontal, 12) 27 | 28 | if (companies.realData.isEmpty) { 29 | Spacer() 30 | if companies.searchText.isEmpty { 31 | ActivityIndicatorView(style: .large) 32 | .setAnimating(true) 33 | } else { 34 | Text(L10n.Sections.Companies.Label.emptyResults) 35 | } 36 | Spacer() 37 | } else { 38 | List(companies.realData) { (company: Company.Model.ItemList) in 39 | NavigationLink(destination: Company.Router.DetailItem.instance(companyId: company.id)) { 40 | Company.View.ListItem(company: company, 41 | stockState: self.companies.stockState(for: company.id)) 42 | } 43 | } 44 | } 45 | } 46 | .navigationBarTitle(Text(L10n.Sections.Companies.Label.title)) 47 | .navigationBarItems(trailing: 48 | Button(action: { 49 | self.companies.reload() 50 | }, label: { 51 | HStack { 52 | Text(L10n.Sections.Companies.Button.reload) 53 | Images.System.reloadIcon 54 | } 55 | }) 56 | ) 57 | } 58 | } 59 | 60 | // 61 | 62 | func onTextFieldCommit() { 63 | #if canImport(UIKit) 64 | UIApplication.shared.keyWindow?.endEditing(true) 65 | #endif 66 | } 67 | } 68 | } 69 | 70 | #if DEBUG 71 | struct ContentView_Previews : PreviewProvider { 72 | static var previews: some View { 73 | Company.Router.ItemList.mockInstance 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/ItemList/Company.ViewModel.ItemList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompaniesViewModel.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | extension Company.ViewModel { 14 | 15 | final class ItemList: ObservableObject { 16 | 17 | // MARK: - Properties 18 | 19 | /// Whenever a property that we specify changes, the view will be updated by sending a signal to it 20 | internal let didChange = PassthroughSubject() 21 | 22 | /// Dictionary indicating the stock state (neutral, growing or falling) and its prices, accessed by a company ID 23 | private(set) var stockState: [Company.Model.ItemList.ID: (state: Company.Model.SharePriceState, price: Double)] = [:] { 24 | didSet { 25 | didChange.send(self) 26 | } 27 | } 28 | private var data: [Company.Model.ItemList] = [] { 29 | didSet { 30 | didChange.send(self) 31 | } 32 | } 33 | private var filteredData: [Company.Model.ItemList] = [] { 34 | didSet { 35 | didChange.send(self) 36 | } 37 | } 38 | 39 | var realData: [Company.Model.ItemList] { 40 | return self.searchText.isEmpty ? self.data : self.filteredData 41 | } 42 | 43 | var searchText: String = "" { 44 | didSet { 45 | if !self.searchText.isEmpty { 46 | let lowercasedText = self.searchText.lowercased() 47 | 48 | self.filteredData = self.data.filter { $0.name.lowercased().range(of: lowercasedText) != nil }.sorted(by: { (lhs, rhs) in 49 | lhs.name.hasPrefix(self.searchText) 50 | }) 51 | if self.filteredData.isEmpty { 52 | self.filteredData = self.data.filter { $0.stockName.lowercased().range(of: lowercasedText) != nil }.sorted(by: { (lhs, rhs) in 53 | lhs.stockName.hasPrefix(self.searchText) 54 | }) 55 | } 56 | if self.filteredData.isEmpty { 57 | self.filteredData = self.data.filter { String($0.sharePrice).range(of: self.searchText) != nil }.sorted(by: { (lhs, rhs) in 58 | String(lhs.sharePrice).hasPrefix(self.searchText) 59 | }) 60 | } 61 | } 62 | didChange.send(self) 63 | } 64 | } 65 | 66 | // MARK: - Initializers 67 | 68 | init(initialData: [Company.Model.ItemList] = [], scheduleToReloadEvery timeInterval: TimeInterval? = 15) { 69 | fetchCompanies() 70 | if let timeInterval = timeInterval { 71 | reloadEvery(timeInterval) 72 | } 73 | } 74 | 75 | // MARK: - Public methods 76 | 77 | func reload() { 78 | self.fetchCompanies() 79 | } 80 | 81 | func stockState(for companyId: Company.Model.ItemList.ID) -> Company.Model.SharePriceState { 82 | stockState[companyId, default: (.neutral, 0)].state 83 | } 84 | 85 | // MARK: - Convenience 86 | 87 | private func fetchCompanies() { 88 | _ = Company.Network.ApiClient.companies().map { response in 89 | response.map { Company.Model.ItemList(id: $0.id, name: $0.name, stockName: $0.stockName, sharePrice: $0.sharePrice) } 90 | }.sink(receiveCompletion: { _ in }, receiveValue: { result in 91 | DispatchQueue.main.async { 92 | self.data = result.sorted(by: >) 93 | /// Updates the stockState dictionary accordingly, so it later changes the share price colors in the view 94 | if self.stockState.isEmpty { 95 | for company in self.data { 96 | self.stockState[company.id] = (.neutral, company.sharePrice) 97 | } 98 | } else { 99 | for company in self.data { 100 | if let (previousStockState, previousPrice) = self.stockState[company.id] { 101 | if company.sharePrice == previousPrice && previousStockState != .neutral { 102 | self.stockState[company.id] = (.neutral, company.sharePrice) 103 | } 104 | else if company.sharePrice > previousPrice && previousStockState != .up { 105 | self.stockState[company.id] = (.up, company.sharePrice) 106 | } 107 | else if company.sharePrice < previousPrice && previousStockState != .down { 108 | self.stockState[company.id] = (.down, company.sharePrice) 109 | } 110 | } 111 | } 112 | } 113 | } 114 | }) 115 | } 116 | 117 | private func reloadEvery(_ timeInterval: TimeInterval) { 118 | Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) {_ in 119 | if self.searchText.isEmpty { 120 | self.fetchCompanies() 121 | } 122 | } 123 | } 124 | } 125 | } 126 | #if DEBUG 127 | extension Company.ViewModel.ItemList { 128 | /// Only used in the preview and maybe when the user doesn't have internet (not implemented) 129 | static var mockData: [Company.Model.ItemList] {[ 130 | .init(id: 1, name: "Apple", stockName: "APPL", sharePrice: 226.304), 131 | .init(id: 2, name: "Microsoft Corporation", stockName: "MSFT", sharePrice: 104.799), 132 | .init(id: 3, name: "Alphabet Inc.", stockName: "GOOG", sharePrice: 1124.317), 133 | .init(id: 4, name: "Amazon.com", stockName: "AMZN", sharePrice: 1900.535), 134 | .init(id: 5, name: "Facebook", stockName: "FB", sharePrice: 164.963), 135 | .init(id: 6, name: "Berkshire Hathaway", stockName: "BRK.A", sharePrice: 339465.146), 136 | .init(id: 7, name: "Alibaba Group Holding Ltd", stockName: "BABA", sharePrice: 141.808), 137 | .init(id: 8, name: "Johnson & Johnson", stockName: "JNJ", sharePrice: 151.82), 138 | .init(id: 9, name: "JPMorgan Chase & Co.", stockName: "JPM", sharePrice: 120.593), 139 | .init(id: 10, name: "ExxonMobil Corporation", stockName: "XOM", sharePrice: 84.103) 140 | ]} 141 | } 142 | #endif 143 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/ItemList/SupportingViews/CompaniesListItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompaniesListView.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Company.View { 12 | struct ListItem : View { 13 | 14 | let company: Company.Model.ItemList 15 | 16 | let stockState: Company.Model.SharePriceState 17 | 18 | var body: some View { 19 | HStack(alignment: .center, spacing: 16) { 20 | VStack(alignment: .leading, spacing: 8) { 21 | Text(company.name) 22 | .font(.headline) 23 | Text(company.stockName) 24 | .font(.footnote) 25 | .foregroundColor(.gray) 26 | } 27 | Spacer() 28 | HStack { 29 | self.stockState.directionImage? 30 | .resizable() 31 | .aspectRatio(contentMode: .fit) 32 | .frame(width: 10, height: 10) 33 | .foregroundColor(self.stockState.color) 34 | Text(company.sharePrice.currencyFormatted) 35 | .font(.system(.headline, design: .monospaced)) 36 | .foregroundColor(self.stockState.color) 37 | } 38 | } 39 | .padding(.vertical, 8) 40 | } 41 | } 42 | } 43 | 44 | #if DEBUG 45 | struct CompaniesListItemView_Previews : PreviewProvider { 46 | static var previews: some View { 47 | Company.View.ListItem(company: .init(id: 0, name: "Apple", stockName: "AMZN", sharePrice: 1000.5), 48 | stockState: .neutral) 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /99_Stocks/Modules/Companies/SubModules/SupportingViews/ChartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartView.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Simple adaptation of gpbl/SwiftChart to SwiftUI 12 | final class ChartView: UIViewRepresentable, ChartDelegate { 13 | 14 | typealias UIViewType = Chart 15 | 16 | let data: [(x: Int, y: Double)] 17 | 18 | init(data: [(x: Int, y: Double)]) { 19 | self.data = data 20 | } 21 | 22 | // MARK: - UIViewRepresentable 23 | 24 | func makeUIView(context: UIViewRepresentableContext) -> Chart { 25 | let chart = Chart() 26 | chart.delegate = self 27 | chart.hideHighlightLineOnTouchEnd = true 28 | 29 | let series = ChartSeries(data: self.data) 30 | series.area = true 31 | chart.add(series) 32 | 33 | return chart 34 | } 35 | 36 | func updateUIView(_ uiView: Chart, context: UIViewRepresentableContext) { 37 | 38 | uiView.layoutIfNeeded() 39 | 40 | uiView.removeAllSeries() 41 | 42 | let series = ChartSeries(data: self.data) 43 | series.area = true 44 | uiView.add(series) 45 | 46 | if self.data.count > 10 { 47 | uiView.xLabelsFormatter = { (_,_) in "" } 48 | } else { 49 | uiView.xLabelsFormatter = { (_,value) in "\(value)" } 50 | } 51 | 52 | switch context.environment.colorScheme { 53 | case .light: 54 | uiView.labelColor = .black 55 | case .dark: 56 | uiView.labelColor = .white 57 | @unknown default: 58 | break 59 | } 60 | } 61 | 62 | // MARK: - ChartDelegate 63 | 64 | // Maybe we could show the money ammount where the user is tapping 65 | // But where do we show it or how? Does it make sense? Is it worth it? 66 | 67 | func didTouchChart(_ chart: Chart, indexes: [Int?], x: Double, left: CGFloat) { 68 | 69 | } 70 | 71 | func didFinishTouchingChart(_ chart: Chart) { 72 | 73 | } 74 | 75 | func didEndTouchingChart(_ chart: Chart) { 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /99_Stocks/Modules/General/SupportingViews/ActivityIndicatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicatorView.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | final class ActivityIndicatorView: UIViewRepresentable { 12 | 13 | typealias UIViewType = UIActivityIndicatorView 14 | 15 | var view: UIActivityIndicatorView 16 | private var _isAnimating: Bool = false 17 | 18 | init(style: UIActivityIndicatorView.Style = .medium) { 19 | self.view = UIActivityIndicatorView(style: style) 20 | self.view.hidesWhenStopped = true 21 | } 22 | 23 | // MARK: - Public methods 24 | 25 | func setAnimating(_ isAnimating: Bool) -> Self { 26 | _isAnimating = isAnimating 27 | return self 28 | } 29 | 30 | // MARK: - UIViewRepresentable 31 | 32 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { 33 | self.view.frame = self.defaultFrame 34 | return self.view 35 | } 36 | 37 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { 38 | uiView.layoutIfNeeded() 39 | uiView.color = context.environment.colorScheme == .dark ? .lightGray : .gray 40 | if !_isAnimating { 41 | uiView.stopAnimating() 42 | uiView.isHidden = true 43 | uiView.frame = .zero 44 | } else { 45 | uiView.startAnimating() 46 | uiView.isHidden = false 47 | uiView.frame = self.defaultFrame 48 | } 49 | } 50 | 51 | // MARK: - Convenience 52 | 53 | private var defaultFrame: CGRect { .init(x: 0, y: 0, width: 50, height: 50) } 54 | } 55 | -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon108x108@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon108x108@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon24x24@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon27.5x27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon27.5x27.5@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon44x44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon44x44@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon50x50@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon86x86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon86x86@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon98x98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon98x98@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-Notification@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-Notification@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small-40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-Small-40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-Notification.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-Notification@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-Small.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-Small@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-Small-40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-Small-40@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "icon.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "24x24", 113 | "idiom" : "watch", 114 | "filename" : "AppIcon24x24@2x.png", 115 | "scale" : "2x", 116 | "role" : "notificationCenter", 117 | "subtype" : "38mm" 118 | }, 119 | { 120 | "size" : "27.5x27.5", 121 | "idiom" : "watch", 122 | "filename" : "AppIcon27.5x27.5@2x.png", 123 | "scale" : "2x", 124 | "role" : "notificationCenter", 125 | "subtype" : "42mm" 126 | }, 127 | { 128 | "size" : "29x29", 129 | "idiom" : "watch", 130 | "filename" : "AppIcon29x29@2x.png", 131 | "role" : "companionSettings", 132 | "scale" : "2x" 133 | }, 134 | { 135 | "size" : "29x29", 136 | "idiom" : "watch", 137 | "filename" : "AppIcon29x29@3x.png", 138 | "role" : "companionSettings", 139 | "scale" : "3x" 140 | }, 141 | { 142 | "size" : "40x40", 143 | "idiom" : "watch", 144 | "filename" : "AppIcon40x40@2x.png", 145 | "scale" : "2x", 146 | "role" : "appLauncher", 147 | "subtype" : "38mm" 148 | }, 149 | { 150 | "size" : "44x44", 151 | "idiom" : "watch", 152 | "filename" : "AppIcon44x44@2x.png", 153 | "scale" : "2x", 154 | "role" : "appLauncher", 155 | "subtype" : "40mm" 156 | }, 157 | { 158 | "size" : "50x50", 159 | "idiom" : "watch", 160 | "filename" : "AppIcon50x50@2x.png", 161 | "scale" : "2x", 162 | "role" : "appLauncher", 163 | "subtype" : "44mm" 164 | }, 165 | { 166 | "size" : "86x86", 167 | "idiom" : "watch", 168 | "filename" : "AppIcon86x86@2x.png", 169 | "scale" : "2x", 170 | "role" : "quickLook", 171 | "subtype" : "38mm" 172 | }, 173 | { 174 | "size" : "98x98", 175 | "idiom" : "watch", 176 | "filename" : "AppIcon98x98@2x.png", 177 | "scale" : "2x", 178 | "role" : "quickLook", 179 | "subtype" : "42mm" 180 | }, 181 | { 182 | "size" : "108x108", 183 | "idiom" : "watch", 184 | "filename" : "AppIcon108x108@2x.png", 185 | "scale" : "2x", 186 | "role" : "quickLook", 187 | "subtype" : "44mm" 188 | }, 189 | { 190 | "size" : "1024x1024", 191 | "idiom" : "watch-marketing", 192 | "filename" : "watchicon.png", 193 | "scale" : "1x" 194 | }, 195 | { 196 | "size" : "16x16", 197 | "idiom" : "mac", 198 | "filename" : "icon_16x16.png", 199 | "scale" : "1x" 200 | }, 201 | { 202 | "size" : "16x16", 203 | "idiom" : "mac", 204 | "filename" : "icon_16x16@2x.png", 205 | "scale" : "2x" 206 | }, 207 | { 208 | "size" : "32x32", 209 | "idiom" : "mac", 210 | "filename" : "icon_32x32.png", 211 | "scale" : "1x" 212 | }, 213 | { 214 | "size" : "32x32", 215 | "idiom" : "mac", 216 | "filename" : "icon_32x32@2x.png", 217 | "scale" : "2x" 218 | }, 219 | { 220 | "size" : "128x128", 221 | "idiom" : "mac", 222 | "filename" : "icon_128x128.png", 223 | "scale" : "1x" 224 | }, 225 | { 226 | "size" : "128x128", 227 | "idiom" : "mac", 228 | "filename" : "icon_128x128@2x.png", 229 | "scale" : "2x" 230 | }, 231 | { 232 | "size" : "256x256", 233 | "idiom" : "mac", 234 | "filename" : "icon_256x256.png", 235 | "scale" : "1x" 236 | }, 237 | { 238 | "size" : "256x256", 239 | "idiom" : "mac", 240 | "filename" : "icon_256x256@2x.png", 241 | "scale" : "2x" 242 | }, 243 | { 244 | "size" : "512x512", 245 | "idiom" : "mac", 246 | "filename" : "icon_512x512.png", 247 | "scale" : "1x" 248 | }, 249 | { 250 | "size" : "512x512", 251 | "idiom" : "mac", 252 | "filename" : "icon_512x512@2x.png", 253 | "scale" : "2x" 254 | } 255 | ], 256 | "info" : { 257 | "version" : 1, 258 | "author" : "xcode" 259 | } 260 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/watchicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/watchicon.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Companies/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Companies/Currency/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Companies/Currency/down.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "platform" : "ios", 11 | "reference" : "systemRedColor" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Companies/Currency/neutral.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "platform" : "ios", 11 | "reference" : "systemBlueColor" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Companies/Currency/up.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "platform" : "ios", 11 | "reference" : "systemGreenColor" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/alibaba.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alibaba.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/alibaba.imageset/alibaba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/alibaba.imageset/alibaba.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/alphabet.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alphabet.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/alphabet.imageset/alphabet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/alphabet.imageset/alphabet.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/amazon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "amzn.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/amazon.imageset/amzn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/amazon.imageset/amzn.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/apple.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "apple.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/apple.imageset/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/apple.imageset/apple.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/berkshire.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "berkshire.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/berkshire.imageset/berkshire.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/berkshire.imageset/berkshire.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/facebook.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "fb.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/facebook.imageset/fb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/facebook.imageset/fb.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/johnson.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "johnson.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/johnson.imageset/johnson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/johnson.imageset/johnson.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/jpmorgan.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "jpmorgan.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/jpmorgan.imageset/jpmorgan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/jpmorgan.imageset/jpmorgan.jpg -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/microsoft.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "microsoft.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Companies/microsoft.imageset/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/Companies/microsoft.imageset/microsoft.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/StocksIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "StocksIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/Assets.xcassets/Images/StocksIcon.imageset/StocksIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/99_Stocks/Resources/Assets.xcassets/Images/StocksIcon.imageset/StocksIcon.png -------------------------------------------------------------------------------- /99_Stocks/Resources/Colors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Colors.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | enum Colors { 12 | enum Companies { 13 | enum Currency { 14 | @Asset("Companies/\(Self.self)/up") 15 | static var up: Color 16 | @Asset("Companies/\(Self.self)/down") 17 | static var down: Color 18 | @Asset("Companies/\(Self.self)/neutral") 19 | static var neutral: Color 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /99_Stocks/Resources/Images.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Images.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | enum Images { 12 | 13 | enum Companies { 14 | 15 | @Asset("\(Self.self)/amazon") 16 | static var amazon: Image 17 | 18 | @Asset("\(Self.self)/alibaba") 19 | static var alibaba: Image 20 | 21 | @Asset("\(Self.self)/alphabet") 22 | static var alphabet: Image 23 | 24 | @Asset("\(Self.self)/apple") 25 | static var apple: Image 26 | 27 | @Asset("\(Self.self)/berkshire") 28 | static var berkshire: Image 29 | 30 | @Asset("\(Self.self)/facebook") 31 | static var facebook: Image 32 | 33 | @Asset("\(Self.self)/johnson") 34 | static var johnson: Image 35 | 36 | @Asset("\(Self.self)/jpmorgan") 37 | static var jpmorgan: Image 38 | 39 | @Asset("\(Self.self)/microsoft") 40 | static var microsoft: Image 41 | 42 | enum SharePriceState { 43 | 44 | @SystemImage("equal") 45 | static var neutral: Image 46 | 47 | @SystemImage("arrowtriangle.up.fill") 48 | static var up: Image 49 | 50 | @SystemImage("arrowtriangle.down.fill") 51 | static var down: Image 52 | } 53 | } 54 | 55 | enum System { 56 | @SystemImage("arrow.counterclockwise.circle.fill") 57 | static var reloadIcon: Image 58 | 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /99_Stocks/Resources/Localization.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | import Foundation 5 | 6 | // swiftlint:disable superfluous_disable_command 7 | // swiftlint:disable file_length 8 | 9 | // MARK: - Strings 10 | 11 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 12 | // swiftlint:disable nesting type_body_length type_name 13 | internal enum L10n { 14 | 15 | internal enum Sections { 16 | internal enum Companies { 17 | internal enum Button { 18 | /// Reload 19 | internal static let reload = L10n.tr("Localizable", "**Sections.Companies.Button.reload**") 20 | } 21 | internal enum Label { 22 | /// Empty 23 | internal static let emptyResults = L10n.tr("Localizable", "**Sections.Companies.Label.emptyResults**") 24 | /// 99_Stocks 25 | internal static let title = L10n.tr("Localizable", "**Sections.Companies.Label.title**") 26 | } 27 | internal enum TextField { 28 | /// Search 29 | internal static let search = L10n.tr("Localizable", "**Sections.Companies.TextField.search**") 30 | } 31 | } 32 | } 33 | } 34 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 35 | // swiftlint:enable nesting type_body_length type_name 36 | 37 | // MARK: - Implementation Details 38 | 39 | extension L10n { 40 | private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { 41 | // swiftlint:disable:next nslocalizedstring_key 42 | let format = NSLocalizedString(key, tableName: table, bundle: BundleToken.bundle, comment: "") 43 | return String(format: format, locale: Locale.current, arguments: args) 44 | } 45 | } 46 | 47 | // swiftlint:disable convenience_type 48 | private final class BundleToken { 49 | static let bundle: Bundle = { 50 | Bundle(for: BundleToken.self) 51 | }() 52 | } 53 | // swiftlint:enable convenience_type 54 | -------------------------------------------------------------------------------- /99_Stocks/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /99_Stocks/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "**Sections.Companies.Label.emptyResults**" = "Empty"; 2 | "**Sections.Companies.Label.title**" = "99_Stocks"; 3 | "**Sections.Companies.TextField.search**" = "Search"; 4 | "**Sections.Companies.Button.reload**" = "Reload"; -------------------------------------------------------------------------------- /99_Stocks/Resources/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "**Sections.Companies.Label.title**" = "99_Bolsa"; 2 | "**Sections.Companies.Button.reload**" = "Recargar"; 3 | "**Sections.Companies.TextField.search**" = "Buscar"; 4 | "**Sections.Companies.Label.emptyResults**" = "Vacío"; -------------------------------------------------------------------------------- /99_Stocks/Utils/Network/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | public protocol APIClient { 13 | associatedtype APIConfigType: APIConfiguration 14 | static func request(_ config: APIConfigType) -> AnyPublisher 15 | } 16 | 17 | public extension APIClient { 18 | static func request(_ config: APIConfigType) -> AnyPublisher { 19 | let urlRequestResult = config.asURLRequest() 20 | switch urlRequestResult { 21 | case .success(let urlRequest): 22 | return URLSession.shared.combine.send(request: urlRequest) 23 | .decode(type: RequestType.self, decoder: JSONDecoder()) 24 | .eraseToAnyPublisher() 25 | case .failure(let urlRequestError): 26 | return Fail(error: urlRequestError).eraseToAnyPublisher() 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /99_Stocks/Utils/Network/APIConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConfiguration.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | public protocol APIConfiguration: URLRequestConvertible { 13 | 14 | var baseURL: URLConvertible { get } 15 | var path: String { get } 16 | var method: HTTPMethod { get } 17 | var queryItems: [URLQueryItem] { get } 18 | var bodyParameters: [String: Any]? { get } 19 | 20 | func asURLRequest() -> Result 21 | } 22 | 23 | public extension APIConfiguration { 24 | 25 | func asURLRequest() -> Result { 26 | 27 | var url: URL! 28 | let urlResult = self.baseURL.asURL() 29 | 30 | switch urlResult { 31 | case .success(let validURL): 32 | url = validURL 33 | case .failure(let urlError): 34 | return .failure(.wrongURLConversion(urlError)) 35 | } 36 | 37 | 38 | let urlWithPath = url.appendingPathComponent(self.path) 39 | var urlComponents = URLComponents(url: urlWithPath, resolvingAgainstBaseURL: true) 40 | urlComponents?.queryItems = self.queryItems 41 | 42 | var urlRequest = URLRequest(url: urlComponents?.url?.absoluteURL ?? urlWithPath) 43 | urlRequest.httpMethod = self.method.rawValue 44 | urlRequest.setValue(ContentType.json.rawValue, 45 | forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue) 46 | urlRequest.setValue(ContentType.json.rawValue, 47 | forHTTPHeaderField: HTTPHeaderField.contentType.rawValue) 48 | 49 | if let bodyParameters = bodyParameters { 50 | do { 51 | urlRequest.httpBody = try JSONSerialization.data(withJSONObject: bodyParameters, options: []) 52 | } catch { 53 | return .failure(.badSerialization) 54 | } 55 | } 56 | 57 | return .success(urlRequest) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /99_Stocks/Utils/Network/NetworkHelpers.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Helpers.swift 4 | // 99_Stocks 5 | // 6 | // Created by Daniel Illescas Romero on 09/06/2019. 7 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | 12 | public enum HTTPMethod: String { 13 | case get 14 | case post 15 | case put 16 | case delete 17 | case patch 18 | } 19 | 20 | public enum HTTPHeaderField: String { 21 | case authentication = "Authorization" 22 | case contentType = "Content-Type" 23 | case acceptType = "Accept" 24 | case acceptEncoding = "Accept-Encoding" 25 | } 26 | 27 | public enum ContentType: String { 28 | case json = "application/json" 29 | // ... 30 | } 31 | 32 | 33 | // 34 | public enum URLError: Error { 35 | case badURL 36 | } 37 | public protocol URLConvertible { 38 | func asURL() -> Result 39 | } 40 | extension String: URLConvertible { 41 | public func asURL() -> Result { 42 | let url = URL(string: self) 43 | switch url { 44 | case .none: 45 | return .failure(.badURL) 46 | case .some(let validURL): 47 | return .success(validURL) 48 | } 49 | } 50 | } 51 | extension URL: URLConvertible { 52 | public func asURL() -> Result { 53 | return .success(self) 54 | } 55 | } 56 | 57 | // 58 | 59 | public enum URLRequestError: Error { 60 | case wrongURLConversion(URLError) 61 | case badSerialization 62 | // ... 63 | } 64 | public protocol URLRequestConvertible { 65 | func asURLRequest() -> Result 66 | } 67 | -------------------------------------------------------------------------------- /99_Stocks/Utils/PropertyWrappers/Asset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 08/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public protocol AssetProtocol: View { 12 | init(_ name: String, bundle: Bundle?) 13 | } 14 | extension Image: AssetProtocol {} 15 | extension Color: AssetProtocol {} 16 | 17 | @propertyWrapper 18 | struct Asset { 19 | 20 | let name: String 21 | let bundle: Bundle? 22 | 23 | init(_ name: String, bundle: Bundle? = nil) { 24 | self.name = "\(AssetType.self)s/\(name)" 25 | self.bundle = bundle 26 | } 27 | 28 | var wrappedValue: AssetType { 29 | return AssetType(self.name, bundle: self.bundle) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /99_Stocks/Utils/PropertyWrappers/SystemImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemImage.swift 3 | // 99_Companies 4 | // 5 | // Created by Daniel Illescas Romero on 09/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @propertyWrapper 12 | struct SystemImage { 13 | let name: String 14 | init(_ name: String) { 15 | self.name = name 16 | } 17 | var wrappedValue: Image { 18 | return Image(systemName: name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /99_StocksTests/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 | -------------------------------------------------------------------------------- /99_StocksTests/StocksTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _9_CompaniesTests.swift 3 | // 99_CompaniesTests 4 | // 5 | // Created by Daniel Illescas Romero on 07/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | @testable import _9_Stocks 12 | 13 | class StocksAPITests: XCTestCase { 14 | 15 | // MARK: - Preparation 16 | 17 | override func setUp() { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDown() { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | // MARK: - Tests 26 | 27 | func testRawFetchCompanies() { 28 | guard let url = try? Company.Network.Endpoint.companies.baseURL.asURL().get() else { 29 | XCTFail("Invalid URL") 30 | return 31 | } 32 | let expectation = XCTestExpectation(description: "RAW Fetch companies") 33 | let task = URLSession.shared.dataTask(with: url) { (data, response, error) in 34 | XCTAssertNotNil(data) 35 | XCTAssertNotNil(response) 36 | if let httpResponse = response as? HTTPURLResponse { 37 | XCTAssertTrue(200..<300 ~= httpResponse.statusCode, "Bad request") 38 | } 39 | XCTAssertNil(error, error?.localizedDescription ?? "error") 40 | expectation.fulfill() 41 | } 42 | task.resume() 43 | wait(for: [expectation], timeout: 20) 44 | } 45 | 46 | func testRealCompaniesAPI() { 47 | let expectation = XCTestExpectation(description: "Fetch companies") 48 | fetchCompanies(onNext: { companies in 49 | XCTAssert(!companies.isEmpty) 50 | expectation.fulfill() 51 | }, onError: { error in 52 | XCTFail(error.localizedDescription) 53 | }) 54 | wait(for: [expectation], timeout: 20) 55 | } 56 | 57 | func testRealCompanyItemDetailAPI() { 58 | let expectation = XCTestExpectation(description: "Fetch company") 59 | fetchCompany(id: 1, onNext: { company in 60 | expectation.fulfill() 61 | }, onError: { error in 62 | XCTFail(error.localizedDescription) 63 | }) 64 | wait(for: [expectation], timeout: 20) 65 | } 66 | 67 | // MARK: - Convenience 68 | 69 | private func fetchCompanies(onNext: @escaping ([Company.Model.ItemListResponse]) -> Void, onError: @escaping (Error) -> Void) { 70 | _ = Company.Network.ApiClient.companies().sink(receiveCompletion: { completion in 71 | switch completion { 72 | case .failure(let error): 73 | onError(error) 74 | case .finished: return 75 | } 76 | }, receiveValue: { value in 77 | onNext(value) 78 | }) 79 | } 80 | 81 | private func fetchCompany(id: Int, onNext: @escaping (Company.Model.DetailItemResponse) -> Void, onError: @escaping (Error) -> Void) { 82 | _ = Company.Network.ApiClient.company(id: id).sink(receiveCompletion: { completion in 83 | switch completion { 84 | case .failure(let error): 85 | onError(error) 86 | case .finished: return 87 | } 88 | }, receiveValue: { value in 89 | onNext(value) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /APIRequestsTestPlan.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "E46B5870-A36F-40EC-8CCB-516C9DFDBE6D", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "codeCoverage" : false 13 | }, 14 | "testTargets" : [ 15 | { 16 | "parallelizable" : true, 17 | "target" : { 18 | "containerPath" : "container:99_Stocks.xcodeproj", 19 | "identifier" : "65B48AB222AAC72A00BE9832", 20 | "name" : "99_StocksTests" 21 | } 22 | }, 23 | { 24 | "target" : { 25 | "containerPath" : "container:99_Stocks.xcodeproj", 26 | "identifier" : "658E93A822AD6A36008CCC32", 27 | "name" : "StocksUITests" 28 | } 29 | } 30 | ], 31 | "version" : 1 32 | } 33 | -------------------------------------------------------------------------------- /Generation/.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - ../99_Stocks 3 | templates: 4 | - sourcery/APIConfig 5 | output: 6 | ../99_Stocks/GeneratedCode 7 | -------------------------------------------------------------------------------- /Generation/Localization/CSV2Localizables: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/Generation/Localization/CSV2Localizables -------------------------------------------------------------------------------- /Generation/Localization/CSV2Localizables.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class CSV2Localizables { 4 | 5 | private typealias Key = String 6 | private typealias LanguageId = String 7 | private typealias Content = String 8 | 9 | private let csvURL: URL 10 | private let options: Options 11 | 12 | init(csvURL: URL, options: Option...) { 13 | self.csvURL = csvURL 14 | self.options = Options(from: options) 15 | } 16 | 17 | func writeFiles(to outputURL: URL) { 18 | 19 | let translationByKey = translations(from: self.csvURL) 20 | let translationsByLanguage = translationByLanguage(from: translationByKey) 21 | 22 | for (languageKey, keyContent) in translationsByLanguage { 23 | 24 | let languageFolderURL = outputURL.appendingPathComponent("\(languageKey).lproj") 25 | guard (try? FileManager.default.createDirectory(at: languageFolderURL, withIntermediateDirectories: true)) != nil else { continue } 26 | 27 | let localizableStrings: String = keyContent.map { #""**\#($0)**" = "\#($1)";"# }.joined(separator: "\n") 28 | let localizableStringURL = languageFolderURL.appendingPathComponent("Localizable.strings") 29 | try? FileManager.default.removeItem(at: localizableStringURL) 30 | guard FileManager.default.createFile(atPath: localizableStringURL.path, contents: Data(localizableStrings.utf8)) else { continue } 31 | } 32 | 33 | } 34 | 35 | private func translations(from url: URL) -> [Key: [LanguageId: Content]] { 36 | 37 | guard let data = FileManager.default.contents(atPath: url.path) else { return [:] } 38 | let csv = String(decoding: data, as: UTF8.self) 39 | var translationsOutput: [Key: [LanguageId: Content]] = [:] 40 | 41 | let lines = csv.components(separatedBy: CharacterSet.newlines).filter { !$0.isEmpty } 42 | let columnNames = lines.first?.split(separator: options.columnSeparator) ?? [] 43 | let languages = Array(columnNames.dropFirst()) 44 | let linesSkippingColumnNames = lines.dropFirst() 45 | 46 | for line in linesSkippingColumnNames { 47 | 48 | let lineParts = line.split(separator: options.columnSeparator) 49 | 50 | guard lineParts.count == columnNames.count else { 51 | if lineParts.count > 0 { 52 | assertionFailure("Bad parse or one language is missing its localization.\n - Full line: \(line)\n - Line parts\(lineParts)") 53 | } 54 | continue 55 | } 56 | 57 | var languagesDictionary: [LanguageId: Content] = [:] 58 | for (language, linePart) in zip(languages, lineParts.dropFirst()) { 59 | let languageId = String(language) 60 | languagesDictionary[languageId] = escape(text: String(linePart)) 61 | } 62 | 63 | let key = String(lineParts[0]) 64 | translationsOutput[key] = languagesDictionary 65 | } 66 | 67 | return translationsOutput 68 | } 69 | 70 | private func escape(text: String) -> String { 71 | let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) 72 | let trimmedQuotes = trimmed.replacingOccurrences(of: "\"", with: #"\""#) 73 | let trimmedSeparator = trimmedQuotes.replacingOccurrences(of: String(options.columnSeparator), with: "_") 74 | let explicitNewlineCharacter = trimmedSeparator.replacingOccurrences(of: #"\\n"#, with: #"\n"#) 75 | return explicitNewlineCharacter 76 | } 77 | 78 | private func translationByLanguage(from translationByKey: [Key: [LanguageId: Content]]) -> [LanguageId: [Key: Content]] { 79 | var translationByLanguage: [LanguageId: [Key: Content]] = [:] 80 | for (key, languageAndContent) in translationByKey { 81 | for (languageKey, content) in languageAndContent { 82 | if translationByLanguage[languageKey] == nil { 83 | translationByLanguage[languageKey] = [:] 84 | } 85 | translationByLanguage[languageKey]?[key] = content 86 | } 87 | } 88 | return translationByLanguage 89 | } 90 | } 91 | extension CSV2Localizables { 92 | enum Option { 93 | case columnSeparator(Character) 94 | case failIfMissingLocalization(Bool) 95 | } 96 | fileprivate struct Options { 97 | 98 | var columnSeparator: Character = "~" 99 | var failIfMissingLocalization: Bool = true 100 | 101 | init(from optionArray: [Option]) { 102 | for option in optionArray { 103 | switch option { 104 | case .columnSeparator(let character): 105 | self.columnSeparator = character 106 | case .failIfMissingLocalization(let flag): 107 | self.failIfMissingLocalization = flag 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // 115 | 116 | let projectName = "99_Stocks" 117 | let projectBaseURL = URL(fileURLWithPath: "/Users/daniel/Documents/Programming/IDE Projects/Xcode/Projects/- TestProjects/\(projectName)", isDirectory: true) 118 | 119 | // 120 | 121 | let currentDirectoryURL = projectBaseURL.appendingPathComponent("Generation/Localization", isDirectory: true) 122 | let csvURL = currentDirectoryURL.appendingPathComponent("Localizable.csv", isDirectory: false) 123 | 124 | 125 | let csv2Localizables = CSV2Localizables(csvURL: csvURL, options: .columnSeparator(";")) 126 | csv2Localizables.writeFiles(to: projectBaseURL.appendingPathComponent("\(projectName)/Resources")) -------------------------------------------------------------------------------- /Generation/Localization/Localizable.csv: -------------------------------------------------------------------------------- 1 | Key;en;es 2 | Sections.Companies.Label.title;99_Stocks;99_Bolsa 3 | Sections.Companies.Label.emptyResults;Empty;Vacío 4 | Sections.Companies.Button.reload;Reload;Recargar 5 | Sections.Companies.TextField.search;Search;Buscar -------------------------------------------------------------------------------- /Generation/Localization/Localizable.numbers: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/Generation/Localization/Localizable.numbers -------------------------------------------------------------------------------- /Generation/sourcery/APIConfig/APIConfigurationSourcery.swifttemplate: -------------------------------------------------------------------------------- 1 | <%- include("Utils.swifttemplate") %> 2 | 3 | import Foundation 4 | import Combine 5 | 6 | <%_ for aType in types.implementing["APIConfiguration"] where aType is Enum { -%> 7 | <%_ let enumType = aType as! Enum -%> 8 | // MARK: - <%= aType.name %> 9 | 10 | <%= aType.accessLevel %> extension <%= aType.name %> { 11 | 12 | var baseURL: URLConvertible { 13 | <%_ if let url = aType.annotations["url"] as? String { -%> 14 | return "<%= url %>" 15 | <%_ } -%> 16 | <%_ else { -%> 17 | #error("You must provide a URL") 18 | <%_ } -%> 19 | } 20 | 21 | var method: HTTPMethod { 22 | switch self { 23 | <%_ for aCase in enumType.cases { -%> 24 | case .<%= aCase.name %>: 25 | return .<%= (aCase.annotations["method"] as? String)?.lowercased() ?? "get" %> 26 | <%_ } -%> 27 | } 28 | } 29 | 30 | var path: String { 31 | switch self { 32 | <%_ for aCase in enumType.cases { -%> 33 | <%_ if let path = aCase.annotations["path"] as? String { -%> 34 | case .<%= aCase.name %><%= caseParameter(aCase) %>: 35 | return "<%= path %>" 36 | <%_ } else { -%> 37 | case .<%= aCase.name %>: 38 | return "/" 39 | <%_ } -%> 40 | <%_ } -%> 41 | } 42 | } 43 | 44 | var queryItems: [URLQueryItem] { 45 | switch self { 46 | <%_ for aCase in enumType.cases { -%> 47 | <%_ if (aCase.annotations["method"] as? String) == "GET" { -%> 48 | case .<%= aCase.name %><%= caseParameter(aCase) %>: 49 | <%_ if let queryParams = aCase.annotations["queryParams"] { -%> 50 | let rawQueryItems = <%= queryParams %> 51 | <%_ } else { -%> 52 | let rawQueryItems: [String: String] = [:] 53 | <%_ } -%> 54 | var mappedItems: [String: String] = [] 55 | <%_ for parameterName in aCase.associatedValues.compactMap({ $0.localName as? String }) ?? [] { -%> 56 | mappedItems[rawQueryItems["<%= parameterName %>", default: "<%= parameterName %>"]] = String(describing: <%= parameterName %>) 57 | <%_ } -%> 58 | return mappedItems.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) } 59 | <%_ } else { -%> 60 | case .<%= aCase.name %>: 61 | return [] 62 | <%_ } -%> 63 | <%_ } -%> 64 | } 65 | } 66 | 67 | var bodyParameters: [String : Any]? { 68 | switch self { 69 | <%_ for aCase in enumType.cases { -%> 70 | <%_ if (aCase.annotations["method"] as? String) == "POST" { -%> 71 | case .<%= aCase.name %><%= caseParameter(aCase) %>: 72 | <%_ if let bodyParams = aCase.annotations["bodyParams"] { -%> 73 | let rawBodyParams = <%= bodyParams %> 74 | <%_ } else { -%> 75 | let rawBodyParams: [String: String] = [:] 76 | <%_ } -%> 77 | var mappedParams: [String: Any] = [] 78 | <%_ for parameterName in aCase.associatedValues.compactMap({ $0.localName as? String }) ?? [] { -%> 79 | mappedParams[rawBodyParams["<%= parameterName %>", default: "<%= parameterName %>"]] = <%= parameterName %> 80 | <%_ } -%> 81 | return mappedParams 82 | <%_ } else { -%> 83 | case .<%= aCase.name %>: return nil 84 | <%_ } -%> 85 | <%_ } -%> 86 | } 87 | } 88 | } 89 | 90 | // MARK: [API Client] protocol and extension 91 | 92 | // By creating this protocol, we could create a mock APIClient (or just another api client in general) that implements 93 | // these methods 94 | protocol <%= (aType.parentName ?? "").replacingOccurrences(of: ".", with: "") %>ApiClientProtocol { 95 | <%_ for aCase in enumType.cases where aCase.annotations["response"] != nil { -%> 96 | <%_ guard let response = aCase.annotations["response"] as? String else { continue } -%> 97 | static func <%= aCase.name %>(<%= caseFunctionParameters(aCase) %>) -> AnyPublisher<<%= response %>, Error> 98 | <%_ } -%> 99 | } 100 | 101 | extension <%= aType.parentName ?? "" %>.ApiClient: <%= (aType.parentName ?? "").replacingOccurrences(of: ".", with: "") %>ApiClientProtocol { 102 | <%_ for aCase in enumType.cases where aCase.annotations["response"] != nil { -%> 103 | <%_ guard let response = aCase.annotations["response"] as? String else { continue } -%> 104 | static func <%= aCase.name %>(<%= caseFunctionParameters(aCase) %>) -> AnyPublisher<<%= response %>, Error> { 105 | return self.request(.<%= aCase.name %><%= caseMatchingFunctionParameters(aCase) %>) 106 | } 107 | <%_ } -%> 108 | } 109 | <%_ } -%> -------------------------------------------------------------------------------- /Generation/sourcery/APIConfig/Utils.swifttemplate: -------------------------------------------------------------------------------- 1 | <% 2 | extension StringProtocol { 3 | var firstUppercased: String { 4 | return prefix(1).uppercased() + dropFirst() 5 | } 6 | var firstCapitalized: String { 7 | return prefix(1).capitalized + dropFirst() 8 | } 9 | } 10 | extension String { 11 | var firstParentName: String { 12 | guard let name = self.split(separator: ".").first else { 13 | return self 14 | } 15 | return String(name) 16 | } 17 | } 18 | func commaSeparatedContent(from values: [T?], parenthesis: Bool, contentHandler: (T) -> String) -> String { 19 | var output = "" 20 | for (index, value_) in values.enumerated() { 21 | guard let value = value_ else { continue } 22 | if index == 0 && parenthesis { 23 | output += "(" 24 | } 25 | output += contentHandler(value) 26 | if index < (values.count - 1) { 27 | output += ", " 28 | } else if parenthesis { 29 | output += ")" 30 | } 31 | } 32 | return output 33 | } 34 | func caseParameter(_ enumCase: EnumCase) -> String { 35 | return commaSeparatedContent(from: enumCase.associatedValues.map { $0.localName }, parenthesis: true) { localName in 36 | "let \(localName)" 37 | } 38 | } 39 | func caseFunctionParameters(_ enumCase: EnumCase) -> String { 40 | return commaSeparatedContent(from: enumCase.associatedValues, parenthesis: false) { associatedValue in 41 | "\(associatedValue.localName ?? ""): \(associatedValue.typeName)" 42 | } 43 | } 44 | func caseMatchingFunctionParameters(_ enumCase: EnumCase) -> String { 45 | return commaSeparatedContent(from: enumCase.associatedValues.map { $0.localName }, parenthesis: true) { localName in 46 | "\(localName): \(localName)" 47 | } 48 | } 49 | -%> -------------------------------------------------------------------------------- /Generation/sourcery/swiftgen/structured-swift5.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | {% if tables.count > 0 %} 5 | {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} 6 | import Foundation 7 | 8 | // swiftlint:disable superfluous_disable_command 9 | // swiftlint:disable file_length 10 | 11 | // MARK: - Strings 12 | 13 | {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} 14 | {% for type in types %} 15 | {% if type == "String" %} 16 | _ p{{forloop.counter}}: Any 17 | {% else %} 18 | _ p{{forloop.counter}}: {{type}} 19 | {% endif %} 20 | {{ ", " if not forloop.last }} 21 | {% endfor %} 22 | {% endfilter %}{% endmacro %} 23 | {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} 24 | {% for type in types %} 25 | {% if type == "String" %} 26 | String(describing: p{{forloop.counter}}) 27 | {% elif type == "UnsafeRawPointer" %} 28 | Int(bitPattern: p{{forloop.counter}}) 29 | {% else %} 30 | p{{forloop.counter}} 31 | {% endif %} 32 | {{ ", " if not forloop.last }} 33 | {% endfor %} 34 | {% endfilter %}{% endmacro %} 35 | {% macro recursiveBlock table item %} 36 | {% for string in item.strings %} 37 | {% if not param.noComments %} 38 | /// {{string.translation}} 39 | {% endif %} 40 | {% if string.types %} 41 | {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { 42 | return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %}) 43 | } 44 | {% elif param.localizeFunction %} 45 | {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #} 46 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") } 47 | {% else %} 48 | {{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}") 49 | {% endif %} 50 | {% endfor %} 51 | {% for child in item.children %} 52 | 53 | {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 54 | {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %} 55 | } 56 | {% endfor %} 57 | {% endmacro %} 58 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 59 | // swiftlint:disable nesting type_body_length type_name 60 | {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} 61 | {{accessModifier}} enum {{enumName}} { 62 | {% if tables.count > 1 %} 63 | {% for table in tables %} 64 | {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 65 | {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %} 66 | } 67 | {% endfor %} 68 | {% else %} 69 | {% call recursiveBlock tables.first.name tables.first.levels %} 70 | {% endif %} 71 | } 72 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 73 | // swiftlint:enable nesting type_body_length type_name 74 | 75 | // MARK: - Implementation Details 76 | 77 | extension {{enumName}} { 78 | private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { 79 | // swiftlint:disable:next nslocalizedstring_key 80 | let format = {{param.localizeFunction|default:"NSLocalizedString"}}(key, tableName: table, bundle: BundleToken.bundle, comment: "") 81 | return String(format: format, locale: Locale.current, arguments: args) 82 | } 83 | } 84 | 85 | // swiftlint:disable convenience_type 86 | private final class BundleToken { 87 | static let bundle: Bundle = { 88 | Bundle(for: BundleToken.self) 89 | }() 90 | } 91 | // swiftlint:enable convenience_type 92 | {% else %} 93 | // No string found 94 | {% endif %} 95 | -------------------------------------------------------------------------------- /Generation/swiftgen.yml: -------------------------------------------------------------------------------- 1 | strings: 2 | inputs: "../99_Stocks/Resources/en.lproj/Localizable.strings" 3 | outputs: 4 | templatePath: "./sourcery/swiftgen/structured-swift5.stencil" 5 | output: "../99_Stocks/Resources/Localization.swift" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Daniel Illescas Romero 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 99Stocks-SwiftUI 2 | [![Swift version](https://img.shields.io/badge/Swift-5-orange.svg)](https://swift.org/download) 3 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/illescasDaniel/99StocksSwiftUI/blob/master/LICENSE) 4 | 5 | Little project that fetches a list of companies, sort them by their share price and can show its details on a separate view. 6 | 7 | **Note:** This project fetches data from an endpoint that seems to be **no longer available**, so this project just serves a my first little adventure with SwiftUI :) 8 | 9 | **Technical Features** 10 | ---- 11 | - [**SwiftUI**](https://developer.apple.com/xcode/swiftui/) | Latest Apple multiplatform UI framework 12 | - [**Combine**](https://developer.apple.com/documentation/combine) | Declarative API for processing values over time (similar to RxSwift) 13 | - [**Sourcery**](https://github.com/krzysztofzablocki/Sourcery) | A utility for code generation from custom templates 14 | - [**Swiftgen**](https://www.github.com/SwiftGen/SwiftGen) | A utility for code generation from app assets (in this case, Localizable.strings) 15 | - [**CSV2Localizables**](https://github.com/illescasDaniel/99StocksSwiftUI/blob/master/Generation/Localization/CSV2Localizables.swift) | Small personal tool to generate from a CSV a Localizable.strings file for each language 16 | - [**@propertyWrapper**](https://medium.com/better-programming/swift-property-delegates-powerful-new-annotations-attributes-system-2e3968b29624) | Annotate properties and do magical things ;). Check [my article](https://medium.com/better-programming/swift-property-delegates-powerful-new-annotations-attributes-system-2e3968b29624) about it. 17 | 18 | **Screenshots** 19 | ------- 20 | **Demo Video** [HERE](https://www.linkedin.com/feed/update/urn:li:activity:6543596406219816960) 21 | 22 | 23 | 24 | # Resources # 25 | 26 | - **API**: thanks to [NinetyNine](https://github.com/99markets/challenges/blob/master/mobile.md) 27 | - **Images**: mostly from [seeklogo](https://seeklogo.net) 28 | - **Chart**: [gpbl/SwiftChart](https://www.github.com/gpbl/SwiftChart) 29 | - Some Combine extensions from [GitHubSearchWithSwiftUI](https://github.com/marty-suzuki/GitHubSearchWithSwiftUI) 30 | -------------------------------------------------------------------------------- /StocksUITests/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 | -------------------------------------------------------------------------------- /StocksUITests/StocksUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StocksUITests.swift 3 | // StocksUITests 4 | // 5 | // Created by Daniel Illescas Romero on 09/06/2019. 6 | // Copyright © 2019 Daniel Illescas Romero. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class StocksUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testSearchAndTapOnRow() { 30 | // for some reason, for now, I can't seem to record events with Xcode 11 and SwiftUI :( 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /github/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/4c8ae1d5d9cb8a080e59f40008be3371bed3f688/github/screenshots.png --------------------------------------------------------------------------------