From f0695102410930ec05099678fb66205cd4fee950 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Tue, 25 Jul 2023 21:37:07 +0300 Subject: [PATCH 01/40] SASU-0077 Enabled User Script sandboxing in order to run scripts like swiftgen&swiftlint on Xcode 15 and newer --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index d2b7ad3..eea19e7 100644 --- a/project.yml +++ b/project.yml @@ -15,6 +15,7 @@ attributes: settings: enableBaseInternationalization: true + ENABLE_USER_SCRIPT_SANDBOXING: false packages: CocoaLumberjack: From aa9a37ba540f12ac6907840975ba4760e83aab95 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 13:37:29 +0300 Subject: [PATCH 02/40] SASU-0077 Project set to iOS 17 --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index eea19e7..4086009 100644 --- a/project.yml +++ b/project.yml @@ -8,7 +8,7 @@ configs: options: bundleIdPrefix: com.adesso deploymentTarget: - iOS: 16.0 + iOS: 17.0 attributes: BuildIndependentTargetsInParallel: true From 6bf974757ae7767901856ca0da948e61f93c3183 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 13:38:11 +0300 Subject: [PATCH 03/40] SASU-0077 iOS 17 Related warnings applied --- .../Application/SampleAppSwiftUIApp.swift | 4 +++- .../Scenes/Favorites/FavoritesView.swift | 8 ++++++-- .../CoinPriceHistoryChartViewModel.swift | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift index a3f9c7c..88a7943 100644 --- a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift +++ b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift @@ -24,7 +24,9 @@ struct SampleAppSwiftUIApp: App { WindowGroup { MainView() .environmentObject(router) - .onChange(of: phase, perform: manageChanges(for:)) + .onChange(of: phase, { _, newValue in + manageChanges(for: newValue) + }) .onOpenURL(perform: onOpenURL(_:)) } } diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index a55e0a1..d049492 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -31,8 +31,12 @@ struct FavoritesView: View { .background(Color.lightGray) .onAppear(perform: viewModel.fetchFavorites) .onDisappear(perform: viewModel.disconnect) - .onChange(of: searchTerm, perform: viewModel.filterResults(searchTerm:)) - .onChange(of: StorageManager.shared.favoriteCoins, perform: fetchFavorites) + .onChange(of: searchTerm, { _, newValue in + viewModel.filterResults(searchTerm: newValue) + }) + .onChange(of: StorageManager.shared.favoriteCoins, { _, newValue in + fetchFavorites(codes: newValue) + }) } private func fetchFavorites(codes: [CoinData]) { diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift index eed4c00..02bfcd6 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift @@ -68,14 +68,16 @@ class CoinPriceHistoryChartViewModel: ObservableObject { } func onChangeDrag(value: DragGesture.Value, chartProxy: ChartProxy, geometryProxy: GeometryProxy) { - let xCurrent = value.location.x - geometryProxy[chartProxy.plotAreaFrame].origin.x - - if let selectedDate: Date = chartProxy.value(atX: xCurrent), - let startDate = dataModel.prices.first?.date, - let lastDate = dataModel.prices.last?.date, - selectedDate >= startDate && selectedDate <= lastDate { - selectedX = selectedDate - selectedXDateText = calculatedSelectedXDateText + if let plotFrame = chartProxy.plotFrame { + let xCurrent = value.location.x - geometryProxy[plotFrame].origin.x + + if let selectedDate: Date = chartProxy.value(atX: xCurrent), + let startDate = dataModel.prices.first?.date, + let lastDate = dataModel.prices.last?.date, + selectedDate >= startDate && selectedDate <= lastDate { + selectedX = selectedDate + selectedXDateText = calculatedSelectedXDateText + } } } From d9fe058dd13ecef55a00b30831d5b1c936205982 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 13:38:31 +0300 Subject: [PATCH 04/40] SASU-0077 New Preview API applied --- .../Scenes/Favorites/FavoritesView.swift | 6 +-- .../Scenes/Home/Coin/CoinListView.swift | 8 ++-- .../Scenes/Home/Coin/CoinView.swift | 20 ++++------ .../Coin/Detail/ChangePercentageView.swift | 6 +-- .../Detail/CoinChartHistoryRangeButtons.swift | 6 +-- .../Home/Coin/Detail/CoinDetailView.swift | 11 ++---- .../Detail/CoinPriceHistoryChartView.swift | 39 ++++++++----------- .../Scenes/Home/HomeFilterView.swift | 6 +-- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 12 ++++-- .../Scenes/Home/SearchBarView.swift | 11 ++---- SampleAppSwiftUI/Scenes/Main/MainView.swift | 6 +-- .../Scenes/Settings/SettingsView.swift | 6 +-- 12 files changed, 56 insertions(+), 81 deletions(-) diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index d049492..76fc775 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -51,8 +51,6 @@ struct FavoritesView: View { } } -struct FavoritesView_Previews: PreviewProvider { - static var previews: some View { - FavoritesView() - } +#Preview { + FavoritesView() } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift index 81a27a9..c11f813 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift @@ -74,10 +74,8 @@ struct CoinListView: View { } } -struct CoinListView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - CoinListView(viewModel: HomeViewModel(), filteredCoins: .constant([.demo, .demo, .demo]), favoriteChanged: {}) - } +#Preview { + NavigationView { + CoinListView(filteredCoins: .constant([.demo, .demo, .demo]), favoriteChanged: {}) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift index 1c37da7..6895a2f 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift @@ -77,17 +77,13 @@ struct CoinView: View { } } -struct CoinView_Previews: PreviewProvider { - static var previews: some View { - Group { - CoinView(coinInfo: .demo) - CoinView(coinInfo: .demo) - .preferredColorScheme(.dark) - } - .previewLayout(.sizeThatFits) - .frame(height: Dimensions.coinCellSize) - .padding(.horizontal, Paddings.side) - .padding(.vertical) - +#Preview { + Group { + CoinView(coinInfo: .demo) + CoinView(coinInfo: .demo) + .preferredColorScheme(.dark) } + .frame(height: Dimensions.coinCellSize) + .padding(.horizontal, Paddings.side) + .padding(.vertical) } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/ChangePercentageView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/ChangePercentageView.swift index 9aa378e..731c61c 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/ChangePercentageView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/ChangePercentageView.swift @@ -27,8 +27,6 @@ struct ChangePercentageView: View { } } -struct ChangePercentageView_Previews: PreviewProvider { - static var previews: some View { - ChangePercentageView(changeRate: CoinData.demo.detail?.usd ?? .init()) - } +#Preview { + ChangePercentageView(changeRate: CoinData.demo.detail?.usd ?? .init()) } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinChartHistoryRangeButtons.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinChartHistoryRangeButtons.swift index e13fa60..20aefb6 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinChartHistoryRangeButtons.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinChartHistoryRangeButtons.swift @@ -34,8 +34,6 @@ struct CoinChartHistoryRangeButtons: View { } } -struct CoinChartHistoryRangeButtons_Previews: PreviewProvider { - static var previews: some View { - CoinChartHistoryRangeButtons(selection: .constant(.oneMonth)) - } +#Preview { + CoinChartHistoryRangeButtons(selection: .constant(.oneMonth)) } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index 9e78d8a..e3921b8 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -97,14 +97,11 @@ struct CoinDetailView: View { } } -struct CoinDetailView_Previews: PreviewProvider { - static var previews: some View { - Group { +#Preview { + Group { + NavigationView { CoinDetailView(coinData: CoinData.demo) - - NavigationView { - CoinDetailView(coinData: CoinData.demo) - } + .previewLayout(.sizeThatFits) } } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift index e4f97df..379b69e 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift @@ -138,31 +138,26 @@ struct CoinPriceHistoryChartView: View { } } -struct CoinPriceHistoryChartView_Previews: PreviewProvider { - static var previews: some View { +#Preview { + Group { Group { - Group { - ForEach(CoinChartHistoryRange.allCases) { item in - CoinPriceHistoryChartView( - selectedRange: item, - dataModel: .demo, - selectedXDateText: .constant("") - ) - .previewDisplayName(item.rawValue) - } + ForEach(CoinChartHistoryRange.allCases) { item in + CoinPriceHistoryChartView( + selectedRange: item, + dataModel: .demo, + selectedXDateText: .constant("") + ) } - - Group { - ForEach(CoinChartHistoryRange.allCases) { item in - CoinPriceHistoryChartView( - selectedRange: item, - dataModel: .demo, - selectedXDateText: .constant("") - ) - .previewDisplayName(item.rawValue + "DARK") - } + } + Group { + ForEach(CoinChartHistoryRange.allCases) { item in + CoinPriceHistoryChartView( + selectedRange: item, + dataModel: .demo, + selectedXDateText: .constant("") + ) } - .preferredColorScheme(.dark) } + .preferredColorScheme(.dark) } } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeFilterView.swift b/SampleAppSwiftUI/Scenes/Home/HomeFilterView.swift index 73181c0..17d21fa 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeFilterView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeFilterView.swift @@ -18,8 +18,6 @@ struct HomeFilterView: View { } } -struct HomeFilterView_Previews: PreviewProvider { - static var previews: some View { - HomeFilterView(filterTitle: "") - } +#Preview { + HomeFilterView(filterTitle: "") } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index 22226a9..e4c9545 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -43,8 +43,12 @@ struct HomeView: View { } } -struct HomeView_Previews: PreviewProvider { - static var previews: some View { - HomeView() - } +#Preview { + HomeView() } + +// struct HomeView_Previews: PreviewProvider { +// static var previews: some View { +// HomeView() +// } +// } diff --git a/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift b/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift index 8592d5e..ff40299 100644 --- a/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift +++ b/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift @@ -32,11 +32,8 @@ struct SearchBarView: View { } } -struct SearchBarView_Previews: PreviewProvider { - static var previews: some View { - SearchBarView(searchText: .constant(""), topPadding: Paddings.SearchBar.shortTop) - .previewLayout(.sizeThatFits) - .frame(width: .infinity, height: Dimensions.searchBarHeight) - .padding(.vertical) - } +#Preview { + SearchBarView(searchText: .constant(""), topPadding: Paddings.SearchBar.shortTop) + .frame(width: .infinity, height: Dimensions.searchBarHeight) + .padding(.vertical) } diff --git a/SampleAppSwiftUI/Scenes/Main/MainView.swift b/SampleAppSwiftUI/Scenes/Main/MainView.swift index b90139f..9cdfd69 100644 --- a/SampleAppSwiftUI/Scenes/Main/MainView.swift +++ b/SampleAppSwiftUI/Scenes/Main/MainView.swift @@ -38,8 +38,6 @@ struct MainView: View { } } -struct MainView_Previews: PreviewProvider { - static var previews: some View { - MainView() - } +#Preview { + MainView() } diff --git a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift index 941a7ff..306cef8 100644 --- a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift +++ b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift @@ -82,8 +82,6 @@ extension SettingsView { } } -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - } +#Preview { + SettingsView() } From 734214095ba962c87850cffb07f173e507d0a573 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 13:44:41 +0300 Subject: [PATCH 05/40] SASU-0077 Some fixes applied --- SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift | 2 +- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift index c11f813..2722a0a 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift @@ -76,6 +76,6 @@ struct CoinListView: View { #Preview { NavigationView { - CoinListView(filteredCoins: .constant([.demo, .demo, .demo]), favoriteChanged: {}) + CoinListView(viewModel: FavoritesViewModel(), filteredCoins: .constant([.demo, .demo, .demo]), favoriteChanged: {}) } } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index e4c9545..3bf5ba8 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -39,16 +39,12 @@ struct HomeView: View { await viewModel.fillModels() } } - .onChange(of: searchTerm, perform: viewModel.filterResults(searchTerm:)) + .onChange(of: searchTerm, { _, newValue in + viewModel.filterResults(searchTerm: newValue) + }) } } #Preview { HomeView() } - -// struct HomeView_Previews: PreviewProvider { -// static var previews: some View { -// HomeView() -// } -// } From 15e96fd0b46714d1154e1875ddbbcbebacc9c74c Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 21:48:25 +0300 Subject: [PATCH 06/40] Couple changes made * Strings moved into String Catalog * Swiftgen Removed & turned on auto-resource creator --- .../RequestModels/FavoritesCoinRequest.swift | 2 +- .../Contents.json | 0 .../Generated/Assets+Generated.swift | 2 +- .../Generated/Strings+Generated.swift | 48 -------- .../Resources/Localizable.xcstrings | 104 ++++++++++++++++++ .../Resources/de.lproj/Localizable.strings | 11 -- .../Resources/en.lproj/Localizable.strings | 11 -- .../Resources/tr.lproj/Localizable.strings | 11 -- .../Scenes/Favorites/FavoritesView.swift | 4 +- .../Scenes/Home/Coin/CoinView.swift | 2 +- .../Home/Coin/Detail/CoinDetailView.swift | 3 +- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 2 +- .../Scenes/Home/SearchBarView.swift | 4 +- .../Scenes/Settings/SettingsView.swift | 15 +-- .../Utility/Extensions/ColorExtensions.swift | 10 -- .../SettingButtonModifier.swift | 2 +- .../SettingLineModifier.swift | 2 +- .../SettingTextModifier.swift | 6 +- project.yml | 7 +- scripts/installation/swiftgen.sh | 6 - swiftgen.yml | 9 -- 21 files changed, 130 insertions(+), 131 deletions(-) rename SampleAppSwiftUI/Resources/Colors.xcassets/{LightGray.colorset => LightestGray.colorset}/Contents.json (100%) delete mode 100644 SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift create mode 100644 SampleAppSwiftUI/Resources/Localizable.xcstrings delete mode 100644 SampleAppSwiftUI/Resources/de.lproj/Localizable.strings delete mode 100644 SampleAppSwiftUI/Resources/en.lproj/Localizable.strings delete mode 100644 SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings delete mode 100644 scripts/installation/swiftgen.sh diff --git a/SampleAppSwiftUI/Network/WebSocket/RequestModels/FavoritesCoinRequest.swift b/SampleAppSwiftUI/Network/WebSocket/RequestModels/FavoritesCoinRequest.swift index 4d7604b..36dda4a 100644 --- a/SampleAppSwiftUI/Network/WebSocket/RequestModels/FavoritesCoinRequest.swift +++ b/SampleAppSwiftUI/Network/WebSocket/RequestModels/FavoritesCoinRequest.swift @@ -16,6 +16,6 @@ extension FavoritesCoinRequest { init(action: SubscriptionRequestAction, codeList: [CoinCode], toChange: String = "USD") { self.action = action.rawValue self.subs = [] - codeList.forEach({ self.subs.append(Strings.coinPreRequest($0, toChange)) }) + codeList.forEach({ self.subs.append(String(format: String(localized: "CoinPreRequest"), $0, toChange)) }) } } diff --git a/SampleAppSwiftUI/Resources/Colors.xcassets/LightGray.colorset/Contents.json b/SampleAppSwiftUI/Resources/Colors.xcassets/LightestGray.colorset/Contents.json similarity index 100% rename from SampleAppSwiftUI/Resources/Colors.xcassets/LightGray.colorset/Contents.json rename to SampleAppSwiftUI/Resources/Colors.xcassets/LightestGray.colorset/Contents.json diff --git a/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift b/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift index a5a518e..92f2088 100644 --- a/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift +++ b/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift @@ -28,7 +28,7 @@ internal enum Resources { } internal enum Colors { internal static let color = ColorAsset(name: "Color") - internal static let lightGray = ColorAsset(name: "LightGray") + internal static let lightestGray = ColorAsset(name: "LightestGray") internal enum SearchBar { internal static let searchBarBackground = ColorAsset(name: "SearchBarBackground") internal static let searchIcon = ColorAsset(name: "SearchIcon") diff --git a/SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift b/SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift deleted file mode 100644 index 7ca7108..0000000 --- a/SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift +++ /dev/null @@ -1,48 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen - -import Foundation - -// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references - -// MARK: - Strings - -// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length -// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces -public enum Strings { - /// 5~CCCAGG~%@~%@ - public static func coinPreRequest(_ p1: Any, _ p2: Any) -> String { - return Strings.tr("Localizable", "CoinPreRequest", String(describing: p1), String(describing: p2), fallback: "5~CCCAGG~%@~%@") - } - /// Favorites - public static let favorites = Strings.tr("Localizable", "Favorites", fallback: "Favorites") - /// Localizable.strings - /// SampleAppSwiftUI - /// - /// Created by Selim Gungorer on 14.09.2022. - /// Copyright © 2022 Adesso Turkey. All rights reserved. - public static let helloWorld = Strings.tr("Localizable", "Hello, World!", fallback: "Hello") -} -// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length -// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces - -// MARK: - Implementation Details - -extension Strings { - private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { - let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) - return String(format: format, locale: Locale.current, arguments: args) - } -} - -// swiftlint:disable convenience_type -private final class BundleToken { - static let bundle: Bundle = { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: BundleToken.self) - #endif - }() -} -// swiftlint:enable convenience_type diff --git a/SampleAppSwiftUI/Resources/Localizable.xcstrings b/SampleAppSwiftUI/Resources/Localizable.xcstrings new file mode 100644 index 0000000..8b6aea1 --- /dev/null +++ b/SampleAppSwiftUI/Resources/Localizable.xcstrings @@ -0,0 +1,104 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "baseCoinChangeInfo" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "When you select a new base currency, all prices in the app will be displayed in that currency." + } + } + } + }, + "CoinPreRequest" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "5~CCCAGG~%@~%@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "5~CCCAGG~%@~%@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "5~CCCAGG~%@~%@" + } + } + } + }, + "Currency" : { + + }, + "Favorites" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorites" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriler" + } + } + } + }, + "Got it!" : { + + }, + "Hello, World!" : { + "comment" : "Localizable.strings\n SampleAppSwiftUI\n\n Created by Selim Gungorer on 14.09.2022.\n Copyright © 2022 Adesso Turkey. All rights reserved.", + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hallo" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hello" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Merhaba" + } + } + } + }, + "No Coins found." : { + + }, + "No price data found" : { + + }, + "Remove All Data" : { + + }, + "Settings" : { + + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings deleted file mode 100644 index 457f974..0000000 --- a/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings +++ /dev/null @@ -1,11 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Hallo"; -"Favorites" = "Favoriten"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; diff --git a/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings deleted file mode 100644 index b349968..0000000 --- a/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,11 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Hello"; -"Favorites" = "Favorites"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; diff --git a/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings deleted file mode 100644 index 4680893..0000000 --- a/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings +++ /dev/null @@ -1,11 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Merhaba"; -"Favorites" = "Favoriler"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index 76fc775..5d4024a 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -24,11 +24,11 @@ struct FavoritesView: View { } } .padding(.horizontal, Paddings.side) - .navigationTitle(Text(Strings.favorites)) + .navigationTitle(Text("Favorites")) .navigationBarTitleDisplayMode(.inline) .toolbar(content: createTopBar) } - .background(Color.lightGray) + .background(Color(.lightestGray)) .onAppear(perform: viewModel.fetchFavorites) .onDisappear(perform: viewModel.disconnect) .onChange(of: searchTerm, { _, newValue in diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift index 6895a2f..77bef41 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift @@ -23,7 +23,7 @@ struct CoinView: View { .imageFrame() } else if phase.error != nil { VStack { - Resources.Images.defaultCoin.swiftUIImage + Image(.defaultCoin) .resizable() .imageFrame() } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index e3921b8..cb24af0 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -23,7 +23,8 @@ struct CoinDetailView: View { image.resizable() } else if phase.error != nil { VStack { - Resources.Images.defaultCoin.swiftUIImage.resizable() + Image(.defaultCoin) + .resizable() } } else { ProgressView() diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index 3bf5ba8..9c43905 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -32,7 +32,7 @@ struct HomeView: View { } } } - .background(Color.lightGray) + .background(Color(.lightestGray)) .ignoresSafeArea(.all, edges: [.top, .trailing, .leading]) .onAppear { Task { diff --git a/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift b/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift index ff40299..e12bb54 100644 --- a/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift +++ b/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift @@ -14,10 +14,10 @@ struct SearchBarView: View { var body: some View { ZStack(alignment: .center) { RoundedRectangle(cornerRadius: Dimensions.CornerRadius.default) - .fill(Color.searchbarBackground) + .fill(Color(.searchBarBackground)) HStack(spacing: Spacings.default) { Image(systemName: Images.search) - .foregroundColor(.searchIcon) + .foregroundStyle(Color(.searchIcon)) TextField("Search for a name or symbol", text: $searchText) .font(Fonts.searchBar) .accessibilityIdentifier("searchBarViewInputField") diff --git a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift index 306cef8..6e5099a 100644 --- a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift +++ b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift @@ -32,7 +32,7 @@ extension SettingsView { VStack { HStack { Text("Settings") - .settingsTextStyle(fontType: .bold, fontSize: .title2, foregroundColor: .settingsViewTitleColor) + .settingsTextStyle(fontType: .bold, fontSize: .title2, foregroundColor: .settingsViewTitle) Spacer() } } @@ -42,7 +42,7 @@ extension SettingsView { private var darkButton: some View { VStack { Toggle("Dark Mode:", isOn: $isDarkModeOn) - .settingsTextStyle(fontSize: .body, foregroundColor: .settingsLineTitleColor) + .settingsTextStyle(fontSize: .body, foregroundColor: .settingsLineTitle) .settingsLineStyle(height: Dimensions.lineHeight) } .preferredColorScheme(isDarkModeOn ? .dark : .light) @@ -52,7 +52,7 @@ extension SettingsView { VStack(spacing: 0) { HStack { Text("Currency") - .settingsTextStyle(fontSize: .body, foregroundColor: .settingsLineTitleColor) + .settingsTextStyle(fontSize: .body, foregroundColor: .settingsLineTitle) Spacer() Picker("Parities", selection: $selectedParity) { ForEach(Parity.allCases) { parity in @@ -60,14 +60,14 @@ extension SettingsView { .accessibilityIdentifier("settingsViewParitySelectionPickerCell") } } - .tint(.settingsParitySetColor) + .tint(Color(.settingsParitySet)) .accessibilityIdentifier("settingsViewParitySelectionPicker") } .settingsLineStyle(height: Dimensions.lineHeight) .padding(.bottom, Spacings.home) - Text("When you select a new base currency, all prices in the app will be displayed in that currency.") - .settingsTextStyle(fontType: .regular, fontSize: .caption2, foregroundColor: .settingsCurrencyExpColor) + Text("baseCoinChangeInfo") + .settingsTextStyle(fontType: .regular, fontSize: .caption2, foregroundColor: .settingsCurrencyExp) } } @@ -76,7 +76,8 @@ extension SettingsView { print("Make an action") } label: { Text("Remove All Data") - .settingsTextStyle(fontType: .bold, fontSize: .body, foregroundColor: .white) + .settingsTextStyle(fontType: .bold, fontSize: .body) + .foregroundStyle(.white) } .settingsButtonStyle() } diff --git a/SampleAppSwiftUI/Utility/Extensions/ColorExtensions.swift b/SampleAppSwiftUI/Utility/Extensions/ColorExtensions.swift index 3d869f1..52e7927 100644 --- a/SampleAppSwiftUI/Utility/Extensions/ColorExtensions.swift +++ b/SampleAppSwiftUI/Utility/Extensions/ColorExtensions.swift @@ -10,16 +10,6 @@ import SwiftUI extension Color { static let label = Color(uiColor: .label) static let coinCellBackground = Color(uiColor: .tertiarySystemBackground) - static let lightGray = Color("LightGray") - static let searchbarBackground = Color("SearchBarBackground") - static let searchIcon = Color("SearchIcon") - /// Settings Screen - static let settingsCurrencyExpColor = Color("settingsCurrencyExpColor") - static let settingsLineTitleColor = Color("settingsLineTitleColor") - static let settingsParitySetColor = Color("settingsParitySetColor") - static let settingsButtonColor = Color("settingsButtonColor") - static let settingsLineColor = Color("settingsLineColor") - static let settingsViewTitleColor = Color("settingsViewTitleColor") /// Shadow static let shadowColor = Color.black.opacity(0.05) diff --git a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingButtonModifier.swift b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingButtonModifier.swift index 36a07d9..0ba0595 100644 --- a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingButtonModifier.swift +++ b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingButtonModifier.swift @@ -12,7 +12,7 @@ struct SettingButtonModifier: ViewModifier { func body(content: Content) -> some View { content .frame(maxWidth: .infinity, minHeight: Dimensions.settingsButonHeight) - .background(Color.settingsButtonColor) + .background(Color(.settingsButton)) .cornerRadius(Dimensions.CornerRadius.settingsButton) .padding(.bottom, Paddings.Settings.bottom) } diff --git a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingLineModifier.swift b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingLineModifier.swift index 48ba09b..23f7a21 100644 --- a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingLineModifier.swift +++ b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingLineModifier.swift @@ -14,7 +14,7 @@ struct SettingLineModifier: ViewModifier { content .padding(Paddings.Settings.line) .frame(height: height) - .background(Color.settingsLineColor) + .background(Color(.settingsLine)) .cornerRadius(Dimensions.CornerRadius.settingsButton) } } diff --git a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingTextModifier.swift b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingTextModifier.swift index 107b078..156a0dd 100644 --- a/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingTextModifier.swift +++ b/SampleAppSwiftUI/Utility/ViewModifiers/SettingViewModifiers/SettingTextModifier.swift @@ -10,17 +10,17 @@ import SwiftUI struct SettingTextModifier: ViewModifier { let fontType: Font.Weight let fontSize: Font - let foregroundColor: Color + let foregroundColor: ColorResource func body(content: Content) -> some View { content - .foregroundColor(foregroundColor) + .foregroundStyle(Color(foregroundColor)) .font(fontSize.weight(fontType)) } } extension View { - func settingsTextStyle(fontType: Font.Weight = .regular, fontSize: Font, foregroundColor: Color) -> some View { + func settingsTextStyle(fontType: Font.Weight = .regular, fontSize: Font, foregroundColor: ColorResource = .color) -> some View { modifier(SettingTextModifier(fontType: fontType, fontSize: fontSize, foregroundColor: foregroundColor)) } } diff --git a/project.yml b/project.yml index 4086009..1b03b3c 100644 --- a/project.yml +++ b/project.yml @@ -14,8 +14,9 @@ attributes: BuildIndependentTargetsInParallel: true settings: - enableBaseInternationalization: true ENABLE_USER_SCRIPT_SANDBOXING: false + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: true packages: CocoaLumberjack: @@ -53,9 +54,6 @@ targets: - path: scripts/installation/swiftlint.sh name: SwiftLint basedOnDependencyAnalysis: false - - path: scripts/installation/swiftgen.sh - name: SwiftGen - basedOnDependencyAnalysis: false SampleAppSwiftUITests: dependencies: - target: SampleAppSwiftUI @@ -74,3 +72,4 @@ targets: platform: iOS sources: - path: SampleAppSwiftUIUITests +parallelizeBuild: true diff --git a/scripts/installation/swiftgen.sh b/scripts/installation/swiftgen.sh deleted file mode 100644 index 6a14df8..0000000 --- a/scripts/installation/swiftgen.sh +++ /dev/null @@ -1,6 +0,0 @@ -export PATH="$PATH:/opt/homebrew/bin" -if which swiftgen > /dev/null; then -swiftgen config run -else -echo "warning: SwiftGen not installed, download it from https://github.com/SwiftGen/SwiftGen" -fi diff --git a/swiftgen.yml b/swiftgen.yml index 5cdb0de..c89ac85 100644 --- a/swiftgen.yml +++ b/swiftgen.yml @@ -1,15 +1,6 @@ input_dir: SampleAppSwiftUI/Resources output_dir: SampleAppSwiftUI/Resources/Constants/Generated -strings: - inputs: - - en.lproj - outputs: - templateName: structured-swift5 - params: - publicAccess: true - enumName: Strings - output: Strings+Generated.swift xcassets: inputs: - Assets.xcassets From 206ad35d65e3c2bf518d8a8e730f1960d2b64034 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 26 Jul 2023 22:56:52 +0300 Subject: [PATCH 07/40] Moved from ObservableObject to Observation API --- .../Application/SampleAppSwiftUIApp.swift | 5 ++--- .../Scenes/Favorites/FavoritesView.swift | 7 ++++--- .../Scenes/Favorites/FavoritesViewModel.swift | 11 ++++++----- .../Scenes/Home/Coin/CoinListView.swift | 4 ++-- .../Scenes/Home/Coin/Detail/CoinDetailView.swift | 6 +++--- .../Home/Coin/Detail/CoinDetailViewModel.swift | 14 ++++++++------ .../Coin/Detail/CoinPriceHistoryChartView.swift | 10 +++++----- .../Detail/CoinPriceHistoryChartViewModel.swift | 10 ++++++---- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 7 ++++--- SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift | 14 ++++++++------ SampleAppSwiftUI/Scenes/Main/MainView.swift | 13 +++++++------ SampleAppSwiftUI/Scenes/Router.swift | 14 ++++++++------ .../Scenes/Settings/SettingsView.swift | 5 +++-- SampleAppSwiftUI/Scenes/ViewModelProtocol.swift | 2 +- .../Utility/Managers/StorageManager.swift | 8 ++------ 15 files changed, 69 insertions(+), 61 deletions(-) diff --git a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift index 88a7943..9216fb7 100644 --- a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift +++ b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift @@ -14,7 +14,7 @@ struct SampleAppSwiftUIApp: App { // Check out https://developer.apple.com/documentation/swiftui/scenephase for more information @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate private var loggingService: LoggingService - @StateObject private var router: Router = Router() + @State private var router: Router = Router() init() { loggingService = LoggingService() @@ -22,8 +22,7 @@ struct SampleAppSwiftUIApp: App { var body: some Scene { WindowGroup { - MainView() - .environmentObject(router) + MainView(router: $router) .onChange(of: phase, { _, newValue in manageChanges(for: newValue) }) diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index 5d4024a..e561117 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -9,8 +9,9 @@ import SwiftUI struct FavoritesView: View { @State private var searchTerm = "" - @StateObject private var viewModel = FavoritesViewModel() - @EnvironmentObject private var router: Router + @State private var viewModel = FavoritesViewModel() + @Binding var router: Router + var body: some View { NavigationStack(path: $router.favoritesNavigationPath) { VStack(spacing: Spacings.favorites) { @@ -52,5 +53,5 @@ struct FavoritesView: View { } #Preview { - FavoritesView() + FavoritesView(router: .constant(.init())) } diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift index e2d26bb..3f120d3 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift @@ -8,10 +8,11 @@ import Foundation import SwiftUI import Combine +import Observation -class FavoritesViewModel: ObservableObject { - @Published var coins: [CoinData] = [] - @Published var filteredCoins: [CoinData] = [] +class FavoritesViewModel { + var coins: [CoinData] = [] + var filteredCoins: [CoinData] = [] private var webSocketService: any WebSocketServiceProtocol private var cancellable = Set() @@ -19,8 +20,8 @@ class FavoritesViewModel: ObservableObject { private var maxReconnectionCount: Int = 3 private let checkWebSocket = true - @Published var coinInfo: CoinData? - @Published var filterTitle = "Most Popular" + var coinInfo: CoinData? + var filterTitle = "Most Popular" let listPageLimit = 10 @State var isLoading: Bool = false diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift index 2722a0a..2813f4a 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift @@ -8,11 +8,11 @@ import SwiftUI struct CoinListView: View { - @ObservedObject var viewModel: ViewModel + @State var viewModel: ViewModel @Binding var filteredCoins: [CoinData] @State private var showingAlert = false @State private var alertTitle = "" - @EnvironmentObject private var router: Router + @State private var router: Router = Router() let favoriteChanged: () -> Void var body: some View { diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index cb24af0..6c5cfb5 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -8,10 +8,10 @@ import SwiftUI struct CoinDetailView: View { - @StateObject private var viewModel: CoinDetailViewModel + @State private var viewModel: CoinDetailViewModel init(coinData: CoinData) { - _viewModel = StateObject(wrappedValue: CoinDetailViewModel(coinData: coinData)) + _viewModel = State(wrappedValue: CoinDetailViewModel(coinData: coinData)) } var body: some View { @@ -61,7 +61,7 @@ struct CoinDetailView: View { CoinPriceHistoryChartView( selectedRange: viewModel.chartHistoryRangeSelection, dataModel: chartDataModel, - selectedXDateText: $viewModel.priceChartSelectedXDateText + selectedXDateText: viewModel.priceChartSelectedXDateText ) .padding(.horizontal, 16) .padding(.top, 34) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift index b7bfa5e..911ca62 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift @@ -6,18 +6,20 @@ // import Foundation +import Observation -class CoinDetailViewModel: ObservableObject { +@Observable +class CoinDetailViewModel { let coinData: CoinData - @Published private(set) var isFavorite: Bool = false - @Published var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth { + private(set) var isFavorite: Bool = false + var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth { didSet { fetchCoinPriceHistory(forSelectedRange: chartHistoryRangeSelection) } } - @Published private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? - @Published private(set) var isLoading: Bool = false - @Published var priceChartSelectedXDateText: String = "" + private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? + private(set) var isLoading: Bool = false + var priceChartSelectedXDateText: String = "" var rangeButtonsOpacity: Double { priceChartSelectedXDateText.isEmpty ? 1.0 : 0.0 diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift index 379b69e..955f8fc 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift @@ -9,10 +9,10 @@ import SwiftUI import Charts struct CoinPriceHistoryChartView: View { - @StateObject private var viewModel: CoinPriceHistoryChartViewModel + @State private var viewModel: CoinPriceHistoryChartViewModel - init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: Binding) { - _viewModel = StateObject( + init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: String) { + _viewModel = State( wrappedValue: CoinPriceHistoryChartViewModel( selectedRange: selectedRange, dataModel: dataModel, @@ -145,7 +145,7 @@ struct CoinPriceHistoryChartView: View { CoinPriceHistoryChartView( selectedRange: item, dataModel: .demo, - selectedXDateText: .constant("") + selectedXDateText: "" ) } } @@ -154,7 +154,7 @@ struct CoinPriceHistoryChartView: View { CoinPriceHistoryChartView( selectedRange: item, dataModel: .demo, - selectedXDateText: .constant("") + selectedXDateText: "" ) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift index 02bfcd6..f318939 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift @@ -8,15 +8,17 @@ import Foundation import SwiftUI import Charts +import Observation -class CoinPriceHistoryChartViewModel: ObservableObject { +@Observable +class CoinPriceHistoryChartViewModel { var selectedRange: CoinChartHistoryRange var dataModel: CoinPriceHistoryChartDataModel - @Binding var selectedXDateText: String + var selectedXDateText: String /// Holds the selected x value of chart when user is dragging on it - @Published var selectedX: (any Plottable)? + var selectedX: (any Plottable)? - init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: Binding) { + init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: String) { self.selectedRange = selectedRange self.dataModel = dataModel self._selectedXDateText = selectedXDateText diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index 9c43905..2333b2f 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -10,9 +10,10 @@ import SwiftUI struct HomeView: View { - @StateObject private var viewModel = HomeViewModel() + @State private var viewModel = HomeViewModel() @State private var searchTerm = "" - @EnvironmentObject private var router: Router + @Binding var router: Router + var body: some View { NavigationStack(path: $router.homeNavigationPath) { VStack(spacing: Spacings.home) { @@ -46,5 +47,5 @@ struct HomeView: View { } #Preview { - HomeView() + HomeView(router: .constant(.init())) } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift index 7ee8ae9..36fb090 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift @@ -8,15 +8,17 @@ import Foundation import SwiftUI import Combine +import Observation -class HomeViewModel: ObservableObject { - @Published var coinInfo: ExcangeRatesResponseModel? - @Published var coinList: [CoinData] = [] - @Published var filteredCoins: [CoinData] = [] - @Published var filterTitle = "Most Popular" +@Observable +class HomeViewModel { + var coinInfo: ExcangeRatesResponseModel? + var coinList: [CoinData] = [] + var filteredCoins: [CoinData] = [] + var filterTitle = "Most Popular" let listPageLimit = 10 - @State var isLoading: Bool = false + var isLoading: Bool = false func fillModels(demo: Bool = false) async { if demo { diff --git a/SampleAppSwiftUI/Scenes/Main/MainView.swift b/SampleAppSwiftUI/Scenes/Main/MainView.swift index 9cdfd69..4a5f024 100644 --- a/SampleAppSwiftUI/Scenes/Main/MainView.swift +++ b/SampleAppSwiftUI/Scenes/Main/MainView.swift @@ -10,23 +10,24 @@ import SwiftUI struct MainView: View { - @StateObject private var storageManager = StorageManager.shared - @EnvironmentObject private var router: Router + @State private var storageManager = StorageManager.shared + @Binding var router: Router + var body: some View { TabView(selection: $router.selectedTab) { - HomeView() + HomeView(router: $router) .tag(TabIndex.home) .tabItem { Image(systemName: TabIndex.home.imageName()) .accessibilityIdentifier("homeTabView") } - FavoritesView() + FavoritesView(router: $router) .tag(TabIndex.favorites) .tabItem { Image(systemName: TabIndex.favorites.imageName()) .accessibilityIdentifier("favoriteTabView") } - SettingsView() + SettingsView(router: $router) .tag(TabIndex.settings) .tabItem { Image(systemName: TabIndex.settings.imageName()) @@ -39,5 +40,5 @@ struct MainView: View { } #Preview { - MainView() + MainView(router: .constant(Router())) } diff --git a/SampleAppSwiftUI/Scenes/Router.swift b/SampleAppSwiftUI/Scenes/Router.swift index d20d980..f649828 100644 --- a/SampleAppSwiftUI/Scenes/Router.swift +++ b/SampleAppSwiftUI/Scenes/Router.swift @@ -7,13 +7,15 @@ import Foundation import SwiftUI +import Observation -final public class Router: ObservableObject { - @Published var homeNavigationPath: [Screen] = [] - @Published var favoritesNavigationPath: [Screen] = [] - @Published var settingsNavigationPath: [Screen] = [] - @Published var selectedTab: TabIndex = .home - var tabbarNames: [TabIndex] = [.home, .favorites, .settings] +@Observable +final public class Router { + var homeNavigationPath: [Screen] = [] + var favoritesNavigationPath: [Screen] = [] + var settingsNavigationPath: [Screen] = [] + var selectedTab: TabIndex = .home + @ObservationIgnored var tabbarNames: [TabIndex] = [.home, .favorites, .settings] func navigateCoinDetail(coinData: CoinData) { if selectedTab == .home { diff --git a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift index 6e5099a..8a3ab19 100644 --- a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift +++ b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift @@ -11,7 +11,8 @@ struct SettingsView: View { @State private var isDarkModeOn = false @State private var selectedParity: Parity = .USD - @EnvironmentObject private var router: Router + @Binding var router: Router + var body: some View { NavigationStack(path: $router.settingsNavigationPath) { VStack(alignment: .leading, spacing: Spacings.settings) { @@ -84,5 +85,5 @@ extension SettingsView { } #Preview { - SettingsView() + SettingsView(router: .constant(.init())) } diff --git a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift index 8b850e4..9954c5e 100644 --- a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift +++ b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift @@ -7,7 +7,7 @@ import Foundation -protocol ViewModelProtocol: ObservableObject { +protocol ViewModelProtocol { var isLoading: Bool { get set } func checkLastItem(_ item: CoinData) diff --git a/SampleAppSwiftUI/Utility/Managers/StorageManager.swift b/SampleAppSwiftUI/Utility/Managers/StorageManager.swift index f5695d2..06ca8d0 100644 --- a/SampleAppSwiftUI/Utility/Managers/StorageManager.swift +++ b/SampleAppSwiftUI/Utility/Managers/StorageManager.swift @@ -7,15 +7,11 @@ import SwiftUI -final class StorageManager: ObservableObject { +final class StorageManager { static let shared = StorageManager() - @AppStorage("favoriteCoins") var favoriteCoins: [CoinData] = [] { - didSet { - objectWillChange.send() - } - } + @AppStorage("favoriteCoins") var favoriteCoins: [CoinData] = [] private init() { } From b8bc9ada746cd49569609266912a9af08f6b59bc Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 2 Aug 2023 15:13:20 +0300 Subject: [PATCH 08/40] Update ios-build-check.yml access to the latest Xcode beta --- .github/workflows/ios-build-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 50724b0..6829d28 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -15,6 +15,9 @@ jobs: runs-on: macos-latest steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest - name: Checkout uses: actions/checkout@v2 - name: Xcodegen From 9727c9f890c6e97dc5fd0e67fdda1c3ed8776c52 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 2 Aug 2023 15:17:07 +0300 Subject: [PATCH 09/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 6829d28..4bde032 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -12,8 +12,7 @@ env: jobs: build: name: Build scheme - runs-on: macos-latest - + runs-on: macos-13 steps: - uses: maxim-lobanov/setup-xcode@v1 with: From 8b0dcca1e98563c16b52f9ec2295a8f5d486fc2b Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 18 Aug 2023 20:48:19 +0300 Subject: [PATCH 10/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 4bde032..d323906 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -36,3 +36,4 @@ jobs: with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: build/SampleAppSwiftUI.app + ios-version: 17 From 40830057e9bb2caa44cf7e35a2de73d31e5b0052 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 18 Aug 2023 22:34:43 +0300 Subject: [PATCH 11/40] SASU-0077 Updated from development merge --- .../Generated/Assets+Generated.swift | 214 ------------------ .../Resources/Localizable.xcstrings | 26 +++ .../Resources/de.lproj/Localizable.strings | 12 - .../Resources/en.lproj/Localizable.strings | 13 -- .../Resources/tr.lproj/Localizable.strings | 13 -- .../Scenes/Favorites/FavoritesView.swift | 4 +- .../Home/Coin/CoinNews/CoinNewsListView.swift | 9 +- .../Home/Coin/Detail/CoinDetailView.swift | 159 +++++++------ .../Coin/Detail/CoinDetailViewModel.swift | 2 +- SampleAppSwiftUI/Scenes/Home/FilterView.swift | 8 +- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 6 +- .../Scenes/Home/HomeViewModel.swift | 12 +- .../Scenes/ViewModelProtocol.swift | 3 +- 13 files changed, 139 insertions(+), 342 deletions(-) delete mode 100644 SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift delete mode 100644 SampleAppSwiftUI/Resources/de.lproj/Localizable.strings delete mode 100644 SampleAppSwiftUI/Resources/en.lproj/Localizable.strings delete mode 100644 SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings diff --git a/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift b/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift deleted file mode 100644 index e022b01..0000000 --- a/SampleAppSwiftUI/Resources/Constants/Generated/Assets+Generated.swift +++ /dev/null @@ -1,214 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen - -#if os(macOS) - import AppKit -#elseif os(iOS) - import UIKit -#elseif os(tvOS) || os(watchOS) - import UIKit -#endif -#if canImport(SwiftUI) - import SwiftUI -#endif - -// Deprecated typealiases -@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") -internal typealias AssetColorTypeAlias = ColorAsset.Color -@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") -internal typealias AssetImageTypeAlias = ImageAsset.Image - -// swiftlint:disable superfluous_disable_command file_length implicit_return - -// MARK: - Asset Catalogs - -// swiftlint:disable identifier_name line_length nesting type_body_length type_name -internal enum Resources { - internal enum Assets { - } - internal enum Colors { - internal static let color = ColorAsset(name: "Color") - internal static let lightestGray = ColorAsset(name: "LightestGray") - internal enum SearchBar { - internal static let searchBarBackground = ColorAsset(name: "SearchBarBackground") - internal static let searchIcon = ColorAsset(name: "SearchIcon") - } - internal enum SettingsScreen { - internal static let settingsButtonColor = ColorAsset(name: "settingsButtonColor") - internal static let settingsCurrencyExpColor = ColorAsset(name: "settingsCurrencyExpColor") - internal static let settingsLineColor = ColorAsset(name: "settingsLineColor") - internal static let settingsLineTitleColor = ColorAsset(name: "settingsLineTitleColor") - internal static let settingsParitySetColor = ColorAsset(name: "settingsParitySetColor") - internal static let settingsViewTitleColor = ColorAsset(name: "settingsViewTitleColor") - } - } - internal enum Icons { - internal static let image = ImageAsset(name: "Image") - } - internal enum Images { - internal static let binance = ImageAsset(name: "binance") - internal static let btc = ImageAsset(name: "btc") - internal static let defaultCoin = ImageAsset(name: "default-coin") - internal static let worldNews = ImageAsset(name: "world-news") - } -} -// swiftlint:enable identifier_name line_length nesting type_body_length type_name - -// MARK: - Implementation Details - -internal final class ColorAsset { - internal fileprivate(set) var name: String - - #if os(macOS) - internal typealias Color = NSColor - #elseif os(iOS) || os(tvOS) || os(watchOS) - internal typealias Color = UIColor - #endif - - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) - internal private(set) lazy var color: Color = { - guard let color = Color(asset: self) else { - fatalError("Unable to load color asset named \(name).") - } - return color - }() - - #if os(iOS) || os(tvOS) - @available(iOS 11.0, tvOS 11.0, *) - internal func color(compatibleWith traitCollection: UITraitCollection) -> Color { - let bundle = BundleToken.bundle - guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { - fatalError("Unable to load color asset named \(name).") - } - return color - } - #endif - - #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - internal private(set) lazy var swiftUIColor: SwiftUI.Color = { - SwiftUI.Color(asset: self) - }() - #endif - - fileprivate init(name: String) { - self.name = name - } -} - -internal extension ColorAsset.Color { - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) - convenience init?(asset: ColorAsset) { - let bundle = BundleToken.bundle - #if os(iOS) || os(tvOS) - self.init(named: asset.name, in: bundle, compatibleWith: nil) - #elseif os(macOS) - self.init(named: NSColor.Name(asset.name), bundle: bundle) - #elseif os(watchOS) - self.init(named: asset.name) - #endif - } -} - -#if canImport(SwiftUI) -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -internal extension SwiftUI.Color { - init(asset: ColorAsset) { - let bundle = BundleToken.bundle - self.init(asset.name, bundle: bundle) - } -} -#endif - -internal struct ImageAsset { - internal fileprivate(set) var name: String - - #if os(macOS) - internal typealias Image = NSImage - #elseif os(iOS) || os(tvOS) || os(watchOS) - internal typealias Image = UIImage - #endif - - @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) - internal var image: Image { - let bundle = BundleToken.bundle - #if os(iOS) || os(tvOS) - let image = Image(named: name, in: bundle, compatibleWith: nil) - #elseif os(macOS) - let name = NSImage.Name(self.name) - let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) - #elseif os(watchOS) - let image = Image(named: name) - #endif - guard let result = image else { - fatalError("Unable to load image asset named \(name).") - } - return result - } - - #if os(iOS) || os(tvOS) - @available(iOS 8.0, tvOS 9.0, *) - internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { - let bundle = BundleToken.bundle - guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { - fatalError("Unable to load image asset named \(name).") - } - return result - } - #endif - - #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - internal var swiftUIImage: SwiftUI.Image { - SwiftUI.Image(asset: self) - } - #endif -} - -internal extension ImageAsset.Image { - @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) - @available(macOS, deprecated, - message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") - convenience init?(asset: ImageAsset) { - #if os(iOS) || os(tvOS) - let bundle = BundleToken.bundle - self.init(named: asset.name, in: bundle, compatibleWith: nil) - #elseif os(macOS) - self.init(named: NSImage.Name(asset.name)) - #elseif os(watchOS) - self.init(named: asset.name) - #endif - } -} - -#if canImport(SwiftUI) -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -internal extension SwiftUI.Image { - init(asset: ImageAsset) { - let bundle = BundleToken.bundle - self.init(asset.name, bundle: bundle) - } - - init(asset: ImageAsset, label: Text) { - let bundle = BundleToken.bundle - self.init(asset.name, bundle: bundle, label: label) - } - - init(decorative asset: ImageAsset) { - let bundle = BundleToken.bundle - self.init(decorative: asset.name, bundle: bundle) - } -} -#endif - -// swiftlint:disable convenience_type -private final class BundleToken { - static let bundle: Bundle = { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: BundleToken.self) - #endif - }() -} -// swiftlint:enable convenience_type diff --git a/SampleAppSwiftUI/Resources/Localizable.xcstrings b/SampleAppSwiftUI/Resources/Localizable.xcstrings index 8b6aea1..f27f936 100644 --- a/SampleAppSwiftUI/Resources/Localizable.xcstrings +++ b/SampleAppSwiftUI/Resources/Localizable.xcstrings @@ -87,6 +87,29 @@ } } }, + "News" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "News" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haberler" + } + } + } + }, "No Coins found." : { }, @@ -98,6 +121,9 @@ }, "Settings" : { + }, + "View More" : { + } }, "version" : "1.0" diff --git a/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings deleted file mode 100644 index 6692311..0000000 --- a/SampleAppSwiftUI/Resources/de.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Hallo"; -"Favorites" = "Favoriten"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; -"News" = "Nachricht"; diff --git a/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings deleted file mode 100644 index 17021f4..0000000 --- a/SampleAppSwiftUI/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,13 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Hello"; -"Favorites" = "Favorites"; -"Home" = "Home"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; -"News" = "News"; diff --git a/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings b/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings deleted file mode 100644 index 9c9c253..0000000 --- a/SampleAppSwiftUI/Resources/tr.lproj/Localizable.strings +++ /dev/null @@ -1,13 +0,0 @@ -/* - Localizable.strings - SampleAppSwiftUI - - Created by Selim Gungorer on 14.09.2022. - Copyright © 2022 Adesso Turkey. All rights reserved. -*/ - -"Hello, World!" = "Merhaba"; -"Favorites" = "Favoriler"; -"Home" = "Ana Sayfa"; -"CoinPreRequest" = "5~CCCAGG~%@~%@"; -"News" = "Haberler"; diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index a2dc35b..a32be57 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -41,7 +41,9 @@ struct FavoritesView: View { .onChange(of: StorageManager.shared.favoriteCoins, { _, newValue in fetchFavorites(codes: newValue) }) - .onChange(of: viewModel.selectedSortOption, perform: viewModel.sortOptions(sort:)) + .onChange(of: viewModel.selectedSortOption, { _, new in + viewModel.sortOptions(sort: new) + }) } private func fetchFavorites(codes: [CoinData]) { diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift index be000d2..1d01bb1 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift @@ -8,10 +8,10 @@ import SwiftUI struct CoinNewsListView: View { - @StateObject private var viewModel: CoinDetailViewModel + @State private var viewModel: CoinDetailViewModel init(coinData: CoinData) { - _viewModel = StateObject(wrappedValue: CoinDetailViewModel(coinData: coinData)) + _viewModel = State(wrappedValue: CoinDetailViewModel(coinData: coinData)) } var body: some View { @@ -27,7 +27,8 @@ struct CoinNewsListView: View { if let image = phase.image { image.resizable() } else { - Resources.Images.worldNews.swiftUIImage.resizable() + Image(.worldNews) + .resizable() } } .scaledToFit() @@ -41,7 +42,7 @@ struct CoinNewsListView: View { .listStyle(.inset) } }.frame(height: UIScreen.main.bounds.height) - .navigationTitle(Strings.news) + .navigationTitle("News") .navigationBarTitleDisplayMode(.inline) .navigationViewStyle(.automatic) } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index 6b5ca31..d97eb26 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -18,100 +18,109 @@ struct CoinDetailView: View { @State private var viewModel: CoinDetailViewModel init(coinData: CoinData) { - _viewModel = State(wrappedValue: CoinDetailViewModel(coinData: coinData)) + viewModel = CoinDetailViewModel(coinData: coinData) } - var body: some View { + var coinDetailImage: some View { VStack { - ScrollView { - VStack { - AsyncImage(url: viewModel.getIconURL()) { phase in - if let image = phase.image { - image.resizable() - } else if phase.error != nil { - VStack { - Image(.defaultCoin) - .resizable() - } - } else { - ProgressView() - .imageFrame() - } + AsyncImage(url: viewModel.getIconURL()) { phase in + if let image = phase.image { + image.resizable() + } else if phase.error != nil { + VStack { + Image(.defaultCoin) + .resizable() } - .scaledToFit() - .imageFrame(width: Dimensions.imageWidth * 2, height: Dimensions.imageHeight * 2) + } else { + ProgressView() + .imageFrame() + } + } + .scaledToFit() + .imageFrame(width: Dimensions.imageWidth * 2, height: Dimensions.imageHeight * 2) - Text(verbatim: viewModel.getPriceString()) + Text(verbatim: viewModel.getPriceString()) - ChangePercentageView(changeRate: viewModel.coinData.detail?.usd ?? .init()) + ChangePercentageView(changeRate: viewModel.coinData.detail?.usd ?? .init()) - ZStack { - CoinChartHistoryRangeButtons(selection: $viewModel.chartHistoryRangeSelection) - .opacity(viewModel.rangeButtonsOpacity) + ZStack { + CoinChartHistoryRangeButtons(selection: $viewModel.chartHistoryRangeSelection) + .opacity(viewModel.rangeButtonsOpacity) - Text(verbatim: viewModel.priceChartSelectedXDateText) - .font(.headline) - .padding(.vertical, 4) - .padding(.horizontal) - } - .padding(.top, 22) - .padding(.bottom, 12) + Text(verbatim: viewModel.priceChartSelectedXDateText) + .font(.headline) + .padding(.vertical, 4) + .padding(.horizontal) + } + .padding(.top, 22) + .padding(.bottom, 12) - ZStack(alignment: .center) { - RoundedRectangle(cornerRadius: Dimensions.CornerRadius.default) - .fill(Color(uiColor: .systemGray6)) + ZStack(alignment: .center) { + RoundedRectangle(cornerRadius: Dimensions.CornerRadius.default) + .fill(Color(uiColor: .systemGray6)) - if viewModel.isLoading { - ProgressView() - } else { - if let chartDataModel = viewModel.coinPriceHistoryChartDataModel { - CoinPriceHistoryChartView( - selectedRange: viewModel.chartHistoryRangeSelection, - dataModel: chartDataModel, - selectedXDateText: viewModel.priceChartSelectedXDateText - ) - .padding(.horizontal, 16) - .padding(.top, 34) - .padding(.bottom, 18) - } else { - Text("No price data found") - } - } + if viewModel.isLoading { + ProgressView() + } else { + if let chartDataModel = viewModel.coinPriceHistoryChartDataModel { + CoinPriceHistoryChartView( + selectedRange: viewModel.chartHistoryRangeSelection, + dataModel: chartDataModel, + selectedXDateText: viewModel.priceChartSelectedXDateText + ) + .padding(.horizontal, 16) + .padding(.top, 34) + .padding(.bottom, 18) + } else { + Text("No price data found") } - .frame(minHeight: CoinDetailView.chartHeight) - .cornerRadius(Dimensions.CornerRadius.default) } - .padding(.horizontal, Paddings.side) - NavigationView { - VStack { - if let newsModel = viewModel.coinNewsDataModel { - List { - Section(Strings.news) { - ForEach(newsModel.prefix(5)) { model in - NavigationLink(destination: WebView(url: URL(string: model.url))) { - HStack { - AsyncImage(url: URL(string: model.imageurl)) { phase in - if let image = phase.image { - image.resizable() - } else { - Resources.Images.worldNews.swiftUIImage.resizable() - } - } - .scaledToFit() - .clipShape(Circle()) - .frame(width: Dimensions.imageWidth, height: Dimensions.imageHeight) - Text(model.title) - .limitedCharacterCount(Numbers.newsCharCount, model.title, "...") + } + .frame(minHeight: CoinDetailView.chartHeight) + .cornerRadius(Dimensions.CornerRadius.default) + } + .padding(.horizontal, Paddings.side) + } + + var news: some View { + NavigationView { + VStack { + if let newsModel = viewModel.coinNewsDataModel { + List { + Section("News") { + ForEach(newsModel.prefix(5)) { model in + NavigationLink(destination: WebView(url: URL(string: model.url))) { + HStack { + AsyncImage(url: URL(string: model.imageurl)) { phase in + if let image = phase.image { + image.resizable() + } else { + Image(.worldNews) + .resizable() } } + .scaledToFit() + .clipShape(Circle()) + .frame(width: Dimensions.imageWidth, height: Dimensions.imageHeight) + Text(model.title) + .limitedCharacterCount(Numbers.newsCharCount, model.title, "...") } } } - .scrollDisabled(true) - .listStyle(.inset) } } + .scrollDisabled(true) + .listStyle(.inset) } + } + } + } + + var body: some View { + VStack { + ScrollView { + coinDetailImage + news Button { } label: { NavigationLink(destination: CoinNewsListView(coinData: viewModel.coinData)) { @@ -121,7 +130,7 @@ struct CoinDetailView: View { .padding() .foregroundColor(Color.searchIcon) } - }.background(Color.lightGray) + }.background(Color.lightestGray) .cornerRadius(CoinDetailView.viewMoreButton) Spacer() } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift index a257d12..a66a07d 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift @@ -19,8 +19,8 @@ class CoinDetailViewModel { } private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? private(set) var isLoading: Bool = false + var coinNewsDataModel: [CoinNewData]? var priceChartSelectedXDateText: String = "" - @Published private(set) var isLoading: Bool = false var rangeButtonsOpacity: Double { priceChartSelectedXDateText.isEmpty ? 1.0 : 0.0 diff --git a/SampleAppSwiftUI/Scenes/Home/FilterView.swift b/SampleAppSwiftUI/Scenes/Home/FilterView.swift index 1e96e90..33949af 100644 --- a/SampleAppSwiftUI/Scenes/Home/FilterView.swift +++ b/SampleAppSwiftUI/Scenes/Home/FilterView.swift @@ -8,17 +8,19 @@ import SwiftUI struct FilterView: View { - @ObservedObject var viewModel: ViewModel + var viewModel: ViewModel + + @State private var selectedSort: SortOptions = .defaultList var body: some View { HStack { - Text(viewModel.selectedSortOption.rawValue) + Text(selectedSort.rawValue) Spacer() Menu { ForEach(SortOptions.allCases, id: \.rawValue) { sortOption in Button { - viewModel.selectedSortOption = sortOption + selectedSort = sortOption } label: { Text(sortOption.rawValue) } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index 3b73d49..0f155b8 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -27,7 +27,7 @@ struct HomeView: View { } } }.padding(.horizontal, Paddings.side) - .navigationTitle(Text(Strings.home)) + .navigationTitle("Home") .navigationBarTitleDisplayMode(.inline) .navigationDestination(for: Screen.self) { screen in if screen.type == .detail, let data = screen.data as? CoinData { @@ -46,7 +46,9 @@ struct HomeView: View { viewModel.filterResults(searchTerm: newValue) viewModel.sortOptions(sort: viewModel.selectedSortOption) }) - .onChange(of: viewModel.selectedSortOption, perform: viewModel.sortOptions(sort:)) + .onChange(of: viewModel.selectedSortOption, { _, newValue in + viewModel.sortOptions(sort: newValue) + }) } } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift index efd222f..65ab43b 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift @@ -15,8 +15,7 @@ class HomeViewModel { var coinInfo: ExcangeRatesResponseModel? var coinList: [CoinData] = [] var filteredCoins: [CoinData] = [] - var filterTitle = "Most Popular" - @Published var filterTitle = SortOptions.defaultList.rawValue + var filterTitle = SortOptions.defaultList.rawValue let listPageLimit = 10 var isLoading: Bool = false @@ -62,6 +61,15 @@ class HomeViewModel { } extension HomeViewModel: ViewModelProtocol { + var selectedSortOption: SortOptions { + get { + .defaultList + } + set { + self.selectedSortOption = newValue + } + } + func checkLastItem(_ item: CoinData) { guard !isLoading else { return } diff --git a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift index 079721e..ee04fac 100644 --- a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift +++ b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift @@ -7,11 +7,10 @@ import Foundation -protocol ViewModelProtocol { +protocol ViewModelProtocol: AnyObject { var isLoading: Bool { get set } var filteredCoins: [CoinData] { get set } var coinList: [CoinData] { get } - var selectedSortOption: SortOptions { get set } func checkLastItem(_ item: CoinData) } From 702319842db0ed1d99338cf81b1bdc99048b0644 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 18 Aug 2023 23:00:25 +0300 Subject: [PATCH 12/40] SASU-0077 Changed the reference --- SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index d97eb26..dfe8de7 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -128,7 +128,7 @@ struct CoinDetailView: View { .frame(width: UIScreen.main.bounds.size.width - CoinDetailView.coinListFrameSize) .font(.system(size: 18)) .padding() - .foregroundColor(Color.searchIcon) + .foregroundColor(Color(.searchIcon)) } }.background(Color.lightestGray) .cornerRadius(CoinDetailView.viewMoreButton) From c761158c7a5e2a699cd888ec9903a21b81fceea5 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Sat, 19 Aug 2023 00:35:40 +0300 Subject: [PATCH 13/40] SASU-0077 Fixed the protocol issues --- .../Scenes/Favorites/FavoritesViewModel.swift | 13 +++++++------ SampleAppSwiftUI/Scenes/Home/FilterView.swift | 6 ++---- SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift | 9 +-------- SampleAppSwiftUI/Scenes/ViewModelProtocol.swift | 4 +++- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift index 23661d5..8caa8c7 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI import Combine import Observation +@Observable class FavoritesViewModel { private let checkWebSocket = true @@ -18,13 +19,13 @@ class FavoritesViewModel { private var reconnectionCount: Int = 0 private var maxReconnectionCount: Int = 3 - @Published var coinList: [CoinData] = [] - @Published var filteredCoins: [CoinData] = [] - @Published var coinInfo: CoinData? - @Published var filterTitle = SortOptions.defaultList.rawValue - @Published var selectedSortOption: SortOptions = .defaultList + var coinList: [CoinData] = [] + var filteredCoins: [CoinData] = [] + var coinInfo: CoinData? + var filterTitle = SortOptions.defaultList.rawValue + var selectedSortOption: SortOptions = .defaultList - @State var isLoading: Bool = false + var isLoading: Bool = false init(webSocketService: any WebSocketServiceProtocol = WebSocketService.shared) { self.webSocketService = webSocketService diff --git a/SampleAppSwiftUI/Scenes/Home/FilterView.swift b/SampleAppSwiftUI/Scenes/Home/FilterView.swift index 33949af..be36c64 100644 --- a/SampleAppSwiftUI/Scenes/Home/FilterView.swift +++ b/SampleAppSwiftUI/Scenes/Home/FilterView.swift @@ -10,17 +10,15 @@ import SwiftUI struct FilterView: View { var viewModel: ViewModel - @State private var selectedSort: SortOptions = .defaultList - var body: some View { HStack { - Text(selectedSort.rawValue) + Text(viewModel.selectedSortOption.rawValue) Spacer() Menu { ForEach(SortOptions.allCases, id: \.rawValue) { sortOption in Button { - selectedSort = sortOption + viewModel.selectedSortOption = sortOption } label: { Text(sortOption.rawValue) } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift index 65ab43b..4ffd177 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift @@ -19,6 +19,7 @@ class HomeViewModel { let listPageLimit = 10 var isLoading: Bool = false + var selectedSortOption: SortOptions = .defaultList func fillModels(demo: Bool = false) async { if demo { @@ -61,14 +62,6 @@ class HomeViewModel { } extension HomeViewModel: ViewModelProtocol { - var selectedSortOption: SortOptions { - get { - .defaultList - } - set { - self.selectedSortOption = newValue - } - } func checkLastItem(_ item: CoinData) { guard !isLoading else { return } diff --git a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift index ee04fac..cd73c28 100644 --- a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift +++ b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift @@ -7,16 +7,18 @@ import Foundation -protocol ViewModelProtocol: AnyObject { +protocol ViewModelProtocol: ObservableObject { var isLoading: Bool { get set } var filteredCoins: [CoinData] { get set } var coinList: [CoinData] { get } + var selectedSortOption: SortOptions { get set } func checkLastItem(_ item: CoinData) } extension ViewModelProtocol { func checkLastItem(_ item: CoinData) {} + func sortOptions(sort: SortOptions) { switch sort { case .defaultList: From e69e9504fc513be1f2f852e5c345652e8fdd3722 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Sat, 19 Aug 2023 00:52:02 +0300 Subject: [PATCH 14/40] SASU-0077 Fixed the issue of build settings not being added. --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 1b03b3c..90dadce 100644 --- a/project.yml +++ b/project.yml @@ -16,7 +16,7 @@ attributes: settings: ENABLE_USER_SCRIPT_SANDBOXING: false CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true - ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: true + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: YES packages: CocoaLumberjack: From 94ad9007686074bd971be13d8a9b9cace4102ba8 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Sat, 19 Aug 2023 00:58:45 +0300 Subject: [PATCH 15/40] SASU-0077 Fixed a broken image reference --- SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index dfe8de7..29d2ab8 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -130,7 +130,7 @@ struct CoinDetailView: View { .padding() .foregroundColor(Color(.searchIcon)) } - }.background(Color.lightestGray) + }.background(Color(.lightestGray)) .cornerRadius(CoinDetailView.viewMoreButton) Spacer() } From e669d692a0ce80f89136b8ba5ff41c4103f8c017 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Sat, 19 Aug 2023 01:08:44 +0300 Subject: [PATCH 16/40] SASU-0077 Added comment to the UI Tests Maestro cloud does not support iOS 17 right now and crashes the builds. --- .github/workflows/ios-build-check.yml | 13 +++++++------ project.yml | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index d323906..b9d0746 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -31,9 +31,10 @@ jobs: - name: Build run: | xcodebuild -scheme SampleAppSwiftUI clean build -sdk iphoneos -configuration Development CODE_SIGNING_ALLOWED=No -destination 'generic/platform=iOS Simulator' CONFIGURATION_BUILD_DIR=$PWD/build - - name: UI Test - uses: mobile-dev-inc/action-maestro-cloud@v1 - with: - api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} - app-file: build/SampleAppSwiftUI.app - ios-version: 17 + # Commenting UI Tests until Maestro supports iOS 17 in order to not crash beta CI/CD Builds + # - name: UI Test + # uses: mobile-dev-inc/action-maestro-cloud@v1 + # with: + # api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} + # app-file: build/SampleAppSwiftUI.app + # ios-version: 17 diff --git a/project.yml b/project.yml index 90dadce..070e23d 100644 --- a/project.yml +++ b/project.yml @@ -11,7 +11,7 @@ options: iOS: 17.0 attributes: - BuildIndependentTargetsInParallel: true + BuildIndependentTargetsInParallel: YES settings: ENABLE_USER_SCRIPT_SANDBOXING: false From 87bf2ae2d760724671301881f957c5d5ee4e12f7 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Sat, 19 Aug 2023 01:36:40 +0300 Subject: [PATCH 17/40] SASU-0077 Bump Checkout to V3. --- .github/workflows/ios-build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index b9d0746..29c828a 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -18,7 +18,7 @@ jobs: with: xcode-version: latest - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Xcodegen uses: xavierLowmiller/xcodegen-action@1.1.2 with: From 7cbd4c5f82227ee06df45fcb5f9e260feafd4279 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Mon, 21 Aug 2023 10:40:59 +0300 Subject: [PATCH 18/40] SASU-0077 Removed unnecessary Swiftgen file --- swiftgen.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 swiftgen.yml diff --git a/swiftgen.yml b/swiftgen.yml deleted file mode 100644 index c89ac85..0000000 --- a/swiftgen.yml +++ /dev/null @@ -1,17 +0,0 @@ -input_dir: SampleAppSwiftUI/Resources -output_dir: SampleAppSwiftUI/Resources/Constants/Generated - -xcassets: - inputs: - - Assets.xcassets - - Colors.xcassets - - Icons.xcassets - - Images.xcassets - outputs: - templateName: swift5 - params: - forceProvidesNamespaces: true - forceFileNameEnum: true - enumName: Resources - output: Assets+Generated.swift - From 627f27206d70abfe328b2127bebe8cd6f218985b Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Mon, 21 Aug 2023 10:45:01 +0300 Subject: [PATCH 19/40] SASU-0077 Update Readme file --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d8fa131..1d22e38 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![swift-version](https://img.shields.io/badge/swift-5.8-brightgreen.svg)](https://github.com/apple/swift) +[![swift-version](https://img.shields.io/badge/swift-5.9-brightgreen.svg)](https://github.com/apple/swift) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/adessoTurkey/boilerplate-ios-swiftui/iOS%20Build%20Check%20Workflow/develop) @@ -8,10 +8,11 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves ### Spesifications -- Minimum target of iOS 16 +- Minimum target of iOS 17 - Usage of Swift concurrency (async/await) - WebSocket implementation for real-time price change. - Up-to-date SWiftUI features (Navigation stack etc.) +- Using the new String Catalog - Using only [SPM](https://www.swift.org/package-manager/) for package dependencies. - Able to favorite and save the coins using [AppStorage](https://developer.apple.com/documentation/swiftui/appstorage). @@ -27,11 +28,10 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves ## Prerequisites -- [Swift 5.8](https://developer.apple.com/support/xcode/) +- [Swift 5.9](https://developer.apple.com/support/xcode/) - [MacOS Monterey (12.5 or higher)](https://www.apple.com/by/macos/monterey/features/) -- [Xcode 14 or higher](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes) +- [Xcode 15 or higher](https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes) - [Swiftlint][github/swiftlint] -- [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [XcodeGen](https://github.com/yonaskolb/XcodeGen) ## Installation @@ -103,7 +103,6 @@ Gitflow is a legacy Git workflow that was originally a disruptive and novel stra ## Useful Tools and Resources -- [SwiftGen](https://github.com/SwiftGen/SwiftGen) - SwiftGen is a tool to automatically generate Swift code for resources of your projects (like images, localised strings, etc), to make them type-safe to use. - [XcodeGen](https://github.com/yonaskolb/XcodeGen) - XcodeGen is a command line tool written in Swift that generates your Xcode project using your folder structure and a project spec. - [SwiftLint][github/swiftlint] - A tool to enforce Swift style and conventions. - [TestFlight](https://help.apple.com/itunes-connect/developer/#/devdc42b26b8) - TestFlight beta testing lets you distribute beta builds of your app to testers and collect feedback. From 39f6f3586a917a2ff0dd3c8eaf67539f868cf62c Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Mon, 21 Aug 2023 10:47:23 +0300 Subject: [PATCH 20/40] SASU-0077 Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d22e38..d0e259e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves ## Prerequisites - [Swift 5.9](https://developer.apple.com/support/xcode/) -- [MacOS Monterey (12.5 or higher)](https://www.apple.com/by/macos/monterey/features/) +- [MacOS Ventura (13.4 or higher)](https://www.apple.com/macos/ventura/features/) - [Xcode 15 or higher](https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes) - [Swiftlint][github/swiftlint] - [XcodeGen](https://github.com/yonaskolb/XcodeGen) From a4912db36744ada60a55d7eec57ab4c7e74873a6 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 23 Aug 2023 13:52:19 +0300 Subject: [PATCH 21/40] SASU-0077 Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d0e259e..4d3048d 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,10 @@ Because XcodeGen is used in this project, there will be no `.xcodeproj` or `.xcw xcodegen generate ``` -Swiftlint and SwiftGen can also be installed via included scripts in the repository. Under the `{project_root}/scripts/installation` directory, simply run either or both of: +Swiftlint can also be installed via included scripts in the repository. Under the `{project_root}/scripts/installation` directory, simply run either or both of: ``` sh swiftlint.sh -sh swiftgen.sh ``` ## Branching Strategy From 1ee95b3848989f211fa0b2516a2bd5f391f4e5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baha=20Ulu=C4=9F?= <75128919+bahaadesso@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:14:56 +0300 Subject: [PATCH 22/40] Update ios-build-check.yml Maestro Cloud github actions --- .github/workflows/ios-build-check.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 29c828a..b882b8c 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -12,7 +12,7 @@ env: jobs: build: name: Build scheme - runs-on: macos-13 + runs-on: macos-latest steps: - uses: maxim-lobanov/setup-xcode@v1 with: @@ -31,10 +31,9 @@ jobs: - name: Build run: | xcodebuild -scheme SampleAppSwiftUI clean build -sdk iphoneos -configuration Development CODE_SIGNING_ALLOWED=No -destination 'generic/platform=iOS Simulator' CONFIGURATION_BUILD_DIR=$PWD/build - # Commenting UI Tests until Maestro supports iOS 17 in order to not crash beta CI/CD Builds - # - name: UI Test - # uses: mobile-dev-inc/action-maestro-cloud@v1 - # with: - # api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} - # app-file: build/SampleAppSwiftUI.app - # ios-version: 17 + - name: UI Test + uses: mobile-dev-inc/action-maestro-cloud@v1 + with: + api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} + app-file: build/SampleAppSwiftUI.app + ios-version: 16 From 90445018f9d82b93aea1fa2e9108963682487719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baha=20Ulu=C4=9F?= <75128919+bahaadesso@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:18:56 +0300 Subject: [PATCH 23/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index b882b8c..394c1b9 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -12,7 +12,7 @@ env: jobs: build: name: Build scheme - runs-on: macos-latest + runs-on: macos-13 steps: - uses: maxim-lobanov/setup-xcode@v1 with: From adbe7616471cb44acac19686b84f908b29b87d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baha=20Ulu=C4=9F?= <75128919+bahaadesso@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:28:10 +0300 Subject: [PATCH 24/40] Update project.yml target version changed to iOS 16 --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 070e23d..912d969 100644 --- a/project.yml +++ b/project.yml @@ -8,7 +8,7 @@ configs: options: bundleIdPrefix: com.adesso deploymentTarget: - iOS: 17.0 + iOS: 16.0 attributes: BuildIndependentTargetsInParallel: YES From b4218e4e8756d745862c54a3e4886862ae4a3d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baha=20Ulu=C4=9F?= <75128919+bahaadesso@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:35:07 +0300 Subject: [PATCH 25/40] Update project.yml deployment target version is changed to 17 --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 912d969..070e23d 100644 --- a/project.yml +++ b/project.yml @@ -8,7 +8,7 @@ configs: options: bundleIdPrefix: com.adesso deploymentTarget: - iOS: 16.0 + iOS: 17.0 attributes: BuildIndependentTargetsInParallel: YES From a56796d1d1d8ddcdf849d48b29167c1e27f50dd4 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Wed, 27 Sep 2023 13:39:25 +0300 Subject: [PATCH 26/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 394c1b9..dd9a93c 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -14,9 +14,6 @@ jobs: name: Build scheme runs-on: macos-13 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest - name: Checkout uses: actions/checkout@v3 - name: Xcodegen From 323ce3faa13a3a1b5e8f3e15b055e5ae17e687e1 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Thu, 19 Oct 2023 20:39:51 +0300 Subject: [PATCH 27/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index dd9a93c..cace95c 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -29,7 +29,7 @@ jobs: run: | xcodebuild -scheme SampleAppSwiftUI clean build -sdk iphoneos -configuration Development CODE_SIGNING_ALLOWED=No -destination 'generic/platform=iOS Simulator' CONFIGURATION_BUILD_DIR=$PWD/build - name: UI Test - uses: mobile-dev-inc/action-maestro-cloud@v1 + uses: mobile-dev-inc/action-maestro-cloud@v1.6.0 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: build/SampleAppSwiftUI.app From 7600f0218fd26859aaab95d31993548b4f4b5577 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Thu, 19 Oct 2023 20:50:45 +0300 Subject: [PATCH 28/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index cace95c..8f67dbd 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -14,6 +14,10 @@ jobs: name: Build scheme runs-on: macos-13 steps: + - name: Select Latest Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable - name: Checkout uses: actions/checkout@v3 - name: Xcodegen From 3df2f881d92c0d8d54974c577b609421ec493468 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Thu, 19 Oct 2023 21:00:01 +0300 Subject: [PATCH 29/40] SASU-0077 SwiftLint Fixes applied --- SampleAppSwiftUI/Network/Base/BaseServiceProtocol.swift | 2 +- .../WebServices/CoinNewsService/CoinNewsServiceEndpoint.swift | 2 +- .../WebServices/ExampleService/ExampleServiceEndpoint.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SampleAppSwiftUI/Network/Base/BaseServiceProtocol.swift b/SampleAppSwiftUI/Network/Base/BaseServiceProtocol.swift index cc2e6e9..f9ff972 100644 --- a/SampleAppSwiftUI/Network/Base/BaseServiceProtocol.swift +++ b/SampleAppSwiftUI/Network/Base/BaseServiceProtocol.swift @@ -35,6 +35,6 @@ extension BaseServiceProtocol { private func prepareAuthenticatedRequest(with requestObject: inout RequestObject) -> RequestObject { // TODO: - handle authenticatedRequest with urlSession - return requestObject + requestObject } } diff --git a/SampleAppSwiftUI/Network/WebServices/CoinNewsService/CoinNewsServiceEndpoint.swift b/SampleAppSwiftUI/Network/WebServices/CoinNewsService/CoinNewsServiceEndpoint.swift index fb8660b..67d6fe2 100644 --- a/SampleAppSwiftUI/Network/WebServices/CoinNewsService/CoinNewsServiceEndpoint.swift +++ b/SampleAppSwiftUI/Network/WebServices/CoinNewsService/CoinNewsServiceEndpoint.swift @@ -19,6 +19,6 @@ enum CoinNewsServiceEndpoint: TargetEndpointProtocol { switch self { case .coinNews(coinCode: let coinCode): return BaseEndpoint.base.path + String(format: Constants.coinNewsEndpoint, coinCode, Configuration.coinApiKey) - } + } } } diff --git a/SampleAppSwiftUI/Network/WebServices/ExampleService/ExampleServiceEndpoint.swift b/SampleAppSwiftUI/Network/WebServices/ExampleService/ExampleServiceEndpoint.swift index 6d5649c..df0041e 100644 --- a/SampleAppSwiftUI/Network/WebServices/ExampleService/ExampleServiceEndpoint.swift +++ b/SampleAppSwiftUI/Network/WebServices/ExampleService/ExampleServiceEndpoint.swift @@ -19,6 +19,6 @@ enum ExampleServiceEndpoint: TargetEndpointProtocol { switch self { case .example(let firstParameter, let secondParameter): return BaseEndpoint.base.path + String(format: Constants.exampleEndpoint, firstParameter, secondParameter) - } + } } } From 692154d44623a2502eb769f4260ce178ecbb6e11 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Thu, 19 Oct 2023 21:06:47 +0300 Subject: [PATCH 30/40] Update ios-build-check.yml --- .github/workflows/ios-build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 8f67dbd..635706c 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -37,4 +37,4 @@ jobs: with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: build/SampleAppSwiftUI.app - ios-version: 16 + ios-version: 16.2 From f21f7213438919fe6a0037f1801e70f360ad181a Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:04:04 +0300 Subject: [PATCH 31/40] SASU-0077 Huge update ocured - iOS 16 backported for UI tests - Observation framework removed - Environment objects fixed - Print statements converted into Logger - App Fully localized - StateObject, ObservedObject & Published values are updated - async/await improved --- .../Application/AppDelegate.swift | 2 +- .../Application/SampleAppSwiftUIApp.swift | 7 +- SampleAppSwiftUI/Managers/LoggerManager.swift | 3 + .../Managers/SwifterManager.swift | 2 +- .../WebSocket/Base/WebSocketStream.swift | 4 +- .../WebSocket/WebSocketSubscription.swift | 9 +- .../Resources/Localizable.xcstrings | 330 +++++++++++++++++- .../Scenes/Favorites/FavoritesView.swift | 13 +- .../Scenes/Favorites/FavoritesViewModel.swift | 16 +- .../Scenes/Home/Coin/CoinListView.swift | 5 +- .../Home/Coin/CoinNews/CoinNewsListView.swift | 11 +- .../Home/Coin/Detail/CoinDetailView.swift | 35 +- .../Coin/Detail/CoinDetailViewModel.swift | 130 ++++--- .../Detail/CoinPriceHistoryChartView.swift | 4 +- .../CoinPriceHistoryChartViewModel.swift | 19 +- SampleAppSwiftUI/Scenes/Home/FilterView.swift | 10 +- SampleAppSwiftUI/Scenes/Home/HomeView.swift | 11 +- .../Scenes/Home/HomeViewModel.swift | 18 +- .../Scenes/Home/SortOptions.swift | 2 +- SampleAppSwiftUI/Scenes/Main/MainView.swift | 13 +- SampleAppSwiftUI/Scenes/Router.swift | 14 +- .../Scenes/Settings/SettingsView.swift | 5 +- .../Scenes/ViewModelProtocol.swift | 1 + SampleAppSwiftUI/Scenes/WebView/WebView.swift | 32 +- .../Utility/JsonHelper/JsonHelper.swift | 14 +- 25 files changed, 528 insertions(+), 182 deletions(-) diff --git a/SampleAppSwiftUI/Application/AppDelegate.swift b/SampleAppSwiftUI/Application/AppDelegate.swift index 31d4d97..9ef5a0a 100644 --- a/SampleAppSwiftUI/Application/AppDelegate.swift +++ b/SampleAppSwiftUI/Application/AppDelegate.swift @@ -15,6 +15,6 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { // Handle remote notification failures here - print(error.localizedDescription) + LoggerManager().setError(errorMessage: error.localizedDescription) } } diff --git a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift index 7e436e2..30d382f 100644 --- a/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift +++ b/SampleAppSwiftUI/Application/SampleAppSwiftUIApp.swift @@ -14,7 +14,7 @@ struct SampleAppSwiftUIApp: App { // Check out https://developer.apple.com/documentation/swiftui/scenephase for more information @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate private var loggingService: LoggingService - @State private var router: Router = Router() + @StateObject private var router = Router() init() { UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance.init(idiom: .unspecified) @@ -23,8 +23,9 @@ struct SampleAppSwiftUIApp: App { var body: some Scene { WindowGroup { - MainView(router: $router) - .onChange(of: phase, { _, newValue in + MainView() + .environmentObject(router) + .onChange(of: phase, perform: { newValue in manageChanges(for: newValue) }) .onOpenURL(perform: onOpenURL(_:)) diff --git a/SampleAppSwiftUI/Managers/LoggerManager.swift b/SampleAppSwiftUI/Managers/LoggerManager.swift index b33f574..75d6964 100644 --- a/SampleAppSwiftUI/Managers/LoggerManager.swift +++ b/SampleAppSwiftUI/Managers/LoggerManager.swift @@ -7,6 +7,8 @@ // import CocoaLumberjack +import CocoaLumberjackSwiftLogBackend +import Logging class LoggerManager { @@ -32,6 +34,7 @@ class LoggerManager { logger.setLogLevel(level) DDLog.add(ddosLogger) + LoggingSystem.bootstrapWithCocoaLumberjack() let fileManager = DDLogFileManagerDefault(logsDirectory: documentsDirectory) fileLogger = DDFileLogger(logFileManager: fileManager) diff --git a/SampleAppSwiftUI/Managers/SwifterManager.swift b/SampleAppSwiftUI/Managers/SwifterManager.swift index 0e4b24e..c548f21 100644 --- a/SampleAppSwiftUI/Managers/SwifterManager.swift +++ b/SampleAppSwiftUI/Managers/SwifterManager.swift @@ -40,7 +40,7 @@ class SwifterManager { do { try swifterServer.start(Constants.port) } catch let error { - print(error) + LoggerManager().setError(errorMessage: error.localizedDescription) } } #endif diff --git a/SampleAppSwiftUI/Network/WebSocket/Base/WebSocketStream.swift b/SampleAppSwiftUI/Network/WebSocket/Base/WebSocketStream.swift index b790420..7f2d644 100644 --- a/SampleAppSwiftUI/Network/WebSocket/Base/WebSocketStream.swift +++ b/SampleAppSwiftUI/Network/WebSocket/Base/WebSocketStream.swift @@ -106,10 +106,10 @@ class WebSocketStream: NSObject, AsyncSequence { guard let self else { return } self.task.sendPing { error in if let error { - debugPrint("Pong Error: ", error) + LoggerManager().setError(errorMessage: "Pong Error: \(error.localizedDescription)") timer.invalidate() } else { - debugPrint("Connection is alive") + Logger().log(level: .info, message: "Connection is alive") } } } diff --git a/SampleAppSwiftUI/Network/WebSocket/WebSocketSubscription.swift b/SampleAppSwiftUI/Network/WebSocket/WebSocketSubscription.swift index becc953..f802b97 100644 --- a/SampleAppSwiftUI/Network/WebSocket/WebSocketSubscription.swift +++ b/SampleAppSwiftUI/Network/WebSocket/WebSocketSubscription.swift @@ -35,14 +35,15 @@ class WebSocketSubscription: Subscription where S.Input == URLSes private func listenSocket() async { guard let subscriber = subscriber else { - debugPrint("no subscriber") - return } + LoggerManager().setError(errorMessage: "no subscriber") + return + } do { for try await message in socket { _ = subscriber.receive(message) } - } catch { - debugPrint("Someting went wrong") + } catch let error { + LoggerManager().setError(errorMessage: "Something went wrong \(error.localizedDescription)") } } } diff --git a/SampleAppSwiftUI/Resources/Localizable.xcstrings b/SampleAppSwiftUI/Resources/Localizable.xcstrings index f27f936..2449566 100644 --- a/SampleAppSwiftUI/Resources/Localizable.xcstrings +++ b/SampleAppSwiftUI/Resources/Localizable.xcstrings @@ -1,13 +1,41 @@ { "sourceLanguage" : "en", "strings" : { + "Average" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durchschnitt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortalama" + } + } + } + }, "baseCoinChangeInfo" : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn Sie eine neue Basiswährung auswählen, werden alle Preise in der App in dieser Währung angezeigt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "When you select a new base currency, all prices in the app will be displayed in that currency." } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni bir temel para birimi seçtiğinizde uygulamadaki tüm fiyatlar bu para biriminde görüntülenecektir." + } } } }, @@ -35,7 +63,69 @@ } }, "Currency" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Währung" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para Birimi" + } + } + } + }, + "Dark Mode:" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dark Mode:" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karanlık Mod:" + } + } + } + }, + "Date" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datum" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarih" + } + } + } + }, + "Default" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Varsayılan" + } + } + } }, "Favorites" : { "extractionState" : "manual", @@ -61,7 +151,20 @@ } }, "Got it!" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstanden!" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anladım!" + } + } + } }, "Hello, World!" : { "comment" : "Localizable.strings\n SampleAppSwiftUI\n\n Created by Selim Gungorer on 14.09.2022.\n Copyright © 2022 Adesso Turkey. All rights reserved.", @@ -87,6 +190,72 @@ } } }, + "Home" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heim" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ana Sayfa" + } + } + } + }, + "Low" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niedrig" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük" + } + } + } + }, + "Name (A-Z)" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name (A-Z)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ad [A-Z]" + } + } + } + }, + "Name (Z-A)" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name (Z-A)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ad [Z-A]" + } + } + } + }, "News" : { "extractionState" : "manual", "localizations" : { @@ -111,19 +280,166 @@ } }, "No Coins found." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Münzen gefunden" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiç Coin bulunamadı." + } + } + } }, "No price data found" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Preisdaten gefunden" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fiyat verisi bulunamadı" + } + } + } + }, + "Parities" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paritäten" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pariteler" + } + } + } + }, + "Price (High-Low)" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preis (Hoch-Niedrig)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fiyat (Düşük-Yüksek)" + } + } + } + }, + "Price (Low-High)" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preis (Niedrig-Hoch)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fiyat (Yüksek-Düşük)" + } + } + } }, "Remove All Data" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Daten entfernen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Veriyi Sil" + } + } + } + }, + "Search for a name or symbol" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suchen Sie nach einem Namen oder Symbol" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir isim veya sembol ara" + } + } + } + }, + "Selected date" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgewähltes Datum" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçili Tarih" + } + } + } }, "Settings" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarlar" + } + } + } }, "View More" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr sehen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha Fazla Göster" + } + } + } } }, "version" : "1.0" diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift index a32be57..1d55f1f 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -9,8 +9,8 @@ import SwiftUI struct FavoritesView: View { @State private var searchTerm = "" - @State private var viewModel = FavoritesViewModel() - @Binding var router: Router + @StateObject private var viewModel = FavoritesViewModel() + @EnvironmentObject var router: Router var body: some View { NavigationStack(path: $router.favoritesNavigationPath) { @@ -34,14 +34,14 @@ struct FavoritesView: View { .background(Color(.lightestGray)) .onAppear(perform: viewModel.fetchFavorites) .onDisappear(perform: viewModel.disconnect) - .onChange(of: searchTerm, { _, newValue in + .onChange(of: searchTerm, perform: { newValue in viewModel.filterResults(searchTerm: newValue) viewModel.sortOptions(sort: viewModel.selectedSortOption) }) - .onChange(of: StorageManager.shared.favoriteCoins, { _, newValue in + .onChange(of: StorageManager.shared.favoriteCoins, perform: { newValue in fetchFavorites(codes: newValue) }) - .onChange(of: viewModel.selectedSortOption, { _, new in + .onChange(of: viewModel.selectedSortOption, perform: { new in viewModel.sortOptions(sort: new) }) } @@ -59,5 +59,6 @@ struct FavoritesView: View { } #Preview { - FavoritesView(router: .constant(.init())) + FavoritesView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift index 8caa8c7..d525f9b 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift @@ -8,10 +8,8 @@ import Foundation import SwiftUI import Combine -import Observation -@Observable -class FavoritesViewModel { +class FavoritesViewModel: ObservableObject { private let checkWebSocket = true private var webSocketService: any WebSocketServiceProtocol @@ -19,13 +17,13 @@ class FavoritesViewModel { private var reconnectionCount: Int = 0 private var maxReconnectionCount: Int = 3 - var coinList: [CoinData] = [] - var filteredCoins: [CoinData] = [] - var coinInfo: CoinData? - var filterTitle = SortOptions.defaultList.rawValue - var selectedSortOption: SortOptions = .defaultList + @Published var coinList: [CoinData] = [] + @Published var filteredCoins: [CoinData] = [] + @Published var coinInfo: CoinData? + @Published var filterTitle = SortOptions.defaultList.rawValue + @Published var selectedSortOption: SortOptions = .defaultList - var isLoading: Bool = false + @Published var isLoading: Bool = false init(webSocketService: any WebSocketServiceProtocol = WebSocketService.shared) { self.webSocketService = webSocketService diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift index 2813f4a..caadf6f 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift @@ -8,11 +8,11 @@ import SwiftUI struct CoinListView: View { - @State var viewModel: ViewModel + @ObservedObject var viewModel: ViewModel @Binding var filteredCoins: [CoinData] @State private var showingAlert = false @State private var alertTitle = "" - @State private var router: Router = Router() + @EnvironmentObject var router: Router let favoriteChanged: () -> Void var body: some View { @@ -77,5 +77,6 @@ struct CoinListView: View { #Preview { NavigationView { CoinListView(viewModel: FavoritesViewModel(), filteredCoins: .constant([.demo, .demo, .demo]), favoriteChanged: {}) + .environmentObject(Router()) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift index 1d01bb1..fab614a 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift @@ -8,11 +8,8 @@ import SwiftUI struct CoinNewsListView: View { - @State private var viewModel: CoinDetailViewModel - - init(coinData: CoinData) { - _viewModel = State(wrappedValue: CoinDetailViewModel(coinData: coinData)) - } + @StateObject private var viewModel = CoinDetailViewModel() + var coinData: CoinData var body: some View { VStack { @@ -47,8 +44,8 @@ struct CoinNewsListView: View { .navigationViewStyle(.automatic) } } - .onAppear { - viewModel.onAppear() + .task { + await viewModel.onAppear(coinData: coinData) } } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index 29d2ab8..866dfb1 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -15,15 +15,13 @@ struct CoinDetailView: View { static let chartHeight: CGFloat = 250 } - @State private var viewModel: CoinDetailViewModel + var coinData: CoinData - init(coinData: CoinData) { - viewModel = CoinDetailViewModel(coinData: coinData) - } + @StateObject private var viewModel = CoinDetailViewModel() var coinDetailImage: some View { VStack { - AsyncImage(url: viewModel.getIconURL()) { phase in + AsyncImage(url: viewModel.getIconURL(coinData: coinData)) { phase in if let image = phase.image { image.resizable() } else if phase.error != nil { @@ -37,11 +35,14 @@ struct CoinDetailView: View { } } .scaledToFit() - .imageFrame(width: Dimensions.imageWidth * 2, height: Dimensions.imageHeight * 2) + .imageFrame( + width: Dimensions.imageWidth * 2, + height: Dimensions.imageHeight * 2 + ) - Text(verbatim: viewModel.getPriceString()) + Text(verbatim: viewModel.getPriceString(coinData: coinData)) - ChangePercentageView(changeRate: viewModel.coinData.detail?.usd ?? .init()) + ChangePercentageView(changeRate: coinData.detail?.usd ?? .init()) ZStack { CoinChartHistoryRangeButtons(selection: $viewModel.chartHistoryRangeSelection) @@ -80,6 +81,14 @@ struct CoinDetailView: View { .cornerRadius(Dimensions.CornerRadius.default) } .padding(.horizontal, Paddings.side) + .onReceive(viewModel.$chartHistoryRangeSelection) { selectedRange in + Task { + await viewModel.fetchCoinPriceHistory( + coinData: coinData, + forSelectedRange: selectedRange + ) + } + } } var news: some View { @@ -123,7 +132,7 @@ struct CoinDetailView: View { news Button { } label: { - NavigationLink(destination: CoinNewsListView(coinData: viewModel.coinData)) { + NavigationLink(destination: CoinNewsListView(coinData: coinData)) { Text("View More") .frame(width: UIScreen.main.bounds.size.width - CoinDetailView.coinListFrameSize) .font(.system(size: 18)) @@ -134,12 +143,12 @@ struct CoinDetailView: View { .cornerRadius(CoinDetailView.viewMoreButton) Spacer() } - .navigationTitle(Text(verbatim: viewModel.coinData.coinInfo?.title ?? "")) + .navigationTitle(Text(verbatim: coinData.coinInfo?.title ?? "")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - viewModel.updateCoinFavoriteState() + viewModel.updateCoinFavoriteState(coinData: coinData) } label: { viewModel.isFavorite ? Image(systemName: Images.favorites) @@ -148,8 +157,8 @@ struct CoinDetailView: View { .tint(.gray) } } - .onAppear { - viewModel.onAppear() + .task { + await viewModel.onAppear(coinData: coinData) } } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift index a66a07d..777d8ad 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift @@ -6,44 +6,33 @@ // import Foundation -import Observation +import Combine -@Observable -class CoinDetailViewModel { - let coinData: CoinData - private(set) var isFavorite: Bool = false - var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth { - didSet { - fetchCoinPriceHistory(forSelectedRange: chartHistoryRangeSelection) - } - } +class CoinDetailViewModel: ObservableObject { + @Published var isFavorite: Bool = false + @Published var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? - private(set) var isLoading: Bool = false - var coinNewsDataModel: [CoinNewData]? - var priceChartSelectedXDateText: String = "" + @Published var isLoading = false + @Published var coinNewsDataModel: [CoinNewData]? + @Published var priceChartSelectedXDateText = "" var rangeButtonsOpacity: Double { priceChartSelectedXDateText.isEmpty ? 1.0 : 0.0 } - private let coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol - private let coinNewsUseCase: CoinNewsUseCaseProtocol - - init(coinData: CoinData, - coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol = CoinPriceHistoryUseCase(), - coinNewsUseCase: CoinNewsUseCaseProtocol = CoinNewsUseCase()) { - self.coinData = coinData - self.coinPriceHistoryUseCase = coinPriceHistoryUseCase - self.coinNewsUseCase = coinNewsUseCase - } + private let coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol = CoinPriceHistoryUseCase() + private let coinNewsUseCase: CoinNewsUseCaseProtocol = CoinNewsUseCase() - func onAppear() { - checkIsCoinFavorite() - fetchCoinPriceHistory(forSelectedRange: chartHistoryRangeSelection) - fetchCoinNews() + func onAppear(coinData: CoinData) async { + await checkIsCoinFavorite(coinData: coinData) + await fetchCoinPriceHistory( + coinData: coinData, + forSelectedRange: chartHistoryRangeSelection + ) + await fetchCoinNews(coinData: coinData) } - func getIconURL() -> URL? { + func getIconURL(coinData: CoinData) -> URL? { guard let coinCode = coinData.coinInfo?.code else { return nil } @@ -51,65 +40,72 @@ class CoinDetailViewModel { return URLs.Icons.getURL(from: coinCode) } - func getPriceString() -> String { + func getPriceString(coinData: CoinData) -> String { coinData.detail?.usd?.createPriceString() ?? "" } - func checkIsCoinFavorite() { - isFavorite = StorageManager.shared.isCoinFavorite(coinData.coinInfo?.code ?? "") + func checkIsCoinFavorite(coinData: CoinData) async { + await MainActor.run { + isFavorite = StorageManager.shared.isCoinFavorite(coinData.coinInfo?.code ?? "") + } } - func updateCoinFavoriteState() { + func updateCoinFavoriteState(coinData: CoinData) { isFavorite.toggle() StorageManager.shared.manageFavorites(coinData: coinData) } - func fetchCoinPriceHistory(forSelectedRange range: CoinChartHistoryRange) { + func fetchCoinPriceHistory(coinData: CoinData, forSelectedRange range: CoinChartHistoryRange) async { guard let coinCode = coinData.coinInfo?.code else { return } - isLoading = true - - Task { + await MainActor.run { + isLoading = true + } + do { var response: CoinPriceHistoryResponse? - let limitAndAggregate = range.limitAndAggregateValue - - if range == .oneDay { - response = try? await coinPriceHistoryUseCase.getHourlyPriceHistory( - coinCode: coinCode, - unitToBeConverted: "USD", - hourLimit: limitAndAggregate.limit, - aggregate: limitAndAggregate.aggregate - ) - } else { - response = try? await coinPriceHistoryUseCase.getDailyPriceHistory( - coinCode: coinCode, - unitToBeConverted: "USD", - dayLimit: limitAndAggregate.limit, - aggregate: limitAndAggregate.aggregate - ) + switch range { + case .oneDay: + response = try await coinPriceHistoryUseCase.getHourlyPriceHistory( + coinCode: coinCode, + unitToBeConverted: "USD", + hourLimit: limitAndAggregate.limit, + aggregate: limitAndAggregate.aggregate + ) + default: + response = try await coinPriceHistoryUseCase.getDailyPriceHistory( + coinCode: coinCode, + unitToBeConverted: "USD", + dayLimit: limitAndAggregate.limit, + aggregate: limitAndAggregate.aggregate + ) } - - guard let response = response, let priceHistoryData = response.data else { return } - - DispatchQueue.main.async { - self.isLoading = false - self.coinPriceHistoryChartDataModel = CoinPriceHistoryChartDataModel(from: priceHistoryData) + if let response = response, let priceHistoryData = response.data { + await MainActor.run { + isLoading = false + coinPriceHistoryChartDataModel = CoinPriceHistoryChartDataModel(from: priceHistoryData) + } + } else { + await MainActor.run { + isLoading = false + } } + } catch let error { + LoggerManager().setError(errorMessage: error.localizedDescription) } } - func fetchCoinNews() { + func fetchCoinNews(coinData: CoinData) async { guard let coinCode = coinData.coinInfo?.code else { return } - - Task { - var response: CoinNewsResponse? - response = try? await coinNewsUseCase.getCoinNews(coinCode: coinCode) - - guard let response = response, let coinNewData = response.data else { return } - DispatchQueue.main.async { - self.coinNewsDataModel = coinNewData + do { + let response = try await coinNewsUseCase.getCoinNews(coinCode: coinCode) + if let data = response.data { + await MainActor.run { + coinNewsDataModel = data + } } + } catch let error { + LoggerManager().setError(errorMessage: error.localizedDescription) } } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift index 955f8fc..546d764 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift @@ -9,10 +9,10 @@ import SwiftUI import Charts struct CoinPriceHistoryChartView: View { - @State private var viewModel: CoinPriceHistoryChartViewModel + @StateObject private var viewModel: CoinPriceHistoryChartViewModel init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: String) { - _viewModel = State( + _viewModel = StateObject( wrappedValue: CoinPriceHistoryChartViewModel( selectedRange: selectedRange, dataModel: dataModel, diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift index f318939..44f171b 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift @@ -8,20 +8,18 @@ import Foundation import SwiftUI import Charts -import Observation -@Observable -class CoinPriceHistoryChartViewModel { - var selectedRange: CoinChartHistoryRange - var dataModel: CoinPriceHistoryChartDataModel - var selectedXDateText: String +class CoinPriceHistoryChartViewModel: ObservableObject { + @Published var selectedRange: CoinChartHistoryRange + @Published var dataModel: CoinPriceHistoryChartDataModel + @Published var selectedXDateText: String /// Holds the selected x value of chart when user is dragging on it - var selectedX: (any Plottable)? + @Published var selectedX: (any Plottable)? init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: String) { self.selectedRange = selectedRange self.dataModel = dataModel - self._selectedXDateText = selectedXDateText + self.selectedXDateText = selectedXDateText self.selectedX = nil } @@ -70,9 +68,7 @@ class CoinPriceHistoryChartViewModel { } func onChangeDrag(value: DragGesture.Value, chartProxy: ChartProxy, geometryProxy: GeometryProxy) { - if let plotFrame = chartProxy.plotFrame { - let xCurrent = value.location.x - geometryProxy[plotFrame].origin.x - + let xCurrent = value.location.x - geometryProxy[chartProxy.plotAreaFrame].origin.x if let selectedDate: Date = chartProxy.value(atX: xCurrent), let startDate = dataModel.prices.first?.date, let lastDate = dataModel.prices.last?.date, @@ -81,7 +77,6 @@ class CoinPriceHistoryChartViewModel { selectedXDateText = calculatedSelectedXDateText } } - } func onEndDrag() { selectedX = nil diff --git a/SampleAppSwiftUI/Scenes/Home/FilterView.swift b/SampleAppSwiftUI/Scenes/Home/FilterView.swift index be36c64..cd55264 100644 --- a/SampleAppSwiftUI/Scenes/Home/FilterView.swift +++ b/SampleAppSwiftUI/Scenes/Home/FilterView.swift @@ -10,23 +10,29 @@ import SwiftUI struct FilterView: View { var viewModel: ViewModel + @State private var sortName = "" + var body: some View { HStack { - Text(viewModel.selectedSortOption.rawValue) + Text(sortName) Spacer() Menu { ForEach(SortOptions.allCases, id: \.rawValue) { sortOption in Button { viewModel.selectedSortOption = sortOption + sortName = sortOption.rawValue.localized } label: { - Text(sortOption.rawValue) + Text(sortOption.rawValue.localized) } } } label: { Image(systemName: Images.filter) } } + .onAppear { + sortName = viewModel.selectedSortOption.rawValue.localized + } } } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeView.swift b/SampleAppSwiftUI/Scenes/Home/HomeView.swift index 0f155b8..4829f47 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -10,9 +10,9 @@ import SwiftUI struct HomeView: View { - @State private var viewModel = HomeViewModel() + @StateObject private var viewModel = HomeViewModel() @State private var searchTerm = "" - @Binding var router: Router + @EnvironmentObject var router: Router var body: some View { NavigationStack(path: $router.homeNavigationPath) { @@ -42,16 +42,17 @@ struct HomeView: View { await viewModel.fillModels() } } - .onChange(of: searchTerm, { _, newValue in + .onChange(of: searchTerm, perform: { newValue in viewModel.filterResults(searchTerm: newValue) viewModel.sortOptions(sort: viewModel.selectedSortOption) }) - .onChange(of: viewModel.selectedSortOption, { _, newValue in + .onChange(of: viewModel.selectedSortOption, perform: { newValue in viewModel.sortOptions(sort: newValue) }) } } #Preview { - HomeView(router: .constant(.init())) + HomeView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift index 4ffd177..e8bbdb6 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift @@ -8,18 +8,16 @@ import Foundation import SwiftUI import Combine -import Observation -@Observable -class HomeViewModel { - var coinInfo: ExcangeRatesResponseModel? - var coinList: [CoinData] = [] - var filteredCoins: [CoinData] = [] - var filterTitle = SortOptions.defaultList.rawValue +class HomeViewModel: ObservableObject { + @Published var coinInfo: ExcangeRatesResponseModel? + @Published var coinList: [CoinData] = [] + @Published var filteredCoins: [CoinData] = [] + @Published var filterTitle = SortOptions.defaultList.rawValue let listPageLimit = 10 - var isLoading: Bool = false - var selectedSortOption: SortOptions = .defaultList + @Published var isLoading: Bool = false + @Published var selectedSortOption: SortOptions = .defaultList func fillModels(demo: Bool = false) async { if demo { @@ -30,7 +28,7 @@ class HomeViewModel { private func fetchAllCoins(page: Int = 1) async { guard let dataSource = try? await AllCoinRemoteDataSource().getAllCoin(limit: self.listPageLimit, unitToBeConverted: "USD", page: page) else { - print("Problem on the convert") + Logger().error("Problem on the convert") return } DispatchQueue.main.async { diff --git a/SampleAppSwiftUI/Scenes/Home/SortOptions.swift b/SampleAppSwiftUI/Scenes/Home/SortOptions.swift index b20a8ae..63f1e84 100644 --- a/SampleAppSwiftUI/Scenes/Home/SortOptions.swift +++ b/SampleAppSwiftUI/Scenes/Home/SortOptions.swift @@ -9,7 +9,7 @@ import Foundation public enum SortOptions: String, CaseIterable { case defaultList = "Default" - case price = "Price (Low- High)" + case price = "Price (Low-High)" case priceReversed = "Price (High-Low)" case name = "Name (A-Z)" case nameReversed = "Name (Z-A)" diff --git a/SampleAppSwiftUI/Scenes/Main/MainView.swift b/SampleAppSwiftUI/Scenes/Main/MainView.swift index 4a5f024..18c4efd 100644 --- a/SampleAppSwiftUI/Scenes/Main/MainView.swift +++ b/SampleAppSwiftUI/Scenes/Main/MainView.swift @@ -10,24 +10,24 @@ import SwiftUI struct MainView: View { - @State private var storageManager = StorageManager.shared - @Binding var router: Router + private var storageManager = StorageManager.shared + @EnvironmentObject var router: Router var body: some View { TabView(selection: $router.selectedTab) { - HomeView(router: $router) + HomeView() .tag(TabIndex.home) .tabItem { Image(systemName: TabIndex.home.imageName()) .accessibilityIdentifier("homeTabView") } - FavoritesView(router: $router) + FavoritesView() .tag(TabIndex.favorites) .tabItem { Image(systemName: TabIndex.favorites.imageName()) .accessibilityIdentifier("favoriteTabView") } - SettingsView(router: $router) + SettingsView() .tag(TabIndex.settings) .tabItem { Image(systemName: TabIndex.settings.imageName()) @@ -40,5 +40,6 @@ struct MainView: View { } #Preview { - MainView(router: .constant(Router())) + MainView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Router.swift b/SampleAppSwiftUI/Scenes/Router.swift index f649828..d20d980 100644 --- a/SampleAppSwiftUI/Scenes/Router.swift +++ b/SampleAppSwiftUI/Scenes/Router.swift @@ -7,15 +7,13 @@ import Foundation import SwiftUI -import Observation -@Observable -final public class Router { - var homeNavigationPath: [Screen] = [] - var favoritesNavigationPath: [Screen] = [] - var settingsNavigationPath: [Screen] = [] - var selectedTab: TabIndex = .home - @ObservationIgnored var tabbarNames: [TabIndex] = [.home, .favorites, .settings] +final public class Router: ObservableObject { + @Published var homeNavigationPath: [Screen] = [] + @Published var favoritesNavigationPath: [Screen] = [] + @Published var settingsNavigationPath: [Screen] = [] + @Published var selectedTab: TabIndex = .home + var tabbarNames: [TabIndex] = [.home, .favorites, .settings] func navigateCoinDetail(coinData: CoinData) { if selectedTab == .home { diff --git a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift index 8a3ab19..0610828 100644 --- a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift +++ b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift @@ -11,7 +11,7 @@ struct SettingsView: View { @State private var isDarkModeOn = false @State private var selectedParity: Parity = .USD - @Binding var router: Router + @EnvironmentObject var router: Router var body: some View { NavigationStack(path: $router.settingsNavigationPath) { @@ -85,5 +85,6 @@ extension SettingsView { } #Preview { - SettingsView(router: .constant(.init())) + SettingsView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift index cd73c28..2e2a090 100644 --- a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift +++ b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift @@ -20,6 +20,7 @@ extension ViewModelProtocol { func checkLastItem(_ item: CoinData) {} func sortOptions(sort: SortOptions) { + selectedSortOption = sort switch sort { case .defaultList: filteredCoins = filteredCoins.count < coinList.count ? filteredCoins : coinList diff --git a/SampleAppSwiftUI/Scenes/WebView/WebView.swift b/SampleAppSwiftUI/Scenes/WebView/WebView.swift index f98ffb8..55210b5 100644 --- a/SampleAppSwiftUI/Scenes/WebView/WebView.swift +++ b/SampleAppSwiftUI/Scenes/WebView/WebView.swift @@ -10,14 +10,30 @@ import SwiftUI struct WebView: UIViewRepresentable { let url: URL? - var activityIndicator: UIActivityIndicatorView! = UIActivityIndicatorView(frame: CGRect(x: (UIScreen.main.bounds.width/Dimensions.WebView.two)-Dimensions.WebView.thirty, - y: (UIScreen.main.bounds.height/Dimensions.WebView.two)-Dimensions.WebView.thirty, - width: Dimensions.WebView.sixty, - height: Dimensions.WebView.sixty)) + var activityIndicator = UIActivityIndicatorView( + frame: CGRect(x: (UIScreen.main.bounds.width/Dimensions.WebView.two)-Dimensions.WebView.thirty, + y: (UIScreen.main.bounds.height/Dimensions.WebView.two)-Dimensions.WebView.thirty, + width: Dimensions.WebView.sixty, + height: Dimensions.WebView.sixty) + ) func makeUIView(context: Context) -> UIView { - let view = UIView(frame: CGRect(x: Dimensions.WebView.zero, y: Dimensions.WebView.zero, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) - let webview = WKWebView(frame: CGRect(x: Dimensions.WebView.zero, y: Dimensions.WebView.zero, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) + let view = UIView( + frame: CGRect( + x: Dimensions.WebView.zero, + y: Dimensions.WebView.zero, + width: UIScreen.main.bounds.width, + height: UIScreen.main.bounds.height + ) + ) + let webview = WKWebView( + frame: CGRect( + x: Dimensions.WebView.zero, + y: Dimensions.WebView.zero, + width: UIScreen.main.bounds.width, + height: UIScreen.main.bounds.height + ) + ) webview.navigationDelegate = context.coordinator if let url = self.url { @@ -56,11 +72,11 @@ class WebViewHelper: NSObject, WKNavigationDelegate { func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { parent.activityIndicator.isHidden = true - print("error: \(error)") + Logger().error(error.localizedDescription) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { parent.activityIndicator.isHidden = true - print("error \(error)") + Logger().error(error.localizedDescription) } } diff --git a/SampleAppSwiftUI/Utility/JsonHelper/JsonHelper.swift b/SampleAppSwiftUI/Utility/JsonHelper/JsonHelper.swift index 6c8f53e..0d8d58d 100644 --- a/SampleAppSwiftUI/Utility/JsonHelper/JsonHelper.swift +++ b/SampleAppSwiftUI/Utility/JsonHelper/JsonHelper.swift @@ -131,8 +131,14 @@ final class JsonHelper { /// /// - Returns: Saves the given `json` to given `withName`. static func save(json: String?, withName: String) { - guard let json = json else { return debugPrint("error json is nil") } - guard let filePath = filePath(withName) else { return debugPrint("error getting path with filePath") } + guard let json = json else { + Logger().error("error json is nil") + return + } + guard let filePath = filePath(withName) else { + Logger().error("error getting path with filePath") + return + } if !FileManager.default.fileExists(atPath: filePath) { FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil) @@ -140,8 +146,8 @@ final class JsonHelper { do { try json.write(toFile: filePath, atomically: true, encoding: .utf8) - } catch { - debugPrint("error writing to file to path: \(filePath)") + } catch let error { + Logger().error("error writing to file to path: \(filePath) error: \(error.localizedDescription)") } } } From 7eaa94bed78037f47146e5610fb739ba9cd331b0 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:21:03 +0300 Subject: [PATCH 32/40] SASU-0077 Added new build settings - Added settings to enable asset generation in compile time - Added settings to enable string catalog in compile time --- project.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 070e23d..5979dbf 100644 --- a/project.yml +++ b/project.yml @@ -16,7 +16,10 @@ attributes: settings: ENABLE_USER_SCRIPT_SANDBOXING: false CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true - ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: YES + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: true + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: true + LOCALIZATION_PREFERS_STRING_CATALOGS: true + SWIFT_EMIT_LOC_STRINGS: true packages: CocoaLumberjack: From c6908a02fa7c244788e4a3b2607656977a718f21 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:28:12 +0300 Subject: [PATCH 33/40] SASU-0077 Added missing import --- project.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project.yml b/project.yml index 5979dbf..5d1f234 100644 --- a/project.yml +++ b/project.yml @@ -38,6 +38,7 @@ targets: - package: CocoaLumberjack product: CocoaLumberjack product: CocoaLumberjackSwift + product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse product: PulseUI @@ -63,6 +64,7 @@ targets: - package: CocoaLumberjack product: CocoaLumberjack product: CocoaLumberjackSwift + product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse product: PulseUI From b944b415e6445b06d02e226ab912d7259007fa11 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:31:57 +0300 Subject: [PATCH 34/40] SASU-0077 Downed the version to 16 for now --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 5d1f234..e809e1e 100644 --- a/project.yml +++ b/project.yml @@ -8,7 +8,7 @@ configs: options: bundleIdPrefix: com.adesso deploymentTarget: - iOS: 17.0 + iOS: 16.0 attributes: BuildIndependentTargetsInParallel: YES From e39c620e4185ef0997c8cd8b5de86ebac1049a20 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:50:10 +0300 Subject: [PATCH 35/40] SASU-0077 Fixed the xcodegen package issue --- project.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project.yml b/project.yml index e809e1e..9c326de 100644 --- a/project.yml +++ b/project.yml @@ -37,7 +37,9 @@ targets: dependencies: - package: CocoaLumberjack product: CocoaLumberjack + - package: CocoaLumberjack product: CocoaLumberjackSwift + - package: CocoaLumberjack product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse @@ -63,7 +65,9 @@ targets: - target: SampleAppSwiftUI - package: CocoaLumberjack product: CocoaLumberjack + - package: CocoaLumberjack product: CocoaLumberjackSwift + - package: CocoaLumberjack product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse From 382e36631d1120345be36ea2eac9f80b6df598f0 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 00:56:25 +0300 Subject: [PATCH 36/40] SASU-0077 Maestro iOS version --- .github/workflows/ios-build-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index 635706c..8f67dbd 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -37,4 +37,4 @@ jobs: with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: build/SampleAppSwiftUI.app - ios-version: 16.2 + ios-version: 16 From c5e8b55d28004666b68204809db28a4d37c21338 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 10:52:18 +0300 Subject: [PATCH 37/40] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d3048d..8f7aac7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves ### Spesifications -- Minimum target of iOS 17 +- Minimum target of iOS 16 - Usage of Swift concurrency (async/await) - WebSocket implementation for real-time price change. - Up-to-date SWiftUI features (Navigation stack etc.) @@ -39,7 +39,7 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves Because XcodeGen is used in this project, there will be no `.xcodeproj` or `.xcworkspace` files when it first cloned. To generate them using the `project.yml` file, run ```sh -xcodegen generate +xcodegen ``` Swiftlint can also be installed via included scripts in the repository. Under the `{project_root}/scripts/installation` directory, simply run either or both of: From 578c9fc6c1b013ce983818ada6c2d81b549a05bf Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 14:16:06 +0300 Subject: [PATCH 38/40] SASU-0077 Changed the StateObject Init --- .../Home/Coin/CoinNews/CoinNewsListView.swift | 13 +++++---- .../Home/Coin/Detail/CoinDetailView.swift | 27 ++++++++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift index fab614a..d83a459 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift @@ -8,8 +8,13 @@ import SwiftUI struct CoinNewsListView: View { - @StateObject private var viewModel = CoinDetailViewModel() - var coinData: CoinData + @StateObject private var viewModel: CoinDetailViewModel + + init(coinData: CoinData) { + _viewModel = StateObject( + wrappedValue: CoinDetailViewModel(coinData: coinData) + ) + } var body: some View { VStack { @@ -44,9 +49,7 @@ struct CoinNewsListView: View { .navigationViewStyle(.automatic) } } - .task { - await viewModel.onAppear(coinData: coinData) - } + .task(viewModel.onAppear) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift index 866dfb1..c8edfed 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -15,13 +15,19 @@ struct CoinDetailView: View { static let chartHeight: CGFloat = 250 } - var coinData: CoinData + @StateObject private var viewModel: CoinDetailViewModel - @StateObject private var viewModel = CoinDetailViewModel() + init(coinData: CoinData) { + _viewModel = StateObject( + wrappedValue: CoinDetailViewModel( + coinData: coinData + ) + ) + } var coinDetailImage: some View { VStack { - AsyncImage(url: viewModel.getIconURL(coinData: coinData)) { phase in + AsyncImage(url: viewModel.getIconURL()) { phase in if let image = phase.image { image.resizable() } else if phase.error != nil { @@ -40,9 +46,9 @@ struct CoinDetailView: View { height: Dimensions.imageHeight * 2 ) - Text(verbatim: viewModel.getPriceString(coinData: coinData)) + Text(verbatim: viewModel.getPriceString()) - ChangePercentageView(changeRate: coinData.detail?.usd ?? .init()) + ChangePercentageView(changeRate: viewModel.coinData.detail?.usd ?? .init()) ZStack { CoinChartHistoryRangeButtons(selection: $viewModel.chartHistoryRangeSelection) @@ -84,7 +90,6 @@ struct CoinDetailView: View { .onReceive(viewModel.$chartHistoryRangeSelection) { selectedRange in Task { await viewModel.fetchCoinPriceHistory( - coinData: coinData, forSelectedRange: selectedRange ) } @@ -132,7 +137,7 @@ struct CoinDetailView: View { news Button { } label: { - NavigationLink(destination: CoinNewsListView(coinData: coinData)) { + NavigationLink(destination: CoinNewsListView(coinData: viewModel.coinData)) { Text("View More") .frame(width: UIScreen.main.bounds.size.width - CoinDetailView.coinListFrameSize) .font(.system(size: 18)) @@ -143,12 +148,12 @@ struct CoinDetailView: View { .cornerRadius(CoinDetailView.viewMoreButton) Spacer() } - .navigationTitle(Text(verbatim: coinData.coinInfo?.title ?? "")) + .navigationTitle(Text(verbatim: viewModel.coinData.coinInfo?.title ?? "")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - viewModel.updateCoinFavoriteState(coinData: coinData) + viewModel.updateCoinFavoriteState() } label: { viewModel.isFavorite ? Image(systemName: Images.favorites) @@ -157,9 +162,7 @@ struct CoinDetailView: View { .tint(.gray) } } - .task { - await viewModel.onAppear(coinData: coinData) - } + .task(viewModel.onAppear) } } } From e560fd3a9aed85c44df956d611655c658c620659 Mon Sep 17 00:00:00 2001 From: Ege Sucu Date: Fri, 20 Oct 2023 14:16:17 +0300 Subject: [PATCH 39/40] SASU-0077 Changed the StateObject Init --- .../Coin/Detail/CoinDetailViewModel.swift | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift index 777d8ad..88c5435 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift @@ -16,6 +16,26 @@ class CoinDetailViewModel: ObservableObject { @Published var coinNewsDataModel: [CoinNewData]? @Published var priceChartSelectedXDateText = "" + let coinData: CoinData + + init( + isFavorite: Bool = false, + chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth, + coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? = nil, + isLoading: Bool = false, + coinNewsDataModel: [CoinNewData]? = nil, + priceChartSelectedXDateText: String = "", + coinData: CoinData + ) { + self.isFavorite = isFavorite + self.chartHistoryRangeSelection = chartHistoryRangeSelection + self.coinPriceHistoryChartDataModel = coinPriceHistoryChartDataModel + self.isLoading = isLoading + self.coinNewsDataModel = coinNewsDataModel + self.priceChartSelectedXDateText = priceChartSelectedXDateText + self.coinData = coinData + } + var rangeButtonsOpacity: Double { priceChartSelectedXDateText.isEmpty ? 1.0 : 0.0 } @@ -23,16 +43,16 @@ class CoinDetailViewModel: ObservableObject { private let coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol = CoinPriceHistoryUseCase() private let coinNewsUseCase: CoinNewsUseCaseProtocol = CoinNewsUseCase() - func onAppear(coinData: CoinData) async { - await checkIsCoinFavorite(coinData: coinData) + @Sendable + func onAppear() async { + await checkIsCoinFavorite() await fetchCoinPriceHistory( - coinData: coinData, forSelectedRange: chartHistoryRangeSelection ) - await fetchCoinNews(coinData: coinData) + await fetchCoinNews() } - func getIconURL(coinData: CoinData) -> URL? { + func getIconURL() -> URL? { guard let coinCode = coinData.coinInfo?.code else { return nil } @@ -40,22 +60,22 @@ class CoinDetailViewModel: ObservableObject { return URLs.Icons.getURL(from: coinCode) } - func getPriceString(coinData: CoinData) -> String { + func getPriceString() -> String { coinData.detail?.usd?.createPriceString() ?? "" } - func checkIsCoinFavorite(coinData: CoinData) async { + func checkIsCoinFavorite() async { await MainActor.run { isFavorite = StorageManager.shared.isCoinFavorite(coinData.coinInfo?.code ?? "") } } - func updateCoinFavoriteState(coinData: CoinData) { + func updateCoinFavoriteState() { isFavorite.toggle() StorageManager.shared.manageFavorites(coinData: coinData) } - func fetchCoinPriceHistory(coinData: CoinData, forSelectedRange range: CoinChartHistoryRange) async { + func fetchCoinPriceHistory(forSelectedRange range: CoinChartHistoryRange) async { guard let coinCode = coinData.coinInfo?.code else { return } await MainActor.run { @@ -95,7 +115,7 @@ class CoinDetailViewModel: ObservableObject { } } - func fetchCoinNews(coinData: CoinData) async { + func fetchCoinNews() async { guard let coinCode = coinData.coinInfo?.code else { return } do { let response = try await coinNewsUseCase.getCoinNews(coinCode: coinCode) From 76752c392d5c51110345ba1a5a77aec7c8370db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baha=20Ulu=C4=9F?= <75128919+bahaadesso@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:04:12 +0300 Subject: [PATCH 40/40] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f7aac7..4ec4993 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Because XcodeGen is used in this project, there will be no `.xcodeproj` or `.xcw xcodegen ``` -Swiftlint can also be installed via included scripts in the repository. Under the `{project_root}/scripts/installation` directory, simply run either or both of: +Swiftlint can also be installed via included scripts in the repository. Under the `{project_root}/scripts/installation` directory, simply run: ``` sh swiftlint.sh