diff --git a/0.application/CryptoPulse.xcodeproj/project.pbxproj b/0.application/CryptoPulse.xcodeproj/project.pbxproj index a7e47ba..34a996d 100644 --- a/0.application/CryptoPulse.xcodeproj/project.pbxproj +++ b/0.application/CryptoPulse.xcodeproj/project.pbxproj @@ -11,8 +11,9 @@ 130C567A2BA48EC50040DEF9 /* CryptoCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 130C56772BA48EC50040DEF9 /* CryptoCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 130C567B2BA48EC50040DEF9 /* CryptoView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 130C56782BA48EC50040DEF9 /* CryptoView.framework */; }; 130C567C2BA48EC50040DEF9 /* CryptoView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 130C56782BA48EC50040DEF9 /* CryptoView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1334008F2BAC72780022678D /* CryptoLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1334008E2BAC72780022678D /* CryptoLogger.framework */; }; + 133400902BAC72780022678D /* CryptoLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1334008E2BAC72780022678D /* CryptoLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1345B79B2BA48BE300408B4A /* CryptoPulseApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1345B79A2BA48BE300408B4A /* CryptoPulseApp.swift */; }; - 1345B79D2BA48BE300408B4A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1345B79C2BA48BE300408B4A /* ContentView.swift */; }; 1345B79F2BA48BE400408B4A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1345B79E2BA48BE400408B4A /* Assets.xcassets */; }; 1345B7A22BA48BE400408B4A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1345B7A12BA48BE400408B4A /* Preview Assets.xcassets */; }; 13CE02CD2BA48DCF00EF1910 /* CryptoNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13CE02CC2BA48DCF00EF1910 /* CryptoNetwork.framework */; }; @@ -29,6 +30,7 @@ dstSubfolderSpec = 10; files = ( 130C567A2BA48EC50040DEF9 /* CryptoCore.framework in Embed Frameworks */, + 133400902BAC72780022678D /* CryptoLogger.framework in Embed Frameworks */, 130C567C2BA48EC50040DEF9 /* CryptoView.framework in Embed Frameworks */, 13DE3B4D2BA49267005F0771 /* CryptoUtilities.framework in Embed Frameworks */, 13CE02CE2BA48DCF00EF1910 /* CryptoNetwork.framework in Embed Frameworks */, @@ -41,9 +43,9 @@ /* Begin PBXFileReference section */ 130C56772BA48EC50040DEF9 /* CryptoCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 130C56782BA48EC50040DEF9 /* CryptoView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1334008E2BAC72780022678D /* CryptoLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1345B7972BA48BE300408B4A /* CryptoPulse.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CryptoPulse.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1345B79A2BA48BE300408B4A /* CryptoPulseApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoPulseApp.swift; sourceTree = ""; }; - 1345B79C2BA48BE300408B4A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 1345B79E2BA48BE400408B4A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1345B7A12BA48BE400408B4A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 13CE02CC2BA48DCF00EF1910 /* CryptoNetwork.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoNetwork.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -56,6 +58,7 @@ buildActionMask = 2147483647; files = ( 130C56792BA48EC50040DEF9 /* CryptoCore.framework in Frameworks */, + 1334008F2BAC72780022678D /* CryptoLogger.framework in Frameworks */, 130C567B2BA48EC50040DEF9 /* CryptoView.framework in Frameworks */, 13DE3B4C2BA49267005F0771 /* CryptoUtilities.framework in Frameworks */, 13CE02CD2BA48DCF00EF1910 /* CryptoNetwork.framework in Frameworks */, @@ -86,7 +89,6 @@ isa = PBXGroup; children = ( 1345B79A2BA48BE300408B4A /* CryptoPulseApp.swift */, - 1345B79C2BA48BE300408B4A /* ContentView.swift */, 1345B79E2BA48BE400408B4A /* Assets.xcassets */, 1345B7A02BA48BE400408B4A /* Preview Content */, ); @@ -104,6 +106,7 @@ 13CE02CB2BA48DCF00EF1910 /* Frameworks */ = { isa = PBXGroup; children = ( + 1334008E2BAC72780022678D /* CryptoLogger.framework */, 13DE3B4B2BA49267005F0771 /* CryptoUtilities.framework */, 130C56772BA48EC50040DEF9 /* CryptoCore.framework */, 130C56782BA48EC50040DEF9 /* CryptoView.framework */, @@ -183,7 +186,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1345B79D2BA48BE300408B4A /* ContentView.swift in Sources */, 1345B79B2BA48BE300408B4A /* CryptoPulseApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -330,7 +332,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoPulse; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.pulse; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -362,7 +364,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoPulse; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.pulse; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; diff --git a/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/1024.png b/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..477350f Binary files /dev/null and b/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/Contents.json b/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..cff1680 100644 --- a/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/0.application/CryptoPulse/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/0.application/CryptoPulse/ContentView.swift b/0.application/CryptoPulse/ContentView.swift deleted file mode 100644 index 5808767..0000000 --- a/0.application/CryptoPulse/ContentView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ContentView.swift -// CryptoPulse -// -// Created by francesco scalise on 15/03/24. -// - -import SwiftUI -import CryptoNetwork - -struct ContentView: View { - - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("morning") - } - } -} - -// Preview -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/0.application/CryptoPulse/CryptoPulseApp.swift b/0.application/CryptoPulse/CryptoPulseApp.swift index 762fd81..94a2449 100644 --- a/0.application/CryptoPulse/CryptoPulseApp.swift +++ b/0.application/CryptoPulse/CryptoPulseApp.swift @@ -32,12 +32,11 @@ */ import SwiftUI +import CryptoView @main struct CryptoPulseApp: App { var body: some Scene { - WindowGroup { - ContentView() - } + CryptoScene() } } diff --git a/1.sdks/0.view/CryptoView.xcodeproj/project.pbxproj b/1.sdks/0.view/CryptoView.xcodeproj/project.pbxproj index a406145..8b442b6 100644 --- a/1.sdks/0.view/CryptoView.xcodeproj/project.pbxproj +++ b/1.sdks/0.view/CryptoView.xcodeproj/project.pbxproj @@ -7,14 +7,28 @@ objects = { /* Begin PBXBuildFile section */ + 130B9F822BA9B0DA00225D24 /* CryptoScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130B9F812BA9B0DA00225D24 /* CryptoScene.swift */; }; + 130B9F852BA9CE5600225D24 /* CryptoDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130B9F842BA9CE5600225D24 /* CryptoDetailsView.swift */; }; + 130B9F872BA9CE6400225D24 /* CryptoDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130B9F862BA9CE6400225D24 /* CryptoDetailsViewModel.swift */; }; 130C567F2BA48ED20040DEF9 /* CryptoCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 130C567E2BA48ED20040DEF9 /* CryptoCore.framework */; }; + 133400972BAC91A00022678D /* ExpandableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133400962BAC91A00022678D /* ExpandableView.swift */; }; 133E7FB52BA48E1D0036F83F /* CryptoView.h in Headers */ = {isa = PBXBuildFile; fileRef = 133E7FB42BA48E1D0036F83F /* CryptoView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE3BA62BA9406B005F0771 /* MVI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3BA52BA9406B005F0771 /* MVI.swift */; }; + 13DE3BAF2BA9484D005F0771 /* CryptoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3BAE2BA9484D005F0771 /* CryptoListViewModel.swift */; }; + 13DE3BB12BA948A0005F0771 /* CryptoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3BB02BA948A0005F0771 /* CryptoListView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 130B9F812BA9B0DA00225D24 /* CryptoScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoScene.swift; sourceTree = ""; }; + 130B9F842BA9CE5600225D24 /* CryptoDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoDetailsView.swift; sourceTree = ""; }; + 130B9F862BA9CE6400225D24 /* CryptoDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoDetailsViewModel.swift; sourceTree = ""; }; 130C567E2BA48ED20040DEF9 /* CryptoCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 133400962BAC91A00022678D /* ExpandableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableView.swift; sourceTree = ""; }; 133E7FB12BA48E1D0036F83F /* CryptoView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CryptoView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 133E7FB42BA48E1D0036F83F /* CryptoView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CryptoView.h; sourceTree = ""; }; + 13DE3BA52BA9406B005F0771 /* MVI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVI.swift; sourceTree = ""; }; + 13DE3BAE2BA9484D005F0771 /* CryptoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoListViewModel.swift; sourceTree = ""; }; + 13DE3BB02BA948A0005F0771 /* CryptoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoListView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -29,6 +43,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 130B9F832BA9CE3300225D24 /* details */ = { + isa = PBXGroup; + children = ( + 130B9F842BA9CE5600225D24 /* CryptoDetailsView.swift */, + 130B9F862BA9CE6400225D24 /* CryptoDetailsViewModel.swift */, + ); + path = details; + sourceTree = ""; + }; 130C567D2BA48ED20040DEF9 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -37,6 +60,14 @@ name = Frameworks; sourceTree = ""; }; + 133400952BAC918B0022678D /* helpers */ = { + isa = PBXGroup; + children = ( + 133400962BAC91A00022678D /* ExpandableView.swift */, + ); + path = helpers; + sourceTree = ""; + }; 133E7FA72BA48E1C0036F83F = { isa = PBXGroup; children = ( @@ -58,10 +89,48 @@ isa = PBXGroup; children = ( 133E7FB42BA48E1D0036F83F /* CryptoView.h */, + 13DE3BB22BA9545E005F0771 /* system-handler */, + 13DE3BA72BA94171005F0771 /* screens */, + 13DE3BA42BA9403D005F0771 /* mvi */, + 133400952BAC918B0022678D /* helpers */, ); path = CryptoView; sourceTree = ""; }; + 13DE3BA42BA9403D005F0771 /* mvi */ = { + isa = PBXGroup; + children = ( + 13DE3BA52BA9406B005F0771 /* MVI.swift */, + ); + path = mvi; + sourceTree = ""; + }; + 13DE3BA72BA94171005F0771 /* screens */ = { + isa = PBXGroup; + children = ( + 13DE3BAD2BA947F4005F0771 /* list */, + 130B9F832BA9CE3300225D24 /* details */, + ); + path = screens; + sourceTree = ""; + }; + 13DE3BAD2BA947F4005F0771 /* list */ = { + isa = PBXGroup; + children = ( + 13DE3BB02BA948A0005F0771 /* CryptoListView.swift */, + 13DE3BAE2BA9484D005F0771 /* CryptoListViewModel.swift */, + ); + path = list; + sourceTree = ""; + }; + 13DE3BB22BA9545E005F0771 /* system-handler */ = { + isa = PBXGroup; + children = ( + 130B9F812BA9B0DA00225D24 /* CryptoScene.swift */, + ); + path = "system-handler"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -105,6 +174,7 @@ TargetAttributes = { 133E7FB02BA48E1D0036F83F = { CreatedOnToolsVersion = 15.3; + LastSwiftMigration = 1530; }; }; }; @@ -141,6 +211,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13DE3BA62BA9406B005F0771 /* MVI.swift in Sources */, + 13DE3BAF2BA9484D005F0771 /* CryptoListViewModel.swift in Sources */, + 133400972BAC91A00022678D /* ExpandableView.swift in Sources */, + 130B9F852BA9CE5600225D24 /* CryptoDetailsView.swift in Sources */, + 13DE3BB12BA948A0005F0771 /* CryptoListView.swift in Sources */, + 130B9F872BA9CE6400225D24 /* CryptoDetailsViewModel.swift in Sources */, + 130B9F822BA9B0DA00225D24 /* CryptoScene.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -276,6 +353,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -294,7 +372,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoView; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.view; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -303,6 +381,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -312,6 +391,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -330,7 +410,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoView; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.view; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/1.sdks/0.view/CryptoView/helpers/ExpandableView.swift b/1.sdks/0.view/CryptoView/helpers/ExpandableView.swift new file mode 100644 index 0000000..fa8a649 --- /dev/null +++ b/1.sdks/0.view/CryptoView/helpers/ExpandableView.swift @@ -0,0 +1,55 @@ +// +// ExpandableView.swift +// CryptoView +// +// Created by francesco scalise on 21/03/24. +// + +import SwiftUI + +struct ExpandableView: View { + @State private var isExpanded = false + + private let headerText: String + private let bodyText: String + + private let textStyle: Font = .body + private let textColor: Color = .white + + private let previewLimit: Int = 100 + + public init(fullText: String) { + if fullText.count > previewLimit { + let index = fullText.index(fullText.startIndex, offsetBy: previewLimit) + self.headerText = String(fullText[.. some View { + ZStack { + Color.black.opacity(0.9) + .edgesIgnoringSafeArea(.all) + + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(2) + } + } + + @ViewBuilder + private func contentView(for details: CryptoDetails) -> some View { + ScrollView { + + closeButton() + + VStack(alignment: .center) { + + imageView(for: details) + + descriptionView(for: details) + + homepageView(for: details) + + chartView(prices: details.historicalPrices) + } + } + .background(Color.black.opacity(0.9)) + } + + @ViewBuilder + private func closeButton() -> some View { + HStack { + Spacer() + Button(action: { + self.presentationMode.wrappedValue.dismiss() + }) { + Image(systemName: "xmark") + .foregroundColor(.white) + .font(.system(size: 14)) + .padding(12) +// .background(Circle().fill(Color.gray.opacity(0.6))) + .frame(width: 24, height: 24) + } + } + .padding(.top, 10) + .padding(.trailing, 10) + } + + @ViewBuilder + private func imageView(for details: CryptoDetails) -> some View { + AsyncImage(url: details.imageURL) { phase in + switch phase { + case .empty: + Image(systemName: "photo") + case .success(let image): + image.resizable() + .scaledToFit() + .frame(height: 200) + case .failure: + Image(systemName: "photo") + @unknown default: + EmptyView() + } + } + .padding() + } + + @ViewBuilder + private func descriptionView(for details: CryptoDetails) -> some View { + VStack(alignment: .center) { + Text("\(details.name) - \(details.symbol)") + // .padding() + .foregroundColor(Color.white) + + HStack { + Text("\(details.currentPrice) \(details.currency)") + // .padding() + .foregroundColor(Color.white) + + Text(formatPriceChange(details.priceChangePercentage24H)) + .foregroundColor(details.priceChangePercentage24H.contains("-") ? Color.red : Color.green) + .font(.headline) + .padding(3) + .background(details.priceChangePercentage24H.contains("-") ? Color.red.opacity(0.2) : Color.green.opacity(0.2)) + .cornerRadius(7) + } + + Divider() + .overlay(Color.white.opacity(0.2)) + + ExpandableView(fullText: details.description) + .padding() + } + } + + @ViewBuilder + private func homepageView(for details: CryptoDetails) -> some View { + if let homepageURL = details.homepageURL { + + Divider() + .overlay(Color.white.opacity(0.2)) + Link("\(homepageURL)", destination: homepageURL) + .padding() + Divider() + .overlay(Color.white.opacity(0.2)) + .padding(.bottom) + } + } + + @ViewBuilder + private func chartView(prices: [Double]) -> some View { + if prices.isEmpty { + Text("No data available") + .foregroundColor(.gray) + .padding() + } else { + let overallChangeColor = overallChangeIsPositive(viewModel.state.details?.priceChangePercentage24H ?? "0") ? Color.green : Color.red + + let daysLabels = calculateDaysLabels(count: prices.count) + + let chartData = zip(daysLabels, prices).map(ChartData.init) + + Chart(chartData, id: \.dayLabel) { data in + LineMark( + x: .value("Day", data.dayLabel), + y: .value("Price", data.price) + ) + .interpolationMethod(.catmullRom) + .foregroundStyle(overallChangeColor) + + AreaMark( + x: .value("Day", data.dayLabel), + y: .value("Price", data.price) + ) + .interpolationMethod(.catmullRom) + .foregroundStyle(LinearGradient(gradient: Gradient(colors: [overallChangeColor.opacity(0.6), overallChangeColor.opacity(0)]), startPoint: .top, endPoint: .bottom)) + } + .chartXAxis { + AxisMarks(preset: .aligned, position: .bottom) { + AxisGridLine().foregroundStyle(Color.white.opacity(0.2)) + AxisTick() + AxisValueLabel().foregroundStyle(Color.white) + } + } + .chartYAxis { + AxisMarks(preset: .aligned, position: .trailing) { + AxisGridLine().foregroundStyle(Color.white.opacity(0.2)) + AxisTick() + AxisValueLabel().foregroundStyle(Color.white) + } + } + .frame(height: 300) + .padding() + } + } + + @ViewBuilder + private func errorView(error: Error) -> some View { + +//#if DEBUG +// let description = String(describing: error) +//#else + let description = error.localizedDescription +//#endif + + ScrollView { + ZStack { + // Reserve space matching the scroll view's frame + Spacer().containerRelativeFrame([.horizontal, .vertical]) + + // Form content + VStack { + closeButton() + + ContentUnavailableView( + "Error", + systemImage: "exclamationmark.triangle", + description: Text(description) + ) + .foregroundStyle(Color.white) + } + .padding() + } + } + .background(Color.black.opacity(0.9)) + } +} + +// MARK: - Helper + +extension CryptoDetailsView { + + private func overallChangeIsPositive(_ change: String) -> Bool { + guard let value = Double(change.trimmingCharacters(in: CharacterSet(charactersIn: "%"))) else { return false } + return value > 0 + } + + private func calculateDaysLabels(count: Int) -> [String] { + let calendar = Calendar.current + let today = Date() + var labels = (0.. String { + guard let value = Double(change.trimmingCharacters(in: CharacterSet(charactersIn: "%"))) else { return change } + return value > 0 ? "+\(change)" : change + } + +} + +// MARK: - Preview + +#if DEBUG +#Preview { + let crypto = Crypto(id: "bitcoin", currency: "EUR", name: "Bitcoin", symbol: "BTC", imageUrl: URL(string: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png")!, currentPrice: "€30,000", priceChangePercentage24H: "-500.32%") + let service = MockCryptoDetailsService() + // let service = CryptoDetailsService() + let listViewModel = CryptoListViewModel() + let viewModel = CryptoDetailsViewModel(crypto: crypto, cryptoDetailsService: service) + return CryptoDetailsView(viewModel: viewModel) + .previewLayout(.sizeThatFits) +} +#endif + +// MARK: - Helper + +struct ChartData { + var dayLabel: String + var price: Double +} diff --git a/1.sdks/0.view/CryptoView/screens/details/CryptoDetailsViewModel.swift b/1.sdks/0.view/CryptoView/screens/details/CryptoDetailsViewModel.swift new file mode 100644 index 0000000..b60b17e --- /dev/null +++ b/1.sdks/0.view/CryptoView/screens/details/CryptoDetailsViewModel.swift @@ -0,0 +1,114 @@ +// +// CryptoDetailsViewModel.swift +// CryptoView +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation +import CryptoCore + +// MARK: - MVI + +struct CryptoDetailsViewState: MVI.State { + var details: CryptoDetails? = nil + var isLoading = false + var error: Error? = nil +} + +enum CryptoDetailsViewIntent: MVI.Intent { + case fetchCryptoDetails +} + +enum CryptoDetailsViewEffect: MVI.Effect { + case showToast(String) + case showError(Error) +} + +// MARK: - CryptoDetailsViewModel + +final class CryptoDetailsViewModel: ViewModel { + + typealias S = CryptoDetailsViewState + typealias I = CryptoDetailsViewIntent + typealias E = CryptoDetailsViewEffect + + @Published var state = CryptoDetailsViewState() + + private let crypto: Crypto + private let cryptoDetailsService: CryptoDetailsServiceProtocol + + init( + crypto: Crypto, + cryptoDetailsService: CryptoDetailsServiceProtocol = CryptoDetailsService() + ) { + self.crypto = crypto + self.cryptoDetailsService = cryptoDetailsService + } +} + +// MARK: - MVI Intent + +extension CryptoDetailsViewModel { + + func dispatch(_ intent: CryptoDetailsViewIntent) { + switch intent { + case .fetchCryptoDetails: + fetchCryptoDetails() + } + } + + private func fetchCryptoDetails() { + state.isLoading = true + state.error = nil + + Task { + do { + let details = try await cryptoDetailsService.fetchCryptoDetails(for: crypto) + DispatchQueue.main.async { + self.state.isLoading = false + self.state.details = details + } + } catch NetworkError.rateLimited(let retryAfter) { + DispatchQueue.main.async { + self.state.isLoading = false + // Handle showing toast here + self.trigger(.showToast("You've hit the rate limit. Please wait \(retryAfter) seconds before retrying.")) + } + } catch { + DispatchQueue.main.async { + self.state.isLoading = false + self.trigger(.showError(error)) + } + } + } + } +} + +// MARK: - MVI Effect + +extension CryptoDetailsViewModel { + + func trigger(_ effect: CryptoDetailsViewEffect) { + switch effect { + case .showToast(let message): + state.error = TooManyRequestError.retryAfter(message) + case .showError(let error): + state.error = error + } + } + +} + +// MARK: - Error + +enum TooManyRequestError: Error, LocalizedError { + case retryAfter(String) + + var errorDescription: String? { + switch self { + case .retryAfter(let message): + return message + } + } +} diff --git a/1.sdks/0.view/CryptoView/screens/list/CryptoListView.swift b/1.sdks/0.view/CryptoView/screens/list/CryptoListView.swift new file mode 100644 index 0000000..19d9797 --- /dev/null +++ b/1.sdks/0.view/CryptoView/screens/list/CryptoListView.swift @@ -0,0 +1,280 @@ +// +// CryptoListView.swift +// CryptoView +// +// Created by francesco scalise on 19/03/24. +// + +import SwiftUI +import CryptoCore + +// MARK: - CryptoListView + +struct CryptoListView: View { + + @ObservedObject var viewModel: CryptoListViewModel + + init(viewModel: CryptoListViewModel) { + self.viewModel = viewModel + + viewModel.dispatch(.fetchTitle) + viewModel.dispatch(.fetchTodayDate) + viewModel.dispatch(.fetchTopCryptos) + } + + var body: some View { + VStack { + ZStack { + Color.black + if let error = viewModel.state.error { + errorView(error: error) + } else if viewModel.state.isLoading { + progressView() + } else { + contentView() + } + } + .edgesIgnoringSafeArea(.all) + .sheet(item: $viewModel.state.selectedCrypto) { crypto in + CryptoDetailsView(viewModel: .init(crypto: crypto)) + } + } + .background(Color.black) + .overlay(toastView, alignment: .bottom) + } + +} + +// MARK: - ViewBuilder + +extension CryptoListView { + + @ViewBuilder + private func progressView() -> some View { + ZStack { + Color.black.opacity(0.9) + .edgesIgnoringSafeArea(.all) + + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(2) + } + } + + @ViewBuilder + private func contentView() -> some View { + VStack(alignment: .leading) { + + Spacer(minLength: 50) + + marketsHedgingView() + todayDateView() + + listView() + //Spacer() + } + } + + @ViewBuilder + private func errorView(error: Error) -> some View { + //#if DEBUG + // let description = String(describing: error) + //#else + let description = error.localizedDescription + //#endif + + ScrollView { + ZStack { + // Reserve space matching the scroll view's frame + Spacer().containerRelativeFrame([.horizontal, .vertical]) + + // Form content + VStack { + ContentUnavailableView( + "Error", + systemImage: "exclamationmark.triangle", + description: Text(description) + ) + .foregroundStyle(Color.white) + } + .padding() + } + } + .refreshable { + viewModel.dispatch(.refreshData) + } + } + + @ViewBuilder + private func marketsHedgingView() -> some View { + Text(viewModel.state.title) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0)) + } + + @ViewBuilder + private func todayDateView() -> some View { + Text(viewModel.state.today) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.gray) + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0)) + } + + @ViewBuilder + private func listView() -> some View { + VStack { + List { + ForEach(viewModel.state.cryptos, id: \.id) { crypto in + Button(action: { + viewModel.trigger(.navigationToDetails(crypto)) + }) { + CryptoCellView(crypto: crypto) + } + } + // .listRowSeparator(.hidden) + .listRowSeparatorTint(Color.gray) + .listRowBackground(Color.clear) + } + .frame(maxWidth: .infinity) + .edgesIgnoringSafeArea(.all) + .listStyle(PlainListStyle()) + .refreshable { + viewModel.dispatch(.refreshData) + } + } + .background(Color.black) + } + + @ViewBuilder + private var toastView: some View { + Group { + if viewModel.state.toast.0 { + Text(viewModel.state.toast.1!) + .padding() + .background(Color.gray.opacity(0.9)) + .foregroundColor(.white) + .cornerRadius(8) + .transition(.opacity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + viewModel.state.toast = (false, nil) + } + } + } + } + } +} + +// MARK: - Preview + +#if DEBUG +#Preview { + let service = MockCryptoListService() + // let service = CryptoListService() + let viewModel = CryptoListViewModel(cryptoListService: service) + return CryptoListView(viewModel: viewModel) + .previewLayout(.sizeThatFits) +} +#endif + +// MARK: - CryptoCellView + +struct CryptoCellView: View { + var crypto: Crypto + + private let priceChangeWidth: CGFloat = 100 + + var body: some View { + HStack { + imageView() + + descriptionView() + + Spacer() + + priceView() + } + .padding() + .background(Color.black) + } +} + +// MARK: - ViewBuilder + +extension CryptoCellView { + + @ViewBuilder + private func imageView() -> some View { + AsyncImage(url: crypto.imageUrl) { phase in + switch phase { + case .empty: + Image(systemName: "photo") + case .success(let image): + image.resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 50, height: 50) + case .failure(_): + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 50, height: 50) + .foregroundColor(.gray) + @unknown default: + EmptyView() + } + } + } + + @ViewBuilder + private func descriptionView() -> some View { + VStack(alignment: .leading) { + Text(crypto.name) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(Color.white) + Text(crypto.symbol) + .foregroundColor(Color.gray) + } + } + + @ViewBuilder + private func priceView() -> some View { + VStack(alignment: .trailing) { + Text(crypto.currentPrice) + .foregroundColor(Color.white) + Text(processPriceChange(crypto.priceChangePercentage24H)) + .frame(width: priceChangeWidth, alignment: .trailing) + .foregroundColor(crypto.priceChangePercentage24H.contains("-") ? Color.red : Color.green) + .font(.headline) + .padding(3) + .background(crypto.priceChangePercentage24H.contains("-") ? Color.red.opacity(0.2) : Color.green.opacity(0.2)) + .cornerRadius(7) + } + } +} + +// MARK: - Helper + +extension CryptoCellView { + + private func processPriceChange(_ change: String) -> String { + if change.contains("-") { + return change // Already has a "-" sign + } else { + return "+\(change)" // Add the "+" sign for positive changes + } + } + +} + +// MARK: - Preview + +#if DEBUG +#Preview { + return CryptoCellView(crypto: Crypto(id: "bitcoin", currency: "EUR", name: "Bitcoin", symbol: "BTC", imageUrl: URL(string: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png")!, currentPrice: "€30,000", priceChangePercentage24H: "-5000.32%")) + // .previewLayout(.sizeThatFits) +} +#endif diff --git a/1.sdks/0.view/CryptoView/screens/list/CryptoListViewModel.swift b/1.sdks/0.view/CryptoView/screens/list/CryptoListViewModel.swift new file mode 100644 index 0000000..904ea2c --- /dev/null +++ b/1.sdks/0.view/CryptoView/screens/list/CryptoListViewModel.swift @@ -0,0 +1,130 @@ +// +// CryptoListViewModel.swift +// CryptoView +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation +import CryptoCore + +// MARK: - MVI + +struct CryptoListViewState: MVI.State { + var cryptos: [Crypto] = [] + var today: String = "" + var title: String = "" + var isLoading = false + var error: Error? = nil + var selectedCrypto: Crypto? = nil + var toast: (Bool, String?) = (false, nil) +} + +enum CryptoListViewIntent: MVI.Intent { + case fetchTopCryptos + case fetchTodayDate + case fetchTitle + case refreshData +} + +enum CryptoListViewEffect: MVI.Effect { + case navigationToDetails(Crypto) + case showToast(Int) + case showError(Error) +} + +// MARK: - CryptoListViewModel + +final class CryptoListViewModel: ViewModel { + + typealias S = CryptoListViewState + typealias I = CryptoListViewIntent + typealias E = CryptoListViewEffect + + @Published var state = CryptoListViewState() + + private let cryptoListService: CryptoListServiceProtocol + + init(cryptoListService: CryptoListServiceProtocol = CryptoListService()) { + self.cryptoListService = cryptoListService + } +} + +// MARK: - MVI Intent + +extension CryptoListViewModel { + + func dispatch(_ intent: CryptoListViewIntent) { + switch intent { + case .fetchTopCryptos: + fetchTopCryptos() + case .fetchTodayDate: + fetchTodayDate() + case .fetchTitle: + fetchTitle() + case .refreshData: + fetchTopCryptos() + fetchTodayDate() + fetchTitle() + } + } + + private func fetchTopCryptos() { + state.isLoading = true + state.error = nil + state.toast = (false, nil) + + Task { + do { + let cryptos = try await cryptoListService.fetchTopCryptos() + DispatchQueue.main.async { + self.state.isLoading = false + self.state.cryptos = cryptos + } + } catch NetworkError.rateLimited(let retryAfter) { + DispatchQueue.main.async { + self.state.isLoading = false + // Handle showing toast here + self.trigger(.showToast(retryAfter)) + } + } catch { + DispatchQueue.main.async { + self.state.isLoading = false + // Convert other errors to your internal error handling as needed + self.trigger(.showError(error)) + } + } + } + } + + private func fetchTodayDate() { + let today = Date() + let formatter = DateFormatter() + formatter.dateFormat = "d MMMM" + let todayString = formatter.string(from: today) + + state.today = todayString + } + + private func fetchTitle() { + state.title = "Markets" + } + +} + +// MARK: - MVI Effect + +extension CryptoListViewModel { + + func trigger(_ effect: CryptoListViewEffect) { + switch effect { + case .navigationToDetails(let crypto): + state.selectedCrypto = crypto + case .showToast(let retryAfter): + state.toast = (true, "You've hit the rate limit. Please wait \(retryAfter) seconds before retrying.") + case .showError(let error): + state.error = error + } + } + +} diff --git a/1.sdks/0.view/CryptoView/system-handler/CryptoScene.swift b/1.sdks/0.view/CryptoView/system-handler/CryptoScene.swift new file mode 100644 index 0000000..95d17bd --- /dev/null +++ b/1.sdks/0.view/CryptoView/system-handler/CryptoScene.swift @@ -0,0 +1,19 @@ +// +// CryptoScene.swift +// CryptoView +// +// Created by francesco scalise on 19/03/24. +// + +import SwiftUI + +public struct CryptoScene: Scene { + + public init() {} + + public var body: some Scene { + WindowGroup { + CryptoListView(viewModel: CryptoListViewModel()) + } + } +} diff --git a/1.sdks/1.core/CryptoCore.xcodeproj/project.pbxproj b/1.sdks/1.core/CryptoCore.xcodeproj/project.pbxproj index 46c803d..6b1772c 100644 --- a/1.sdks/1.core/CryptoCore.xcodeproj/project.pbxproj +++ b/1.sdks/1.core/CryptoCore.xcodeproj/project.pbxproj @@ -7,14 +7,22 @@ objects = { /* Begin PBXBuildFile section */ + 130B9F8A2BA9D60900225D24 /* CryptoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130B9F892BA9D60900225D24 /* CryptoDetails.swift */; }; + 130B9F8C2BA9D61500225D24 /* CryptoDetailsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130B9F8B2BA9D61500225D24 /* CryptoDetailsService.swift */; }; 130C56842BA48EDF0040DEF9 /* CryptoNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 130C56832BA48EDF0040DEF9 /* CryptoNetwork.framework */; }; 133E7FC92BA48E2B0036F83F /* CryptoCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 133E7FC82BA48E2B0036F83F /* CryptoCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE3BAA2BA94497005F0771 /* CryptoListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3BA92BA94497005F0771 /* CryptoListService.swift */; }; + 13DE3BAC2BA944E0005F0771 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3BAB2BA944E0005F0771 /* Crypto.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 130B9F892BA9D60900225D24 /* CryptoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoDetails.swift; sourceTree = ""; }; + 130B9F8B2BA9D61500225D24 /* CryptoDetailsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoDetailsService.swift; sourceTree = ""; }; 130C56832BA48EDF0040DEF9 /* CryptoNetwork.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoNetwork.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 133E7FC52BA48E2B0036F83F /* CryptoCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CryptoCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 133E7FC82BA48E2B0036F83F /* CryptoCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CryptoCore.h; sourceTree = ""; }; + 13DE3BA92BA94497005F0771 /* CryptoListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoListService.swift; sourceTree = ""; }; + 13DE3BAB2BA944E0005F0771 /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -29,6 +37,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 130B9F882BA9D5F900225D24 /* details */ = { + isa = PBXGroup; + children = ( + 130B9F8B2BA9D61500225D24 /* CryptoDetailsService.swift */, + 130B9F892BA9D60900225D24 /* CryptoDetails.swift */, + ); + path = details; + sourceTree = ""; + }; 130C56822BA48EDF0040DEF9 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -58,10 +75,21 @@ isa = PBXGroup; children = ( 133E7FC82BA48E2B0036F83F /* CryptoCore.h */, + 13DE3BA82BA94477005F0771 /* list */, + 130B9F882BA9D5F900225D24 /* details */, ); path = CryptoCore; sourceTree = ""; }; + 13DE3BA82BA94477005F0771 /* list */ = { + isa = PBXGroup; + children = ( + 13DE3BA92BA94497005F0771 /* CryptoListService.swift */, + 13DE3BAB2BA944E0005F0771 /* Crypto.swift */, + ); + path = list; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -105,6 +133,7 @@ TargetAttributes = { 133E7FC42BA48E2B0036F83F = { CreatedOnToolsVersion = 15.3; + LastSwiftMigration = 1530; }; }; }; @@ -141,6 +170,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13DE3BAA2BA94497005F0771 /* CryptoListService.swift in Sources */, + 130B9F8C2BA9D61500225D24 /* CryptoDetailsService.swift in Sources */, + 13DE3BAC2BA944E0005F0771 /* Crypto.swift in Sources */, + 130B9F8A2BA9D60900225D24 /* CryptoDetails.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -276,6 +309,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -294,7 +328,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoCore; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -303,6 +337,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -312,6 +347,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -330,7 +366,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoCore; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/1.sdks/1.core/CryptoCore/details/CryptoDetails.swift b/1.sdks/1.core/CryptoCore/details/CryptoDetails.swift new file mode 100644 index 0000000..dfe4319 --- /dev/null +++ b/1.sdks/1.core/CryptoCore/details/CryptoDetails.swift @@ -0,0 +1,34 @@ +// +// CryptoDetails.swift +// CryptoCore +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation + +public struct CryptoDetails: Identifiable { + public let id: String + public let currency: String + public let symbol: String + public let name: String + public let description: String + public let homepageURL: URL? + public let historicalPrices: [Double] + public let currentPrice: String + public let priceChangePercentage24H: String + public let imageURL: URL + + public init(id: String, currency: String, symbol: String, name: String, description: String, homepageURL: URL?, historicalPrices: [Double], currentPrice: String, priceChangePercentage24H: String, imageURL: URL) { + self.id = id + self.currency = currency + self.symbol = symbol + self.name = name + self.description = description + self.homepageURL = homepageURL + self.historicalPrices = historicalPrices + self.currentPrice = currentPrice + self.priceChangePercentage24H = priceChangePercentage24H + self.imageURL = imageURL + } +} diff --git a/1.sdks/1.core/CryptoCore/details/CryptoDetailsService.swift b/1.sdks/1.core/CryptoCore/details/CryptoDetailsService.swift new file mode 100644 index 0000000..6a3e049 --- /dev/null +++ b/1.sdks/1.core/CryptoCore/details/CryptoDetailsService.swift @@ -0,0 +1,85 @@ +// +// CryptoDetailsService.swift +// CryptoCore +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation +import CryptoNetwork + +public protocol CryptoDetailsServiceProtocol { + func fetchCryptoDetails(for crypto: Crypto) async throws -> CryptoDetails +} + +public final class CryptoDetailsService: CryptoDetailsServiceProtocol { + private let coinGeckoService: CoinGeckoServiceProtocol + + public init(coinGeckoService: CoinGeckoServiceProtocol = CoinGeckoService()) { + self.coinGeckoService = coinGeckoService + } + + public func fetchCryptoDetails(for crypto: Crypto) async throws -> CryptoDetails { + do { + let id = crypto.id + + let marketDetailsRequest = MarketDetailsRequest(id: id) + let marketDetailsResponse = try await coinGeckoService.fetchMarketDetails(request: marketDetailsRequest) + + let marketChartRequest = MarketChartRequest(id: id) + let marketChartResponse = try await coinGeckoService.fetchMarketChart(request: marketChartRequest) + + return .init(crypto: crypto, marketDetails: marketDetailsResponse, marketChart: marketChartResponse) + + } catch let error as WrongStatusCodeError where error.statusCode == 429 { + // Assuming `WrongStatusCodeError` includes the necessary response details + let retryAfterSeconds = error.response?.value(forHTTPHeaderField: "Retry-After").flatMap(Int.init) ?? 60 // Default retry after 60 seconds + throw NetworkError.rateLimited(retryAfter: retryAfterSeconds) + } catch { + // Re-throw other errors + throw error + } + } +} + +private extension CryptoDetails { + + init(crypto: Crypto, marketDetails: MarketDetailsResponse, marketChart: MarketChartResponse) { + self.id = marketDetails.id + self.currency = crypto.currency + self.symbol = marketDetails.symbol + self.name = marketDetails.name + self.description = marketDetails.description["en"] ?? "" + self.homepageURL = URL(string: marketDetails.links.homepage.first ?? "") + self.historicalPrices = marketChart.prices.map { $0.last ?? 0.0 } + self.currentPrice = crypto.currentPrice + self.priceChangePercentage24H = crypto.priceChangePercentage24H + self.imageURL = marketDetails.image.large + } +} + +#if DEBUG + +public class MockCryptoDetailsService: CryptoDetailsServiceProtocol { + public init() {} + + public func fetchCryptoDetails(for crypto: Crypto) async throws -> CryptoDetails { + let id = crypto.id + let currency = crypto.currency + + return CryptoDetails( + id: id, + currency: currency, + symbol: "BTC", + name: "Bitcoin", + description: "Bitcoin è una criptovaluta inventata nel 2008 da una persona o un gruppo di persone con lo pseudonimo di Satoshi Nakamoto.\nBitcoin è una criptovaluta inventata nel 2008 da una persona o un gruppo di persone con lo pseudonimo di Satoshi Nakamoto.\nBitcoin è una criptovaluta inventata nel 2008 da una persona o un gruppo di persone con lo pseudonimo di Satoshi Nakamoto.", + homepageURL: URL(string: "https://bitcoin.org"), + historicalPrices: [30000.0, 31000.0, 32000.0, 31500.0, 33000.0, 34000.0, 500.0], + currentPrice: crypto.currentPrice, + priceChangePercentage24H: crypto.priceChangePercentage24H, + imageURL: URL(string: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png")! + ) + } +} + +#endif diff --git a/1.sdks/1.core/CryptoCore/list/Crypto.swift b/1.sdks/1.core/CryptoCore/list/Crypto.swift new file mode 100644 index 0000000..795661c --- /dev/null +++ b/1.sdks/1.core/CryptoCore/list/Crypto.swift @@ -0,0 +1,29 @@ +// +// Market.swift +// CryptoCore +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation + +public struct Crypto: Identifiable { + public let id: String + public let currency: String + public let name: String + public let symbol: String + public let imageUrl: URL + public let currentPrice: String + public let priceChangePercentage24H: String + + public init(id: String, currency: String, name: String, symbol: String, imageUrl: URL, currentPrice: String, priceChangePercentage24H: String) { + self.id = id + self.currency = currency + self.name = name + self.symbol = symbol + self.imageUrl = imageUrl + self.currentPrice = currentPrice + self.priceChangePercentage24H = priceChangePercentage24H + } +} + diff --git a/1.sdks/1.core/CryptoCore/list/CryptoListService.swift b/1.sdks/1.core/CryptoCore/list/CryptoListService.swift new file mode 100644 index 0000000..1332235 --- /dev/null +++ b/1.sdks/1.core/CryptoCore/list/CryptoListService.swift @@ -0,0 +1,81 @@ +// +// CryptoListService.swift +// CryptoCore +// +// Created by francesco scalise on 19/03/24. +// + +import Foundation +import CryptoNetwork + +public enum NetworkError: Error { + case rateLimited(retryAfter: Int) +} + +public protocol CryptoListServiceProtocol { + func fetchTopCryptos() async throws -> [Crypto] +} + +public final class CryptoListService: CryptoListServiceProtocol { + private let coinGeckoService: CoinGeckoServiceProtocol + + public init(coinGeckoService: CoinGeckoServiceProtocol = CoinGeckoService()) { + self.coinGeckoService = coinGeckoService + } +} + +extension CryptoListService { + + public func fetchTopCryptos() async throws -> [Crypto] { + do { + let request = MarketsRequest() + let response = try await coinGeckoService.fetchMarkets(request: request) + return response.map(Crypto.init) + } catch let error as WrongStatusCodeError where error.statusCode == 429 { + // Assuming `WrongStatusCodeError` includes the necessary response details + let retryAfterSeconds = error.response?.value(forHTTPHeaderField: "Retry-After").flatMap(Int.init) ?? 60 // Default retry after 60 seconds + throw NetworkError.rateLimited(retryAfter: retryAfterSeconds) + } catch { + // Re-throw other errors + throw error + } + } +} + +private extension Crypto { + init(from market: CryptoNetwork.Market) { + self.id = market.id + self.currency = "EUR" + self.name = market.name + self.symbol = market.symbol.uppercased() + self.imageUrl = market.image + self.currentPrice = Crypto.formatPrice(market.currentPrice) + let priceChangeFormatted = String(format: "%.2f", market.priceChangePercentage24h) + self.priceChangePercentage24H = "\(priceChangeFormatted)%" + } + + private static func formatPrice(_ price: Double) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.groupingSeparator = "." + formatter.decimalSeparator = "," + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + return formatter.string(from: NSNumber(value: price)) ?? "\(price)" + } +} + +#if DEBUG + +public class MockCryptoListService: CryptoListServiceProtocol { + public init() {} + + public func fetchTopCryptos() async throws -> [Crypto] { + return [ + Crypto(id: "bitcoin", currency: "EUR", name: "Bitcoin", symbol: "BTC", imageUrl: URL(string: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png")!, currentPrice: "€30,000", priceChangePercentage24H: "-500.32%"), + Crypto(id: "ethereum", currency: "EUR", name: "Ethereum", symbol: "ETH", imageUrl: URL(string: "https://assets.coingecko.com/coins/images/279/large/ethereum.png")!, currentPrice: "€2,000", priceChangePercentage24H: "300.10%"), + ] + } +} + +#endif diff --git a/1.sdks/2.network/CryptoNetwork.xcodeproj/project.pbxproj b/1.sdks/2.network/CryptoNetwork.xcodeproj/project.pbxproj index 230e179..cc9b6a4 100644 --- a/1.sdks/2.network/CryptoNetwork.xcodeproj/project.pbxproj +++ b/1.sdks/2.network/CryptoNetwork.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 130C56882BA490200040DEF9 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C56872BA490200040DEF9 /* Endpoint.swift */; }; + 133400922BAC72850022678D /* CryptoLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 133400912BAC72850022678D /* CryptoLogger.framework */; }; 13DE3B482BA49255005F0771 /* CryptoUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13DE3B472BA49255005F0771 /* CryptoUtilities.framework */; }; 13DE3B4F2BA494F0005F0771 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3B4E2BA494F0005F0771 /* NetworkSession.swift */; }; 13DE3B532BA49638005F0771 /* CoinGeckoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DE3B522BA49638005F0771 /* CoinGeckoAPI.swift */; }; @@ -35,6 +36,7 @@ /* Begin PBXFileReference section */ 130C56872BA490200040DEF9 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + 133400912BAC72850022678D /* CryptoLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13DE3B472BA49255005F0771 /* CryptoUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13DE3B4E2BA494F0005F0771 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; 13DE3B522BA49638005F0771 /* CoinGeckoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinGeckoAPI.swift; sourceTree = ""; }; @@ -65,6 +67,7 @@ buildActionMask = 2147483647; files = ( 13DE3B482BA49255005F0771 /* CryptoUtilities.framework in Frameworks */, + 133400922BAC72850022678D /* CryptoLogger.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,6 +77,7 @@ 13DE3B462BA49255005F0771 /* Frameworks */ = { isa = PBXGroup; children = ( + 133400912BAC72850022678D /* CryptoLogger.framework */, 13DE3B472BA49255005F0771 /* CryptoUtilities.framework */, ); name = Frameworks; @@ -166,8 +170,8 @@ isa = PBXGroup; children = ( 13E750792BA48CA2003713E9 /* CryptoNetwork.h */, - 13DE3B512BA49606005F0771 /* coin-gecko */, 13DE3B502BA495FD005F0771 /* core */, + 13DE3B512BA49606005F0771 /* coin-gecko */, ); path = CryptoNetwork; sourceTree = ""; @@ -321,7 +325,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoNetworkTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -337,7 +341,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoNetworkTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -495,7 +499,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoNetwork; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.network; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -533,7 +537,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoNetwork; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.network; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketDetailsRequest.swift b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketDetailsRequest.swift index 3dc5c62..aac02a3 100644 --- a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketDetailsRequest.swift +++ b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketDetailsRequest.swift @@ -7,10 +7,12 @@ import Foundation -public struct MarketDetailsRequest { +public struct MarketDetailsRequest: Codable { public let id: String + public let sparkline: Bool - public init(id: String) { + public init(id: String, sparkline: Bool = true) { self.id = id + self.sparkline = sparkline } } diff --git a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketsRequest.swift b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketsRequest.swift index 69b47db..37b94bc 100644 --- a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketsRequest.swift +++ b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/request/MarketsRequest.swift @@ -20,7 +20,7 @@ public struct MarketsRequest: Codable { order: String = "market_cap_desc", perPage: Int = 10, page: Int = 1, - sparkline: Bool = false, + sparkline: Bool = true, locale: String = "en" ) { self.currency = currency diff --git a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketDetailsResponse.swift b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketDetailsResponse.swift index ed81979..ee4ea8d 100644 --- a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketDetailsResponse.swift +++ b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketDetailsResponse.swift @@ -7,19 +7,35 @@ import Foundation +extension MarketDetailsResponse { + public typealias Locale = String +} + public struct MarketDetailsResponse: Codable { public let id: String public let symbol: String public let name: String - public let description: String + public let description: [MarketDetailsResponse.Locale: String] public let links: MarketDetailsResponse.Links public let marketData: MarketDetailsResponse.Data + public let image: MarketDetailsResponse.Image + + public init(id: String, symbol: String, name: String, description: [MarketDetailsResponse.Locale: String], links: MarketDetailsResponse.Links, marketData: MarketDetailsResponse.Data, image: MarketDetailsResponse.Image) { + self.id = id + self.symbol = symbol + self.name = name + self.description = description + self.links = links + self.marketData = marketData + self.image = image + } enum CodingKeys: String, CodingKey { case id, symbol, name case description case links case marketData = "market_data" + case image } } @@ -28,6 +44,11 @@ extension MarketDetailsResponse { public let homepage: [String] public let officialForumUrl: [String] + public init(homepage: [String], officialForumUrl: [String]) { + self.homepage = homepage + self.officialForumUrl = officialForumUrl + } + enum CodingKeys: String, CodingKey { case homepage case officialForumUrl = "official_forum_url" @@ -36,7 +57,12 @@ extension MarketDetailsResponse { public struct Data: Codable { public let currentPrice: [String: Double] - public let historicalData: [HistoricalData] + public let historicalData: HistoricalData + + public init(currentPrice: [String : Double], historicalData: HistoricalData) { + self.currentPrice = currentPrice + self.historicalData = historicalData + } enum CodingKeys: String, CodingKey { case currentPrice = "current_price" @@ -45,7 +71,19 @@ extension MarketDetailsResponse { } public struct HistoricalData: Codable { - public let prices: [[Double]] + public let price: [Double] + + public init(price: [Double]) { + self.price = price + } + } + + public struct Image: Codable { + public let large: URL + + public init(large: URL) { + self.large = large + } } } diff --git a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketsResponse.swift b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketsResponse.swift index 844236d..a536eb5 100644 --- a/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketsResponse.swift +++ b/1.sdks/2.network/CryptoNetwork/coin-gecko/model/response/MarketsResponse.swift @@ -108,3 +108,9 @@ extension Market { } } + +/** + + [{"id":"bitcoin","symbol":"btc","name":"Bitcoin","image":"https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1696501400","current_price":61355,"market_cap":1206208681112,"market_cap_rank":1,"fully_diluted_valuation":1288398570858,"total_volume":55026750582,"high_24h":62316,"low_24h":57185,"price_change_24h":2801.67,"price_change_percentage_24h":4.78481,"market_cap_change_24h":53016049911,"market_cap_change_percentage_24h":4.59733,"circulating_supply":19660362.0,"total_supply":21000000.0,"max_supply":21000000.0,"ath":67405,"ath_change_percentage":-8.59177,"ath_date":"2024-03-14T07:10:36.635Z","atl":51.3,"atl_change_percentage":120009.16396,"atl_date":"2013-07-05T00:00:00.000Z","roi":null,"last_updated":"2024-03-21T13:53:57.620Z"},{"id":"ethereum","symbol":"eth","name":"Ethereum","image":"https://assets.coingecko.com/coins/images/279/large/ethereum.png?1696501628","current_price":3237.5,"market_cap":388810251074,"market_cap_rank":2,"fully_diluted_valuation":388810251074,"total_volume":31922580117,"high_24h":3281.08,"low_24h":2937.98,"price_change_24h":151.16,"price_change_percentage_24h":4.89775,"market_cap_change_24h":17857554648,"market_cap_change_percentage_24h":4.81397,"circulating_supply":120074313.051938,"total_supply":120074313.051938,"max_supply":null,"ath":4228.93,"ath_change_percentage":-23.20537,"ath_date":"2021-12-01T08:38:24.623Z","atl":0.381455,"atl_change_percentage":851268.57902,"atl_date":"2015-10-20T00:00:00.000Z","roi":{"times":69.50651248404263,"currency":"btc","percentage":6950.651248404262},"last_updated":"2024-03-21T13:53:47.248Z"},{"id":"tether","symbol":"usdt","name":"Tether","image":"https://assets.coingecko.com/coins/images/325/large/Tether.png?1696501661","current_price":0.915505,"market_cap":95124675931,"market_cap_rank":3,"fully_diluted_valuation":95124675931,"total_volume":68209867523,"high_24h":0.926241,"low_24h":0.909579,"price_change_24h":-0.006330189074564397,"price_change_percentage_24h":-0.68669,"market_cap_change_24h":-663430892.289856,"market_cap_change_percentage_24h":-0.6926,"circulating_supply":103904029582.796,"total_supply":103904029582.796,"max_supply":null,"ath":1.13,"ath_change_percentage":-18.92552,"ath_date":"2018-07-24T00:00:00.000Z","atl":0.533096,"atl_change_percentage":72.09646,"atl_date":"2015-03-02T00:00:00.000Z","roi":null,"last_updated":"2024-03-21T13:50:33.197Z"},{"id":"binancecoin","symbol":"bnb","name":"BNB","image":"https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png?1696501970","current_price":514.67,"market_cap":79076826302,"market_cap_rank":4,"fully_diluted_valuation":79076826302,"total_volume":2952578527,"high_24h":520.04,"low_24h":464.72,"price_change_24h":25.77,"price_change_percentage_24h":5.27071,"market_cap_change_24h":3771870396,"market_cap_change_percentage_24h":5.00879,"circulating_supply":153856150.0,"total_supply":153856150.0,"max_supply":200000000.0,"ath":586.39,"ath_change_percentage":-12.25733,"ath_date":"2024-03-16T00:10:54.176Z","atl":0.03359941,"atl_change_percentage":1531230.87517,"atl_date":"2017-10-19T00:00:00.000Z","roi":null,"last_updated":"2024-03-21T13:53:51.191Z"},{"id":"solana","symbol":"sol","name":"Solana","image":"https://assets.coingecko.com/coins/images/4128/large/solana.png?1696504756","current_price":172.8,"market_cap":76587848808,"market_cap_rank":5,"fully_diluted_valuation":98723865894,"total_volume":7531247719,"high_24h":177.13,"low_24h":153.77,"price_change_24h":13.98,"price_change_percentage_24h":8.8026,"market_cap_change_24h":6047892681,"market_cap_change_percentage_24h":8.57371,"circulating_supply":443964734.328275,"total_supply":572282882.674173,"max_supply":null,"ath":225.04,"ath_change_percentage":-22.90661,"ath_date":"2021-11-06T21:54:35.825Z","atl":0.46316,"atl_change_percentage":37358.70646,"atl_date":"2020-05-11T19:35:23.449Z","roi":null,"last_updated":"2024-03-21T13:54:10.292Z"},{"id":"staked-ether","symbol":"steth","name":"Lido Staked Ether","image":"https://assets.coingecko.com/coins/images/13442/large/steth_logo.png?1696513206","current_price":3231.93,"market_cap":31641215311,"market_cap_rank":6,"fully_diluted_valuation":31641215311,"total_volume":89646193,"high_24h":3279.98,"low_24h":2926.94,"price_change_24h":152.71,"price_change_percentage_24h":4.95922,"market_cap_change_24h":1530433953,"market_cap_change_percentage_24h":5.08268,"circulating_supply":9783303.36100505,"total_supply":9783303.36100505,"max_supply":null,"ath":4188.52,"ath_change_percentage":-22.40269,"ath_date":"2021-11-12T02:16:02.325Z","atl":394.87,"atl_change_percentage":723.11019,"atl_date":"2020-12-22T04:08:21.854Z","roi":null,"last_updated":"2024-03-21T13:53:30.599Z"},{"id":"ripple","symbol":"xrp","name":"XRP","image":"https://assets.coingecko.com/coins/images/44/large/xrp-symbol-white-128.png?1696501442","current_price":0.567764,"market_cap":31154299239,"market_cap_rank":7,"fully_diluted_valuation":56756703962,"total_volume":2279190142,"high_24h":0.569134,"low_24h":0.525032,"price_change_24h":0.01985164,"price_change_percentage_24h":3.62314,"market_cap_change_24h":1114762093,"market_cap_change_percentage_24h":3.71098,"circulating_supply":54884241878.0,"total_supply":99987762348.0,"max_supply":100000000000.0,"ath":2.82,"ath_change_percentage":-79.96432,"ath_date":"2018-01-07T00:00:00.000Z","atl":0.00194619,"atl_change_percentage":28962.53411,"atl_date":"2013-08-16T00:00:00.000Z","roi":null,"last_updated":"2024-03-21T13:53:55.854Z"},{"id":"usd-coin","symbol":"usdc","name":"USDC","image":"https://assets.coingecko.com/coins/images/6319/large/usdc.png?1696506694","current_price":0.916419,"market_cap":28994711912,"market_cap_rank":8,"fully_diluted_valuation":29015114327,"total_volume":12230927797,"high_24h":0.930143,"low_24h":0.90923,"price_change_24h":-0.005619678626935243,"price_change_percentage_24h":-0.60948,"market_cap_change_24h":163234147,"market_cap_change_percentage_24h":0.56617,"circulating_supply":31641612805.407,"total_supply":31663877738.893,"max_supply":null,"ath":1.059,"ath_change_percentage":-13.57285,"ath_date":"2022-09-27T16:25:08.674Z","atl":0.730265,"atl_change_percentage":25.35686,"atl_date":"2021-05-19T13:14:05.611Z","roi":null,"last_updated":"2024-03-21T13:53:59.155Z"},{"id":"cardano","symbol":"ada","name":"Cardano","image":"https://assets.coingecko.com/coins/images/975/large/cardano.png?1696502090","current_price":0.579139,"market_cap":20382042369,"market_cap_rank":9,"fully_diluted_valuation":26022173618,"total_volume":754409466,"high_24h":0.591756,"low_24h":0.533965,"price_change_24h":0.01954465,"price_change_percentage_24h":3.49265,"market_cap_change_24h":643529685,"market_cap_change_percentage_24h":3.26027,"circulating_supply":35246552423.2316,"total_supply":45000000000.0,"max_supply":45000000000.0,"ath":2.61,"ath_change_percentage":-77.74674,"ath_date":"2021-09-02T06:00:10.474Z","atl":0.01722339,"atl_change_percentage":3267.89552,"atl_date":"2020-03-13T02:22:55.044Z","roi":null,"last_updated":"2024-03-21T13:54:06.359Z"},{"id":"dogecoin","symbol":"doge","name":"Dogecoin","image":"https://assets.coingecko.com/coins/images/5/large/dogecoin.png?1696501409","current_price":0.140279,"market_cap":20119149165,"market_cap_rank":10,"fully_diluted_valuation":20121875369,"total_volume":3062356691,"high_24h":0.14339,"low_24h":0.117526,"price_change_24h":0.01673801,"price_change_percentage_24h":13.54858,"market_cap_change_24h":2354224512,"market_cap_change_percentage_24h":13.25209,"circulating_supply":143539316383.705,"total_supply":143559356383.705,"max_supply":null,"ath":0.601466,"ath_change_percentage":-76.62287,"ath_date":"2021-05-08T05:08:23.458Z","atl":7.662e-05,"atl_change_percentage":183418.73452,"atl_date":"2015-05-06T00:00:00.000Z","roi":null,"last_updated":"2024-03-21T13:54:01.670Z"}] + + */ diff --git a/1.sdks/2.network/CryptoNetwork/coin-gecko/service/CoinGeckoService.swift b/1.sdks/2.network/CryptoNetwork/coin-gecko/service/CoinGeckoService.swift index 0b0219b..34c5421 100644 --- a/1.sdks/2.network/CryptoNetwork/coin-gecko/service/CoinGeckoService.swift +++ b/1.sdks/2.network/CryptoNetwork/coin-gecko/service/CoinGeckoService.swift @@ -46,11 +46,12 @@ extension CoinGeckoService { public func fetchMarketDetails(request: MarketDetailsRequest) async throws -> MarketDetailsResponse { let url = URL(string: "\(CoinGeckoAPI.baseURL)\(CoinGeckoAPICall.coins)/\(request.id)")! + let queryItems = try request.asURLQueryItems(excluding: ["id"]) let endpoint = Endpoint( json: .get, url: url, - queryItems: [] + queryItems: queryItems ) return try await session.load(endpoint) @@ -61,8 +62,8 @@ extension CoinGeckoService { public func fetchMarketChart(request: MarketChartRequest) async throws -> MarketChartResponse { let url = URL(string: "\(CoinGeckoAPI.baseURL)\(CoinGeckoAPICall.coins)/\(request.id)/market_chart")! - let queryItems = try request.asURLQueryItems() - + let queryItems = try request.asURLQueryItems(excluding: ["id"]) + let endpoint = Endpoint( json: .get, url: url, diff --git a/1.sdks/2.network/CryptoNetwork/core/Endpoint.swift b/1.sdks/2.network/CryptoNetwork/core/Endpoint.swift index 996ae4a..65d72e1 100644 --- a/1.sdks/2.network/CryptoNetwork/core/Endpoint.swift +++ b/1.sdks/2.network/CryptoNetwork/core/Endpoint.swift @@ -8,6 +8,7 @@ import Foundation import Combine import CryptoUtilities +import CryptoLogger /// Built-in Content Types public enum ContentType: String { @@ -314,7 +315,7 @@ public struct UnknownError: Error { extension UnknownError: LocalizedError { public var errorDescription: String? { - return "error_unknown".localized + return "unknown error" } } @@ -332,7 +333,7 @@ public struct NoDataError: Error { extension NoDataError: LocalizedError { public var errorDescription: String? { - return "error_network_no_data".localized + return "no data error" } } @@ -357,7 +358,7 @@ public struct WrongStatusCodeError: Error { extension WrongStatusCodeError: LocalizedError { public var errorDescription: String? { - return "error_network_wrong_status_code".localized([statusCode.description, response?.description ?? ""]) + return "wrong status code error - [\(statusCode.description), \(response?.description ?? "")]" } } @@ -368,6 +369,28 @@ extension WrongStatusCodeError: CryptoUtilities.Indexable { } +extension URLRequest { + /// Returns a cURL command for a request + var curlString: String { + guard let url = self.url else { return "" } + var baseCommand = "curl \(url.absoluteString)" + + if self.httpMethod == "POST" { + baseCommand += " -X POST" + } + + for (headerField, headerValue) in self.allHTTPHeaderFields ?? [:] { + baseCommand += " -H \"\(headerField): \(headerValue)\"" + } + + if let httpBody = self.httpBody, let httpBodyString = String(data: httpBody, encoding: .utf8) { + baseCommand += " -d '\(httpBodyString)'" + } + + return baseCommand + } +} + extension URLSession { @discardableResult /// Loads an endpoint by creating (and directly resuming) a data task. @@ -430,10 +453,35 @@ extension URLSession { /// - Returns: The parsed `A` value specified in `Endpoint` public func load(_ e: Endpoint) async throws -> A { let request = e.request + + var message = """ + +--------- + +cURL: +\(request.curlString) +""" let (data, resp) = try await self.data(for: request) guard let h = resp as? HTTPURLResponse else { throw UnknownError() } + + message += """ + +Response HTTP Status code: \(h.statusCode) +""" + if h.mimeType == "application/json" { + message += """ + +Response data: \(String(data: data, encoding: .utf8) ?? "") + +--------- +""" + } + + let logger = CryptoLog() + logger.debug(message) + guard e.expectedStatusCode(h.statusCode) else { throw WrongStatusCodeError(statusCode: h.statusCode, response: h, responseBody: data) } diff --git a/1.sdks/3.utilities/CryptoUtilities.xcodeproj/project.pbxproj b/1.sdks/3.utilities/CryptoUtilities.xcodeproj/project.pbxproj index 9ae1771..a01a38f 100644 --- a/1.sdks/3.utilities/CryptoUtilities.xcodeproj/project.pbxproj +++ b/1.sdks/3.utilities/CryptoUtilities.xcodeproj/project.pbxproj @@ -222,7 +222,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -309,6 +309,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -317,7 +318,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoUtilities; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.utilities; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -343,6 +344,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -351,7 +353,7 @@ MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = com.fs.CryptoUtilities; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.utilities; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/1.sdks/4.logger/CryptoLogger.xcodeproj/project.pbxproj b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5b71cd4 --- /dev/null +++ b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.pbxproj @@ -0,0 +1,370 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 130747EA2934C8EF00CF59BB /* CryptoLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 130747E92934C8EF00CF59BB /* CryptoLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 130747F22934C95400CF59BB /* CryptoLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130747F02934C95400CF59BB /* CryptoLog.swift */; }; + 13BF6FF72BA057260046B057 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13BAB4C62BA0535A0080B1DB /* PrivacyInfo.xcprivacy */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 130747E62934C8EF00CF59BB /* CryptoLogger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CryptoLogger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 130747E92934C8EF00CF59BB /* CryptoLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CryptoLogger.h; sourceTree = ""; }; + 130747F02934C95400CF59BB /* CryptoLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoLog.swift; sourceTree = ""; }; + 13BAB4C62BA0535A0080B1DB /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 130747E32934C8EF00CF59BB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 130747DC2934C8EE00CF59BB = { + isa = PBXGroup; + children = ( + 130747E82934C8EF00CF59BB /* CryptoLoggerSDK */, + 130747E72934C8EF00CF59BB /* Products */, + ); + sourceTree = ""; + }; + 130747E72934C8EF00CF59BB /* Products */ = { + isa = PBXGroup; + children = ( + 130747E62934C8EF00CF59BB /* CryptoLogger.framework */, + ); + name = Products; + sourceTree = ""; + }; + 130747E82934C8EF00CF59BB /* CryptoLoggerSDK */ = { + isa = PBXGroup; + children = ( + 130747E92934C8EF00CF59BB /* CryptoLogger.h */, + 130747F02934C95400CF59BB /* CryptoLog.swift */, + 13BAB4C62BA0535A0080B1DB /* PrivacyInfo.xcprivacy */, + ); + path = CryptoLoggerSDK; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 130747E12934C8EF00CF59BB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 130747EA2934C8EF00CF59BB /* CryptoLogger.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 130747E52934C8EF00CF59BB /* CryptoLogger */ = { + isa = PBXNativeTarget; + buildConfigurationList = 130747ED2934C8EF00CF59BB /* Build configuration list for PBXNativeTarget "CryptoLogger" */; + buildPhases = ( + 130747E12934C8EF00CF59BB /* Headers */, + 130747E22934C8EF00CF59BB /* Sources */, + 130747E32934C8EF00CF59BB /* Frameworks */, + 130747E42934C8EF00CF59BB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CryptoLogger; + productName = CustomLoggerSDK; + productReference = 130747E62934C8EF00CF59BB /* CryptoLogger.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 130747DD2934C8EE00CF59BB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1530; + TargetAttributes = { + 130747E52934C8EF00CF59BB = { + CreatedOnToolsVersion = 14.1; + LastSwiftMigration = 1410; + }; + }; + }; + buildConfigurationList = 130747E02934C8EE00CF59BB /* Build configuration list for PBXProject "CryptoLogger" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 130747DC2934C8EE00CF59BB; + productRefGroup = 130747E72934C8EF00CF59BB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 130747E52934C8EF00CF59BB /* CryptoLogger */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 130747E42934C8EF00CF59BB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13BF6FF72BA057260046B057 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 130747E22934C8EF00CF59BB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 130747F22934C95400CF59BB /* CryptoLog.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 130747EB2934C8EF00CF59BB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 130747EC2934C8EF00CF59BB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 130747EE2934C8EF00CF59BB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.logger; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 130747EF2934C8EF00CF59BB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.fs.crypto.logger; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 130747E02934C8EE00CF59BB /* Build configuration list for PBXProject "CryptoLogger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 130747EB2934C8EF00CF59BB /* Debug */, + 130747EC2934C8EF00CF59BB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 130747ED2934C8EF00CF59BB /* Build configuration list for PBXNativeTarget "CryptoLogger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 130747EE2934C8EF00CF59BB /* Debug */, + 130747EF2934C8EF00CF59BB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 130747DD2934C8EE00CF59BB /* Project object */; +} diff --git a/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/1.sdks/4.logger/CryptoLogger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/1.sdks/4.logger/CryptoLoggerSDK/CryptoLog.swift b/1.sdks/4.logger/CryptoLoggerSDK/CryptoLog.swift new file mode 100644 index 0000000..1f9e32c --- /dev/null +++ b/1.sdks/4.logger/CryptoLoggerSDK/CryptoLog.swift @@ -0,0 +1,229 @@ +// +// OSLogExtension.swift +// CryptoLoggerSDK +// +// Created by francesco scalise on 12/11/21. +// + +import Foundation +@_exported import os.log + +public class CryptoLog { + + public private(set) var oslog: OSLog + + public var logLevel: CustomLogLevel = .debug + public var isSystemLogEnabled: Bool = true + + public init(_ bundle: Bundle = .main, category: String? = nil) { + oslog = OSLog(bundle, category: category) + } + + public init(_ aClass: AnyClass, category: String? = nil) { + oslog = OSLog(aClass, category: category) + } +} + +// MARK: CryptoLogger + Disable + +public extension CryptoLog { + + func disable() { + oslog = OSLog.disabled + } +} + +// MARK: CryptoLogger + CustomLogLevel + +public extension CryptoLog { + + @inlinable func debug(_ message: String) { + guard logLevel >= .debug else { return } + + oslog.debug(message) + } + + @inlinable func info(_ message: String) { + guard logLevel >= .info else { return } + + oslog.info(message) + } + + @inlinable func error(_ message: String) { + guard logLevel >= .error else { return } + + oslog.error(message) + } + + @inlinable func fault(_ message: String) { + guard logLevel >= .fault else { return } + + oslog.fault(message) + } + + func print(_ value: @autoclosure () -> Any) { + guard isSystemLogEnabled else { return } + + oslog.print(value()) + } + + func dump(_ value: @autoclosure () -> Any) { + guard isSystemLogEnabled else { return } + + oslog.dump(value()) + } + + func trace() { + guard isSystemLogEnabled else { return } + + oslog.trace() + } + +} + +// MARK: OSLog + CustomLogLevel + +extension OSLog { + + convenience init(_ bundle: Bundle = .main, category: String? = nil) { + self.init(subsystem: bundle.bundleIdentifier ?? "com.intesigroup.logging.system", category: category ?? "") + } + + convenience init(_ aClass: AnyClass, category: String? = nil) { + self.init(Bundle(for: aClass), category: category ?? String(describing: aClass)) + } + +} + +extension OSLog { + // @inlinable func log(_ message: String) { + // log("%@", message) + // } + + @inlinable func debug(_ message: String) { +#if DEBUG + let m = CustomLogLevel.debug.rawValue + " " + message + debug("%{public}@", m) +#else + debug("%{private}@", message) +#endif + } + + @inlinable func info(_ message: String) { +#if DEBUG + let m = CustomLogLevel.info.rawValue + " " + message +#else + let m = message +#endif + info("%{public}@", m) + } + + @inlinable func error(_ message: String) { +#if DEBUG + let m = CustomLogLevel.error.rawValue + " " + message +#else + let m = message +#endif + error("%{public}@", m) + } + + @inlinable func fault(_ message: String) { +#if DEBUG + let m = CustomLogLevel.fault.rawValue + " " + message + let icon = "🚨" +#else + let m = message + let icon = "" +#endif + fault("%{public}@", m) + fault("%{public}@%{public}@", icon, Thread.callStackSymbols) + } + + func print(_ value: @autoclosure () -> Any) { +#if DEBUG + guard isEnabled(type: .debug) else { return } + os_log("%{public}@", log: self, type: .debug, String(describing: value())) +#endif + } + + func dump(_ value: @autoclosure () -> Any) { +#if DEBUG + guard isEnabled(type: .debug) else { return } + var string = String() + Swift.dump(value(), to: &string) + os_log("%{public}@", log: self, type: .debug, string) +#endif + } + + func trace(file: StaticString = #file, function: StaticString = #function, line: UInt = #line) { +#if DEBUG + guard isEnabled(type: .debug) else { return } + let file = URL(fileURLWithPath: String(describing: file)).deletingPathExtension().lastPathComponent + var function = String(describing: function) + function.removeSubrange(function.firstIndex(of: "(")!...function.lastIndex(of: ")")!) + os_log("%{public}@.%{public}@():%ld", log: self, type: .debug, file, function, line) +#endif + } + +} + +extension OSLog { + + // @inlinable func log(_ message: StaticString, _ args: CVarArg...) { + // log(message, type: .default, args) + // } + + @usableFromInline func debug(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .debug, args) + } + + @usableFromInline func info(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .info, args) + } + + @usableFromInline func error(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .error, args) + } + + @usableFromInline func fault(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .fault, args) + } + + @usableFromInline func log(_ message: StaticString, type: OSLogType, _ a: [CVarArg]) { + // The Swift overlay of os_log prevents from accepting an unbounded number of args + // http://www.openradar.me/33203955 + assert(a.count <= 5) + switch a.count { + case 5: os_log(message, log: self, type: type, a[0], a[1], a[2], a[3], a[4]) + case 4: os_log(message, log: self, type: type, a[0], a[1], a[2], a[3]) + case 3: os_log(message, log: self, type: type, a[0], a[1], a[2]) + case 2: os_log(message, log: self, type: type, a[0], a[1]) + case 1: os_log(message, log: self, type: type, a[0]) + default: os_log(message, log: self, type: type) + } + } + + +} + +public enum CustomLogLevel: Comparable { + case fault + case error + case info + case debug + + public var rawValue: String { + switch self { + case .fault: + return "🚨 FAULT" + case .error: + return "❌ ERROR" + case .info: + return "ℹ️ INFO" + case .debug: + return "🔬 DEBUG" + } + } +} + + diff --git a/1.sdks/4.logger/CryptoLoggerSDK/CryptoLogger.h b/1.sdks/4.logger/CryptoLoggerSDK/CryptoLogger.h new file mode 100644 index 0000000..4bd0ce1 --- /dev/null +++ b/1.sdks/4.logger/CryptoLoggerSDK/CryptoLogger.h @@ -0,0 +1,18 @@ +// +// CryptoLoggerSDK.h +// CryptoLoggerSDK +// +// Created by francesco scalise on 28/11/22. +// + +#import + +//! Project version number for CryptoLoggerSDK. +FOUNDATION_EXPORT double CryptoLoggerVersionNumber; + +//! Project version string for CryptoLoggerSDK. +FOUNDATION_EXPORT const unsigned char CryptoLoggerVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/1.sdks/4.logger/CryptoLoggerSDK/PrivacyInfo.xcprivacy b/1.sdks/4.logger/CryptoLoggerSDK/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..c8290d0 --- /dev/null +++ b/1.sdks/4.logger/CryptoLoggerSDK/PrivacyInfo.xcprivacy @@ -0,0 +1,28 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + NSPrivacyTrackingDomains + + + diff --git a/EncodableExtension.swift b/EncodableExtension.swift index 18d9f11..eb031c6 100644 --- a/EncodableExtension.swift +++ b/EncodableExtension.swift @@ -7,17 +7,28 @@ import Foundation -public extension Encodable { - func asDictionary() throws -> [String: Any] { +extension Encodable { + public func asDictionary(excluding keys: [String] = []) throws -> [String: Any] { let data = try JSONEncoder().encode(self) - let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) - return dictionary as? [String: Any] ?? [:] + var dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] ?? [:] + + keys.forEach { dictionary.removeValue(forKey: $0) } + + return dictionary } - func asURLQueryItems() throws -> [URLQueryItem] { - let dictionary = try self.asDictionary() - return dictionary.map { key, value in - URLQueryItem(name: key, value: "\(value)") + public func asURLQueryItems(excluding keys: [String] = []) throws -> [URLQueryItem] { + let dictionary = try self.asDictionary(excluding: keys) + return dictionary.compactMap { key, value in + + let valueString: String + if let boolValue = value as? Bool { + valueString = boolValue ? "true" : "false" + } else { + valueString = "\(value)" + } + + return URLQueryItem(name: key, value: valueString) } } } diff --git a/crypto-pulse.xcworkspace/contents.xcworkspacedata b/crypto-pulse.xcworkspace/contents.xcworkspacedata index bb5bbd9..37440fd 100644 --- a/crypto-pulse.xcworkspace/contents.xcworkspacedata +++ b/crypto-pulse.xcworkspace/contents.xcworkspacedata @@ -16,4 +16,7 @@ + +