├── github
└── screenshots.png
├── 99_Stocks
├── Resources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── icon.png
│ │ │ ├── Icon-76.png
│ │ │ ├── watchicon.png
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-76@2x.png
│ │ │ ├── Icon-83.5@2x.png
│ │ │ ├── Icon-Small.png
│ │ │ ├── icon_128x128.png
│ │ │ ├── icon_16x16.png
│ │ │ ├── icon_256x256.png
│ │ │ ├── icon_32x32.png
│ │ │ ├── icon_512x512.png
│ │ │ ├── Icon-Small-40.png
│ │ │ ├── Icon-Small@2x.png
│ │ │ ├── Icon-Small@3x.png
│ │ │ ├── icon_16x16@2x.png
│ │ │ ├── icon_32x32@2x.png
│ │ │ ├── AppIcon108x108@2x.png
│ │ │ ├── AppIcon24x24@2x.png
│ │ │ ├── AppIcon29x29@2x.png
│ │ │ ├── AppIcon29x29@3x.png
│ │ │ ├── AppIcon40x40@2x.png
│ │ │ ├── AppIcon44x44@2x.png
│ │ │ ├── AppIcon50x50@2x.png
│ │ │ ├── AppIcon86x86@2x.png
│ │ │ ├── AppIcon98x98@2x.png
│ │ │ ├── Icon-Notification.png
│ │ │ ├── Icon-Small-40@2x.png
│ │ │ ├── Icon-Small-40@3x.png
│ │ │ ├── icon_128x128@2x.png
│ │ │ ├── icon_256x256@2x.png
│ │ │ ├── icon_512x512@2x.png
│ │ │ ├── AppIcon27.5x27.5@2x.png
│ │ │ ├── Icon-Notification@2x.png
│ │ │ ├── Icon-Notification@3x.png
│ │ │ └── Contents.json
│ │ ├── Colors
│ │ │ ├── Contents.json
│ │ │ └── Companies
│ │ │ │ ├── Contents.json
│ │ │ │ └── Currency
│ │ │ │ ├── Contents.json
│ │ │ │ ├── down.colorset
│ │ │ │ └── Contents.json
│ │ │ │ ├── up.colorset
│ │ │ │ └── Contents.json
│ │ │ │ └── neutral.colorset
│ │ │ │ └── Contents.json
│ │ └── Images
│ │ │ ├── Contents.json
│ │ │ ├── Companies
│ │ │ ├── Contents.json
│ │ │ ├── amazon.imageset
│ │ │ │ ├── amzn.png
│ │ │ │ └── Contents.json
│ │ │ ├── apple.imageset
│ │ │ │ ├── apple.png
│ │ │ │ └── Contents.json
│ │ │ ├── facebook.imageset
│ │ │ │ ├── fb.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── alibaba.imageset
│ │ │ │ ├── alibaba.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── johnson.imageset
│ │ │ │ ├── johnson.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── alphabet.imageset
│ │ │ │ ├── alphabet.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── jpmorgan.imageset
│ │ │ │ ├── jpmorgan.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── berkshire.imageset
│ │ │ │ ├── berkshire.jpg
│ │ │ │ └── Contents.json
│ │ │ └── microsoft.imageset
│ │ │ │ ├── microsoft.png
│ │ │ │ └── Contents.json
│ │ │ └── StocksIcon.imageset
│ │ │ ├── StocksIcon.png
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── en.lproj
│ │ └── Localizable.strings
│ ├── es.lproj
│ │ └── Localizable.strings
│ ├── Colors.swift
│ ├── Images.swift
│ └── Localization.swift
├── 99_Stocks.entitlements
├── Modules
│ ├── Companies
│ │ ├── Company.swift
│ │ ├── Models
│ │ │ ├── DetailItem
│ │ │ │ ├── Company.Model.DetailItem.swift
│ │ │ │ └── Company.Model.DetailItemResponse.swift
│ │ │ ├── ItemList
│ │ │ │ ├── Company.Model.ItemList.swift
│ │ │ │ └── Company.Model.ItemListResponse.swift
│ │ │ ├── Country.Model.Country.swift
│ │ │ ├── Company.Model.StockState.swift
│ │ │ └── Company.Model.StockName.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
│ │ │ ├── SupportingViews
│ │ │ │ └── CompaniesListItemView.swift
│ │ │ ├── Company.View.ItemList.swift
│ │ │ └── Company.ViewModel.ItemList.swift
│ │ │ └── SupportingViews
│ │ │ └── ChartView.swift
│ ├── -App
│ │ ├── SceneRouter.swift
│ │ ├── AppDelegate.swift
│ │ ├── SceneDelegate.swift
│ │ └── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ └── General
│ │ └── SupportingViews
│ │ └── ActivityIndicatorView.swift
├── Utils
│ ├── PropertyWrappers
│ │ ├── SystemImage.swift
│ │ └── Asset.swift
│ └── Network
│ │ ├── APIClient.swift
│ │ ├── NetworkHelpers.swift
│ │ └── APIConfiguration.swift
├── Extensions
│ ├── Double+Currency.swift
│ └── URLSession+Combine.swift
├── GeneratedCode
│ └── APIConfigurationSourcery.generated.swift
├── 3rdPartyLibs
│ └── SwiftChart
│ │ ├── ChartSeries.swift
│ │ ├── ChartColors.swift
│ │ └── Chart.swift
└── Info.plist
├── Generation
├── .sourcery.yml
├── Localization
│ ├── CSV2Localizables
│ ├── Localizable.numbers
│ ├── Localizable.csv
│ └── CSV2Localizables.swift
├── swiftgen.yml
└── sourcery
│ ├── APIConfig
│ ├── Utils.swifttemplate
│ └── APIConfigurationSourcery.swifttemplate
│ └── swiftgen
│ └── structured-swift5.stencil
├── 99_Stocks.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── APIRequestsTestPlan.xctestplan
├── 99_StocksTests
├── Info.plist
└── StocksTests.swift
├── StocksUITests
├── Info.plist
└── StocksUITests.swift
├── LICENSE
├── README.md
└── .gitignore
/github/screenshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/github/screenshots.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/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/HEAD/Generation/Localization/CSV2Localizables
--------------------------------------------------------------------------------
/Generation/Localization/Localizable.numbers:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/Generation/Localization/Localizable.numbers
--------------------------------------------------------------------------------
/99_Stocks/Resources/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/watchicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/watchicon.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/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/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/AppIcon.appiconset/Icon-Small-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon108x108@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon24x24@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon98x98@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.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/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/AppIcon.appiconset/AppIcon27.5x27.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon27.5x27.5@2x.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/amazon.imageset/amzn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/amazon.imageset/amzn.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/apple.imageset/apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/apple.imageset/apple.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/facebook.imageset/fb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/facebook.imageset/fb.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/StocksIcon.imageset/StocksIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/StocksIcon.imageset/StocksIcon.png
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/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/HEAD/99_Stocks/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png
--------------------------------------------------------------------------------
/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/Images/Companies/alibaba.imageset/alibaba.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/alibaba.imageset/alibaba.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/johnson.imageset/johnson.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/johnson.imageset/johnson.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/alphabet.imageset/alphabet.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/alphabet.imageset/alphabet.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/jpmorgan.imageset/jpmorgan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/jpmorgan.imageset/jpmorgan.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/berkshire.imageset/berkshire.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/berkshire.imageset/berkshire.jpg
--------------------------------------------------------------------------------
/99_Stocks/Resources/Assets.xcassets/Images/Companies/microsoft.imageset/microsoft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/illescasDaniel/99StocksSwiftUI/HEAD/99_Stocks/Resources/Assets.xcassets/Images/Companies/microsoft.imageset/microsoft.png
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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.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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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_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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 | -%>
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 99Stocks-SwiftUI
2 | [](https://swift.org/download)
3 | [](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 |
--------------------------------------------------------------------------------
/.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/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/-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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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"))
--------------------------------------------------------------------------------
/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/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/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.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 |
--------------------------------------------------------------------------------