diff --git a/.github/workflows/ios-build-check.yml b/.github/workflows/ios-build-check.yml index c7bd4d4..8f67dbd 100644 --- a/.github/workflows/ios-build-check.yml +++ b/.github/workflows/ios-build-check.yml @@ -12,9 +12,12 @@ env: jobs: build: name: Build scheme - runs-on: macos-latest - + 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 @@ -30,8 +33,8 @@ 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 - ios-version: 16 + api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} + app-file: build/SampleAppSwiftUI.app + ios-version: 16 diff --git a/README.md b/README.md index d8fa131..4ec4993 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) @@ -12,6 +12,7 @@ This is the iOS SwiftUI Sample App created by adesso Turkey. The project serves - 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/) -- [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) +- [Swift 5.9](https://developer.apple.com/support/xcode/) +- [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] -- [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [XcodeGen](https://github.com/yonaskolb/XcodeGen) ## Installation @@ -39,14 +39,13 @@ 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 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: ``` sh swiftlint.sh -sh swiftgen.sh ``` ## Branching Strategy @@ -103,7 +102,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. 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 77e10b0..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 - @StateObject private var router: Router = Router() + @StateObject private var router = Router() init() { UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance.init(idiom: .unspecified) @@ -25,7 +25,9 @@ struct SampleAppSwiftUIApp: App { WindowGroup { MainView() .environmentObject(router) - .onChange(of: phase, perform: manageChanges(for:)) + .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/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) - } + } } } 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/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/Network/WebSocket/WebSocketSubscription.swift b/SampleAppSwiftUI/Network/WebSocket/WebSocketSubscription.swift index 7a16c7f..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("Something went wrong") + } catch let error { + LoggerManager().setError(errorMessage: "Something went wrong \(error.localizedDescription)") } } } 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 deleted file mode 100644 index d548728..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 lightGray = ColorAsset(name: "LightGray") - 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/Constants/Generated/Strings+Generated.swift b/SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift deleted file mode 100644 index 14bfe5e..0000000 --- a/SampleAppSwiftUI/Resources/Constants/Generated/Strings+Generated.swift +++ /dev/null @@ -1,52 +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") - /// Home - public static let home = Strings.tr("Localizable", "Home", fallback: "Home") - /// News - public static let news = Strings.tr("Localizable", "News", fallback: "News") -} -// 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..2449566 --- /dev/null +++ b/SampleAppSwiftUI/Resources/Localizable.xcstrings @@ -0,0 +1,446 @@ +{ + "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." + } + } + } + }, + "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" : { + "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", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorites" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriler" + } + } + } + }, + "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.", + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hallo" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hello" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Merhaba" + } + } + } + }, + "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" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "News" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haberler" + } + } + } + }, + "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" +} \ 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 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 420ae6e..1d55f1f 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesView.swift @@ -10,7 +10,8 @@ import SwiftUI struct FavoritesView: View { @State private var searchTerm = "" @StateObject private var viewModel = FavoritesViewModel() - @EnvironmentObject private var router: Router + @EnvironmentObject var router: Router + var body: some View { NavigationStack(path: $router.favoritesNavigationPath) { VStack(spacing: Spacings.favorites) { @@ -26,19 +27,23 @@ 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) { searchTerm in - viewModel.filterResults(searchTerm: searchTerm) + .onChange(of: searchTerm, perform: { newValue in + viewModel.filterResults(searchTerm: newValue) viewModel.sortOptions(sort: viewModel.selectedSortOption) - } - .onChange(of: StorageManager.shared.favoriteCoins, perform: fetchFavorites) - .onChange(of: viewModel.selectedSortOption, perform: viewModel.sortOptions(sort:)) + }) + .onChange(of: StorageManager.shared.favoriteCoins, perform: { newValue in + fetchFavorites(codes: newValue) + }) + .onChange(of: viewModel.selectedSortOption, perform: { new in + viewModel.sortOptions(sort: new) + }) } private func fetchFavorites(codes: [CoinData]) { @@ -53,8 +58,7 @@ struct FavoritesView: View { } } -struct FavoritesView_Previews: PreviewProvider { - static var previews: some View { - FavoritesView() - } +#Preview { + FavoritesView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift index 96c07ec..a6f5b62 100644 --- a/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Favorites/FavoritesViewModel.swift @@ -10,7 +10,6 @@ import SwiftUI import Combine class FavoritesViewModel: ObservableObject { - private let checkWebSocket = true private var webSocketService: any WebSocketServiceProtocol @@ -24,7 +23,7 @@ class FavoritesViewModel: ObservableObject { @Published var filterTitle = SortOptions.defaultList.rawValue @Published var selectedSortOption: SortOptions = .defaultList - @State 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 81a27a9..caadf6f 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinListView.swift @@ -12,7 +12,7 @@ struct CoinListView: View { @Binding var filteredCoins: [CoinData] @State private var showingAlert = false @State private var alertTitle = "" - @EnvironmentObject private var router: Router + @EnvironmentObject var router: Router let favoriteChanged: () -> Void var body: some View { @@ -74,10 +74,9 @@ 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(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 be000d2..d83a459 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/CoinNews/CoinNewsListView.swift @@ -11,7 +11,9 @@ struct CoinNewsListView: View { @StateObject private var viewModel: CoinDetailViewModel init(coinData: CoinData) { - _viewModel = StateObject(wrappedValue: CoinDetailViewModel(coinData: coinData)) + _viewModel = StateObject( + wrappedValue: CoinDetailViewModel(coinData: coinData) + ) } var body: some View { @@ -27,7 +29,8 @@ struct CoinNewsListView: View { if let image = phase.image { image.resizable() } else { - Resources.Images.worldNews.swiftUIImage.resizable() + Image(.worldNews) + .resizable() } } .scaledToFit() @@ -41,14 +44,12 @@ struct CoinNewsListView: View { .listStyle(.inset) } }.frame(height: UIScreen.main.bounds.height) - .navigationTitle(Strings.news) + .navigationTitle("News") .navigationBarTitleDisplayMode(.inline) .navigationViewStyle(.automatic) } } - .onAppear { - viewModel.onAppear() - } + .task(viewModel.onAppear) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift b/SampleAppSwiftUI/Scenes/Home/Coin/CoinView.swift index 1c37da7..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() } @@ -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 55cf2c8..c8edfed 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailView.swift @@ -18,99 +18,123 @@ struct CoinDetailView: View { @StateObject private var viewModel: CoinDetailViewModel init(coinData: CoinData) { - _viewModel = StateObject(wrappedValue: CoinDetailViewModel(coinData: coinData)) + _viewModel = StateObject( + wrappedValue: 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 { - Resources.Images.defaultCoin.swiftUIImage.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) + .onReceive(viewModel.$chartHistoryRangeSelection) { selectedRange in + Task { + await viewModel.fetchCoinPriceHistory( + forSelectedRange: selectedRange + ) + } + } + } + + 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)) { @@ -118,9 +142,9 @@ 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.lightGray) + }.background(Color(.lightestGray)) .cornerRadius(CoinDetailView.viewMoreButton) Spacer() } @@ -138,20 +162,16 @@ struct CoinDetailView: View { .tint(.gray) } } - .onAppear { - viewModel.onAppear() - } + .task(viewModel.onAppear) } } } -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/CoinDetailViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift index a9a7c31..88c5435 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinDetailViewModel.swift @@ -6,39 +6,50 @@ // import Foundation +import Combine class CoinDetailViewModel: ObservableObject { + @Published var isFavorite: Bool = false + @Published var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth + private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? + @Published var isLoading = false + @Published var coinNewsDataModel: [CoinNewData]? + @Published var priceChartSelectedXDateText = "" + let coinData: CoinData - @Published private(set) var isFavorite: Bool = false - @Published var chartHistoryRangeSelection: CoinChartHistoryRange = .sixMonth { - didSet { - fetchCoinPriceHistory(forSelectedRange: chartHistoryRangeSelection) - } + + 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 } - @Published private(set) var coinPriceHistoryChartDataModel: CoinPriceHistoryChartDataModel? - @Published private(set) var coinNewsDataModel: [CoinNewData]? - @Published private(set) var isLoading: Bool = false - @Published var priceChartSelectedXDateText: String = "" var rangeButtonsOpacity: Double { priceChartSelectedXDateText.isEmpty ? 1.0 : 0.0 } - private let coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol - private let coinNewsUseCase: CoinNewsUseCaseProtocol + private let coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol = CoinPriceHistoryUseCase() + private let coinNewsUseCase: CoinNewsUseCaseProtocol = CoinNewsUseCase() - init(coinData: CoinData, - coinPriceHistoryUseCase: CoinPriceHistoryUseCaseProtocol = CoinPriceHistoryUseCase(), - coinNewsUseCase: CoinNewsUseCaseProtocol = CoinNewsUseCase()) { - self.coinData = coinData - self.coinPriceHistoryUseCase = coinPriceHistoryUseCase - self.coinNewsUseCase = coinNewsUseCase - } - - func onAppear() { - checkIsCoinFavorite() - fetchCoinPriceHistory(forSelectedRange: chartHistoryRangeSelection) - fetchCoinNews() + @Sendable + func onAppear() async { + await checkIsCoinFavorite() + await fetchCoinPriceHistory( + forSelectedRange: chartHistoryRangeSelection + ) + await fetchCoinNews() } func getIconURL() -> URL? { @@ -53,8 +64,10 @@ class CoinDetailViewModel: ObservableObject { coinData.detail?.usd?.createPriceString() ?? "" } - func checkIsCoinFavorite() { - isFavorite = StorageManager.shared.isCoinFavorite(coinData.coinInfo?.code ?? "") + func checkIsCoinFavorite() async { + await MainActor.run { + isFavorite = StorageManager.shared.isCoinFavorite(coinData.coinInfo?.code ?? "") + } } func updateCoinFavoriteState() { @@ -62,52 +75,57 @@ class CoinDetailViewModel: ObservableObject { StorageManager.shared.manageFavorites(coinData: coinData) } - func fetchCoinPriceHistory(forSelectedRange range: CoinChartHistoryRange) { + func fetchCoinPriceHistory(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() 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 e4f97df..546d764 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartView.swift @@ -11,7 +11,7 @@ import Charts struct CoinPriceHistoryChartView: View { @StateObject private var viewModel: CoinPriceHistoryChartViewModel - init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: Binding) { + init(selectedRange: CoinChartHistoryRange, dataModel: CoinPriceHistoryChartDataModel, selectedXDateText: String) { _viewModel = StateObject( wrappedValue: CoinPriceHistoryChartViewModel( selectedRange: selectedRange, @@ -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: "" + ) } - - 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: "" + ) } - .preferredColorScheme(.dark) } + .preferredColorScheme(.dark) } } diff --git a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift index eed4c00..44f171b 100644 --- a/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/Coin/Detail/CoinPriceHistoryChartViewModel.swift @@ -10,16 +10,16 @@ import SwiftUI import Charts class CoinPriceHistoryChartViewModel: ObservableObject { - var selectedRange: CoinChartHistoryRange - var dataModel: CoinPriceHistoryChartDataModel - @Binding var selectedXDateText: String + @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 @Published 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 + self.selectedXDateText = selectedXDateText self.selectedX = nil } @@ -69,15 +69,14 @@ 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 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 + } } - } func onEndDrag() { selectedX = nil diff --git a/SampleAppSwiftUI/Scenes/Home/FilterView.swift b/SampleAppSwiftUI/Scenes/Home/FilterView.swift index 1e96e90..cd55264 100644 --- a/SampleAppSwiftUI/Scenes/Home/FilterView.swift +++ b/SampleAppSwiftUI/Scenes/Home/FilterView.swift @@ -8,25 +8,31 @@ import SwiftUI struct FilterView: View { - @ObservedObject var viewModel: ViewModel + 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 fbcb04a..4829f47 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeView.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeView.swift @@ -12,7 +12,8 @@ struct HomeView: View { @StateObject private var viewModel = HomeViewModel() @State private var searchTerm = "" - @EnvironmentObject private var router: Router + @EnvironmentObject var router: Router + var body: some View { NavigationStack(path: $router.homeNavigationPath) { VStack(spacing: Spacings.home) { @@ -26,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 { @@ -34,22 +35,24 @@ struct HomeView: View { } } } - .background(Color.lightGray) - .onFirstAppear { + .background(Color(.lightestGray)) + .ignoresSafeArea(.all, edges: [.top, .trailing, .leading]) + .onAppear { Task { await viewModel.fillModels() } } - .onChange(of: searchTerm) { searchTerm in - viewModel.filterResults(searchTerm: searchTerm) + .onChange(of: searchTerm, perform: { newValue in + viewModel.filterResults(searchTerm: newValue) viewModel.sortOptions(sort: viewModel.selectedSortOption) - } - .onChange(of: viewModel.selectedSortOption, perform: viewModel.sortOptions(sort:)) + }) + .onChange(of: viewModel.selectedSortOption, perform: { newValue in + viewModel.sortOptions(sort: newValue) + }) } } -struct HomeView_Previews: PreviewProvider { - static var previews: some View { - HomeView() - } +#Preview { + HomeView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift index 46bce84..e8bbdb6 100644 --- a/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift +++ b/SampleAppSwiftUI/Scenes/Home/HomeViewModel.swift @@ -14,10 +14,10 @@ class HomeViewModel: ObservableObject { @Published var coinList: [CoinData] = [] @Published var filteredCoins: [CoinData] = [] @Published var filterTitle = SortOptions.defaultList.rawValue - @Published var selectedSortOption: SortOptions = .defaultList let listPageLimit = 10 - @State var isLoading: Bool = false + @Published var isLoading: Bool = false + @Published var selectedSortOption: SortOptions = .defaultList func fillModels(demo: Bool = false) async { if demo { @@ -28,7 +28,7 @@ class HomeViewModel: ObservableObject { 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 { @@ -60,6 +60,7 @@ class HomeViewModel: ObservableObject { } extension HomeViewModel: ViewModelProtocol { + func checkLastItem(_ item: CoinData) { guard !isLoading else { return } diff --git a/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift b/SampleAppSwiftUI/Scenes/Home/SearchBarView.swift index 8592d5e..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") @@ -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/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 b90139f..18c4efd 100644 --- a/SampleAppSwiftUI/Scenes/Main/MainView.swift +++ b/SampleAppSwiftUI/Scenes/Main/MainView.swift @@ -10,8 +10,9 @@ import SwiftUI struct MainView: View { - @StateObject private var storageManager = StorageManager.shared - @EnvironmentObject private var router: Router + private var storageManager = StorageManager.shared + @EnvironmentObject var router: Router + var body: some View { TabView(selection: $router.selectedTab) { HomeView() @@ -38,8 +39,7 @@ struct MainView: View { } } -struct MainView_Previews: PreviewProvider { - static var previews: some View { - MainView() - } +#Preview { + MainView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift b/SampleAppSwiftUI/Scenes/Settings/SettingsView.swift index 941a7ff..0610828 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 + @EnvironmentObject var router: Router + var body: some View { NavigationStack(path: $router.settingsNavigationPath) { VStack(alignment: .leading, spacing: Spacings.settings) { @@ -32,7 +33,7 @@ extension SettingsView { VStack { HStack { Text("Settings") - .settingsTextStyle(fontType: .bold, fontSize: .title2, foregroundColor: .settingsViewTitleColor) + .settingsTextStyle(fontType: .bold, fontSize: .title2, foregroundColor: .settingsViewTitle) Spacer() } } @@ -42,7 +43,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 +53,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 +61,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,14 +77,14 @@ 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() } } -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - } +#Preview { + SettingsView() + .environmentObject(Router()) } diff --git a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift index a5e3dcb..2e2a090 100644 --- a/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift +++ b/SampleAppSwiftUI/Scenes/ViewModelProtocol.swift @@ -18,7 +18,9 @@ protocol ViewModelProtocol: ObservableObject { 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/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/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)") } } } 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() { } 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 d2b7ad3..9c326de 100644 --- a/project.yml +++ b/project.yml @@ -11,10 +11,15 @@ options: iOS: 16.0 attributes: - BuildIndependentTargetsInParallel: true + BuildIndependentTargetsInParallel: YES settings: - enableBaseInternationalization: true + ENABLE_USER_SCRIPT_SANDBOXING: false + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true + 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: @@ -32,7 +37,10 @@ targets: dependencies: - package: CocoaLumberjack product: CocoaLumberjack + - package: CocoaLumberjack product: CocoaLumberjackSwift + - package: CocoaLumberjack + product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse product: PulseUI @@ -52,15 +60,15 @@ targets: - path: scripts/installation/swiftlint.sh name: SwiftLint basedOnDependencyAnalysis: false - - path: scripts/installation/swiftgen.sh - name: SwiftGen - basedOnDependencyAnalysis: false SampleAppSwiftUITests: dependencies: - target: SampleAppSwiftUI - package: CocoaLumberjack product: CocoaLumberjack + - package: CocoaLumberjack product: CocoaLumberjackSwift + - package: CocoaLumberjack + product: CocoaLumberjackSwiftLogBackend - package: Pulse product: Pulse product: PulseUI @@ -73,3 +81,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 deleted file mode 100644 index 5cdb0de..0000000 --- a/swiftgen.yml +++ /dev/null @@ -1,26 +0,0 @@ -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 - - Colors.xcassets - - Icons.xcassets - - Images.xcassets - outputs: - templateName: swift5 - params: - forceProvidesNamespaces: true - forceFileNameEnum: true - enumName: Resources - output: Assets+Generated.swift -