diff --git a/RickAndMorty.xcodeproj/project.pbxproj b/RickAndMorty.xcodeproj/project.pbxproj index f663c4e..a857420 100644 --- a/RickAndMorty.xcodeproj/project.pbxproj +++ b/RickAndMorty.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 30010EE628DE83FB00AF9383 /* EpisodeListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30010EE528DE83FB00AF9383 /* EpisodeListViewModel.swift */; }; + 30010EE828DE878400AF9383 /* TvMazeEpisode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30010EE728DE878400AF9383 /* TvMazeEpisode.swift */; }; + 30023A4928DE764800A5CD2C /* CharacterDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30023A4828DE764800A5CD2C /* CharacterDetail.swift */; }; 304C07B828C26E710025529B /* RickAndMortyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304C07B728C26E710025529B /* RickAndMortyApp.swift */; }; 304C07BA28C26E710025529B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304C07B928C26E710025529B /* ContentView.swift */; }; 304C07BC28C26E740025529B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 304C07BB28C26E740025529B /* Assets.xcassets */; }; @@ -14,6 +17,27 @@ 304C07C928C26E740025529B /* RickAndMortyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304C07C828C26E740025529B /* RickAndMortyTests.swift */; }; 304C07D328C26E740025529B /* RickAndMortyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304C07D228C26E740025529B /* RickAndMortyUITests.swift */; }; 304C07D528C26E740025529B /* RickAndMortyUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304C07D428C26E740025529B /* RickAndMortyUITestsLaunchTests.swift */; }; + 3052053D28DA7FD0001F99F7 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052053C28DA7FD0001F99F7 /* Api.swift */; }; + 3052054028DA84B9001F99F7 /* CharacterListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052053F28DA84B9001F99F7 /* CharacterListViewModel.swift */; }; + 3052054228DA858D001F99F7 /* PaginatedIndexEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052054128DA858D001F99F7 /* PaginatedIndexEndpoint.swift */; }; + 3052054428DB70AE001F99F7 /* SquareImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052054328DB70AE001F99F7 /* SquareImage.swift */; }; + 305E707A28DE6735004AD856 /* Checksum.m in Sources */ = {isa = PBXBuildFile; fileRef = 305E707928DE6735004AD856 /* Checksum.m */; }; + 3095385628E7B22B004673C9 /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3095385528E7B22B004673C9 /* SettingsStore.swift */; }; + 30E0FA3B28D7974C001696EB /* Amaca in Frameworks */ = {isa = PBXBuildFile; productRef = 30E0FA3A28D7974C001696EB /* Amaca */; }; + 30ED036028DE687D005948ED /* StorageBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED035D28DE687D005948ED /* StorageBucket.swift */; }; + 30ED036128DE687D005948ED /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED035E28DE687D005948ED /* DataCache.swift */; }; + 30ED036228DE687D005948ED /* StorageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED035F28DE687D005948ED /* StorageType.swift */; }; + 30ED036428DE68BC005948ED /* CachedAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED036328DE68BC005948ED /* CachedAsyncImage.swift */; }; + 30ED036628DE68DB005948ED /* CachedAsyncImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED036528DE68DB005948ED /* CachedAsyncImageViewModel.swift */; }; + 30F4518028E7A9AF0030E9ED /* CharacterListFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F4517F28E7A9AF0030E9ED /* CharacterListFilterView.swift */; }; + 30F87C1028CBF19000157848 /* CharacterListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C0F28CBF19000157848 /* CharacterListView.swift */; }; + 30F87C1228CBF1B900157848 /* LocationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1128CBF1B900157848 /* LocationListView.swift */; }; + 30F87C1428CBF1E700157848 /* EpisodeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1328CBF1E700157848 /* EpisodeListView.swift */; }; + 30F87C1728CBF58900157848 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1628CBF58900157848 /* Character.swift */; }; + 30F87C1928CBF85000157848 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1828CBF85000157848 /* Location.swift */; }; + 30F87C1B28CBF98700157848 /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1A28CBF98700157848 /* Episode.swift */; }; + 30F87C1D28CBF9FE00157848 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1C28CBF9FE00157848 /* TestData.swift */; }; + 30F87C1F28CC028400157848 /* CharacterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F87C1E28CC028400157848 /* CharacterRowView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,6 +58,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 30010EE528DE83FB00AF9383 /* EpisodeListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListViewModel.swift; sourceTree = ""; }; + 30010EE728DE878400AF9383 /* TvMazeEpisode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TvMazeEpisode.swift; sourceTree = ""; }; + 30023A4828DE764800A5CD2C /* CharacterDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterDetail.swift; sourceTree = ""; }; 304C07B428C26E710025529B /* RickAndMorty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RickAndMorty.app; sourceTree = BUILT_PRODUCTS_DIR; }; 304C07B728C26E710025529B /* RickAndMortyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyApp.swift; sourceTree = ""; }; 304C07B928C26E710025529B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -44,6 +71,28 @@ 304C07CE28C26E740025529B /* RickAndMortyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RickAndMortyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 304C07D228C26E740025529B /* RickAndMortyUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyUITests.swift; sourceTree = ""; }; 304C07D428C26E740025529B /* RickAndMortyUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyUITestsLaunchTests.swift; sourceTree = ""; }; + 3052053C28DA7FD0001F99F7 /* Api.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Api.swift; sourceTree = ""; }; + 3052053F28DA84B9001F99F7 /* CharacterListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListViewModel.swift; sourceTree = ""; }; + 3052054128DA858D001F99F7 /* PaginatedIndexEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedIndexEndpoint.swift; sourceTree = ""; }; + 3052054328DB70AE001F99F7 /* SquareImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImage.swift; sourceTree = ""; }; + 305E707828DE6734004AD856 /* RickAndMorty-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RickAndMorty-Bridging-Header.h"; sourceTree = ""; }; + 305E707928DE6735004AD856 /* Checksum.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Checksum.m; sourceTree = ""; }; + 305E707B28DE6760004AD856 /* Checksum.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Checksum.h; sourceTree = ""; }; + 3095385528E7B22B004673C9 /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = ""; }; + 30ED035D28DE687D005948ED /* StorageBucket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageBucket.swift; sourceTree = ""; }; + 30ED035E28DE687D005948ED /* DataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = ""; }; + 30ED035F28DE687D005948ED /* StorageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageType.swift; sourceTree = ""; }; + 30ED036328DE68BC005948ED /* CachedAsyncImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedAsyncImage.swift; sourceTree = ""; }; + 30ED036528DE68DB005948ED /* CachedAsyncImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedAsyncImageViewModel.swift; sourceTree = ""; }; + 30F4517F28E7A9AF0030E9ED /* CharacterListFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListFilterView.swift; sourceTree = ""; }; + 30F87C0F28CBF19000157848 /* CharacterListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListView.swift; sourceTree = ""; }; + 30F87C1128CBF1B900157848 /* LocationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationListView.swift; sourceTree = ""; }; + 30F87C1328CBF1E700157848 /* EpisodeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListView.swift; sourceTree = ""; }; + 30F87C1628CBF58900157848 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; + 30F87C1828CBF85000157848 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; + 30F87C1A28CBF98700157848 /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = ""; }; + 30F87C1C28CBF9FE00157848 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; + 30F87C1E28CC028400157848 /* CharacterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRowView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,6 +100,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 30E0FA3B28D7974C001696EB /* Amaca in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,6 +121,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 30010EE328DE83C600AF9383 /* Episodes */ = { + isa = PBXGroup; + children = ( + 30F87C1328CBF1E700157848 /* EpisodeListView.swift */, + ); + path = Episodes; + sourceTree = ""; + }; + 30023A4728DE762400A5CD2C /* Characters */ = { + isa = PBXGroup; + children = ( + 30F87C0F28CBF19000157848 /* CharacterListView.swift */, + 30F87C1E28CC028400157848 /* CharacterRowView.swift */, + 30023A4828DE764800A5CD2C /* CharacterDetail.swift */, + 30F4517F28E7A9AF0030E9ED /* CharacterListFilterView.swift */, + ); + path = Characters; + sourceTree = ""; + }; 304C07AB28C26E710025529B = { isa = PBXGroup; children = ( @@ -94,10 +163,19 @@ 304C07B628C26E710025529B /* RickAndMorty */ = { isa = PBXGroup; children = ( + 30ED035C28DE681F005948ED /* Storage */, + 30F87C1528CBF57700157848 /* Models */, + 3052053E28DA8463001F99F7 /* ViewModels */, + 30F87C0E28CBF17900157848 /* Views */, + 3052053B28DA7FBB001F99F7 /* Utils */, 304C07B728C26E710025529B /* RickAndMortyApp.swift */, 304C07B928C26E710025529B /* ContentView.swift */, 304C07BB28C26E740025529B /* Assets.xcassets */, 304C07BD28C26E740025529B /* Preview Content */, + 30F87C1C28CBF9FE00157848 /* TestData.swift */, + 305E707B28DE6760004AD856 /* Checksum.h */, + 305E707928DE6735004AD856 /* Checksum.m */, + 305E707828DE6734004AD856 /* RickAndMorty-Bridging-Header.h */, ); path = RickAndMorty; sourceTree = ""; @@ -127,6 +205,59 @@ path = RickAndMortyUITests; sourceTree = ""; }; + 3052053B28DA7FBB001F99F7 /* Utils */ = { + isa = PBXGroup; + children = ( + 3052053C28DA7FD0001F99F7 /* Api.swift */, + 3052054128DA858D001F99F7 /* PaginatedIndexEndpoint.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 3052053E28DA8463001F99F7 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 3052053F28DA84B9001F99F7 /* CharacterListViewModel.swift */, + 30ED036528DE68DB005948ED /* CachedAsyncImageViewModel.swift */, + 30010EE528DE83FB00AF9383 /* EpisodeListViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 30ED035C28DE681F005948ED /* Storage */ = { + isa = PBXGroup; + children = ( + 30ED035E28DE687D005948ED /* DataCache.swift */, + 30ED035D28DE687D005948ED /* StorageBucket.swift */, + 30ED035F28DE687D005948ED /* StorageType.swift */, + 3095385528E7B22B004673C9 /* SettingsStore.swift */, + ); + path = Storage; + sourceTree = ""; + }; + 30F87C0E28CBF17900157848 /* Views */ = { + isa = PBXGroup; + children = ( + 30010EE328DE83C600AF9383 /* Episodes */, + 30023A4728DE762400A5CD2C /* Characters */, + 30ED036328DE68BC005948ED /* CachedAsyncImage.swift */, + 30F87C1128CBF1B900157848 /* LocationListView.swift */, + 3052054328DB70AE001F99F7 /* SquareImage.swift */, + ); + path = Views; + sourceTree = ""; + }; + 30F87C1528CBF57700157848 /* Models */ = { + isa = PBXGroup; + children = ( + 30F87C1628CBF58900157848 /* Character.swift */, + 30F87C1828CBF85000157848 /* Location.swift */, + 30F87C1A28CBF98700157848 /* Episode.swift */, + 30010EE728DE878400AF9383 /* TvMazeEpisode.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -143,6 +274,9 @@ dependencies = ( ); name = RickAndMorty; + packageProductDependencies = ( + 30E0FA3A28D7974C001696EB /* Amaca */, + ); productName = RickAndMorty; productReference = 304C07B428C26E710025529B /* RickAndMorty.app */; productType = "com.apple.product-type.application"; @@ -195,6 +329,7 @@ TargetAttributes = { 304C07B328C26E710025529B = { CreatedOnToolsVersion = 13.4.1; + LastSwiftMigration = 1400; }; 304C07C328C26E740025529B = { CreatedOnToolsVersion = 13.4.1; @@ -215,6 +350,9 @@ Base, ); mainGroup = 304C07AB28C26E710025529B; + packageReferences = ( + 30E0FA3928D7974C001696EB /* XCRemoteSwiftPackageReference "Amaca" */, + ); productRefGroup = 304C07B528C26E710025529B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -257,8 +395,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3052054428DB70AE001F99F7 /* SquareImage.swift in Sources */, + 30ED036028DE687D005948ED /* StorageBucket.swift in Sources */, + 30F87C1D28CBF9FE00157848 /* TestData.swift in Sources */, + 30ED036628DE68DB005948ED /* CachedAsyncImageViewModel.swift in Sources */, + 30ED036128DE687D005948ED /* DataCache.swift in Sources */, + 30F87C1028CBF19000157848 /* CharacterListView.swift in Sources */, + 30F87C1B28CBF98700157848 /* Episode.swift in Sources */, + 30F87C1228CBF1B900157848 /* LocationListView.swift in Sources */, + 30010EE628DE83FB00AF9383 /* EpisodeListViewModel.swift in Sources */, + 3052054028DA84B9001F99F7 /* CharacterListViewModel.swift in Sources */, + 30023A4928DE764800A5CD2C /* CharacterDetail.swift in Sources */, + 30ED036428DE68BC005948ED /* CachedAsyncImage.swift in Sources */, + 30F87C1428CBF1E700157848 /* EpisodeListView.swift in Sources */, + 30ED036228DE687D005948ED /* StorageType.swift in Sources */, + 3052053D28DA7FD0001F99F7 /* Api.swift in Sources */, + 3095385628E7B22B004673C9 /* SettingsStore.swift in Sources */, + 30F87C1728CBF58900157848 /* Character.swift in Sources */, 304C07BA28C26E710025529B /* ContentView.swift in Sources */, + 3052054228DA858D001F99F7 /* PaginatedIndexEndpoint.swift in Sources */, + 30F4518028E7A9AF0030E9ED /* CharacterListFilterView.swift in Sources */, 304C07B828C26E710025529B /* RickAndMortyApp.swift in Sources */, + 30010EE828DE878400AF9383 /* TvMazeEpisode.swift in Sources */, + 30F87C1928CBF85000157848 /* Location.swift in Sources */, + 305E707A28DE6735004AD856 /* Checksum.m in Sources */, + 30F87C1F28CC028400157848 /* CharacterRowView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -414,6 +575,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"RickAndMorty/Preview Content\""; @@ -433,8 +595,10 @@ PRODUCT_BUNDLE_IDENTIFIER = com.3zcurdia.RickAndMorty; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "RickAndMorty/RickAndMorty-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -443,6 +607,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"RickAndMorty/Preview Content\""; @@ -462,8 +627,9 @@ PRODUCT_BUNDLE_IDENTIFIER = com.3zcurdia.RickAndMorty; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "RickAndMorty/RickAndMorty-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -583,6 +749,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 30E0FA3928D7974C001696EB /* XCRemoteSwiftPackageReference "Amaca" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/3zcurdia/Amaca"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 30E0FA3A28D7974C001696EB /* Amaca */ = { + isa = XCSwiftPackageProductDependency; + package = 30E0FA3928D7974C001696EB /* XCRemoteSwiftPackageReference "Amaca" */; + productName = Amaca; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 304C07AC28C26E710025529B /* Project object */; } diff --git a/RickAndMorty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RickAndMorty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..113e4c4 --- /dev/null +++ b/RickAndMorty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "amaca", + "kind" : "remoteSourceControl", + "location" : "https://github.com/3zcurdia/Amaca", + "state" : { + "branch" : "main", + "revision" : "b89e23fe622037a15fa5f5fbe6788178a8eb79cb" + } + } + ], + "version" : 2 +} diff --git a/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/ezcurdia.xcuserdatad/UserInterfaceState.xcuserstate b/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/ezcurdia.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..f22ff57 Binary files /dev/null and b/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/ezcurdia.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/samo92.xcuserdatad/UserInterfaceState.xcuserstate b/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/samo92.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..8aba463 Binary files /dev/null and b/RickAndMorty.xcodeproj/project.xcworkspace/xcuserdata/samo92.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/RickAndMorty.xcodeproj/xcshareddata/xcschemes/RickAndMorty.xcscheme b/RickAndMorty.xcodeproj/xcshareddata/xcschemes/RickAndMorty.xcscheme new file mode 100644 index 0000000..1ac706a --- /dev/null +++ b/RickAndMorty.xcodeproj/xcshareddata/xcschemes/RickAndMorty.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RickAndMorty.xcodeproj/xcuserdata/ezcurdia.xcuserdatad/xcschemes/xcschememanagement.plist b/RickAndMorty.xcodeproj/xcuserdata/ezcurdia.xcuserdatad/xcschemes/xcschememanagement.plist index 397acba..c734238 100644 --- a/RickAndMorty.xcodeproj/xcuserdata/ezcurdia.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/RickAndMorty.xcodeproj/xcuserdata/ezcurdia.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,23 @@ 0 + SuppressBuildableAutocreation + + 304C07B328C26E710025529B + + primary + + + 304C07C328C26E740025529B + + primary + + + 304C07CD28C26E740025529B + + primary + + + diff --git a/RickAndMorty/Assets.xcassets/AccentColor.colorset/Contents.json b/RickAndMorty/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..8a4063b 100644 --- a/RickAndMorty/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/RickAndMorty/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x71", + "green" : "0x37", + "red" : "0x33" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x40", + "green" : "0xC1", + "red" : "0x82" + } + }, "idiom" : "universal" } ], diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png new file mode 100644 index 0000000..6704342 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x 1.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x 1.png new file mode 100644 index 0000000..b5a4c97 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x 1.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png new file mode 100644 index 0000000..b5a4c97 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png new file mode 100644 index 0000000..dc6e50a Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png new file mode 100644 index 0000000..86d82e1 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x 1.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x 1.png new file mode 100644 index 0000000..8f49ff3 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x 1.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png new file mode 100644 index 0000000..8f49ff3 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png new file mode 100644 index 0000000..e978a60 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png new file mode 100644 index 0000000..b5a4c97 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x 1.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x 1.png new file mode 100644 index 0000000..8ed372b Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x 1.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png new file mode 100644 index 0000000..8ed372b Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png new file mode 100644 index 0000000..a4d080c Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png new file mode 100644 index 0000000..a4d080c Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png new file mode 100644 index 0000000..7f60d7f Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Contents.json b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Contents.json index 5a3257a..cd56ab2 100644 --- a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,86 +1,109 @@ { "images" : [ { + "filename" : "AppIcon-20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "AppIcon-20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "AppIcon-29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "AppIcon-29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "AppIcon-40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "AppIcon-40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "AppIcon-60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "AppIcon-60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "AppIcon-20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "AppIcon-20@2x 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "AppIcon-29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "AppIcon-29@2x 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "AppIcon-40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "AppIcon-40@2x 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "Icon-Small-83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "iTunesArtwork@2x.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 0000000..aef0500 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 0000000..d48d634 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-Small-83.5@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-Small-83.5@2x.png new file mode 100644 index 0000000..b9c8157 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/Icon-Small-83.5@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png new file mode 100644 index 0000000..3cc1c1d Binary files /dev/null and b/RickAndMorty/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png differ diff --git a/RickAndMorty/Assets.xcassets/BabyPownder.colorset/Contents.json b/RickAndMorty/Assets.xcassets/BabyPownder.colorset/Contents.json new file mode 100644 index 0000000..80d1823 --- /dev/null +++ b/RickAndMorty/Assets.xcassets/BabyPownder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF4", + "green" : "0xF7", + "red" : "0xF3" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RickAndMorty/Assets.xcassets/GreenPigment.colorset/Contents.json b/RickAndMorty/Assets.xcassets/GreenPigment.colorset/Contents.json new file mode 100644 index 0000000..f6cbeee --- /dev/null +++ b/RickAndMorty/Assets.xcassets/GreenPigment.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0xA5", + "red" : "0x4C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RickAndMorty/Assets.xcassets/MidnightBlue.colorset/Contents.json b/RickAndMorty/Assets.xcassets/MidnightBlue.colorset/Contents.json new file mode 100644 index 0000000..6bde98f --- /dev/null +++ b/RickAndMorty/Assets.xcassets/MidnightBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x66", + "green" : "0x1F", + "red" : "0x1F" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RickAndMorty/Assets.xcassets/SteelBlue.colorset/Contents.json b/RickAndMorty/Assets.xcassets/SteelBlue.colorset/Contents.json new file mode 100644 index 0000000..c911da8 --- /dev/null +++ b/RickAndMorty/Assets.xcassets/SteelBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA4", + "green" : "0x80", + "red" : "0x5A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RickAndMorty/Assets.xcassets/character-placeholder.imageset/Contents.json b/RickAndMorty/Assets.xcassets/character-placeholder.imageset/Contents.json new file mode 100644 index 0000000..ea5fe17 --- /dev/null +++ b/RickAndMorty/Assets.xcassets/character-placeholder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "character.jpeg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RickAndMorty/Assets.xcassets/character-placeholder.imageset/character.jpeg b/RickAndMorty/Assets.xcassets/character-placeholder.imageset/character.jpeg new file mode 100644 index 0000000..66d4bf5 Binary files /dev/null and b/RickAndMorty/Assets.xcassets/character-placeholder.imageset/character.jpeg differ diff --git a/RickAndMorty/Checksum.h b/RickAndMorty/Checksum.h new file mode 100644 index 0000000..c30a840 --- /dev/null +++ b/RickAndMorty/Checksum.h @@ -0,0 +1,17 @@ +// +// Checksum.h +// RickAndMorty +// +// Created by Luis Ezcurdia on 23/09/22. +// + +#ifndef Checksum_h +#define Checksum_h +#import +#import + +@interface Checksum : NSObject ++(NSString *) sha1: (NSString *)input; ++(NSString *) sha256: (NSString *)input; +@end +#endif /* Checksum_h */ diff --git a/RickAndMorty/Checksum.m b/RickAndMorty/Checksum.m new file mode 100644 index 0000000..17cf639 --- /dev/null +++ b/RickAndMorty/Checksum.m @@ -0,0 +1,40 @@ +// +// Checksum.m +// RickAndMorty +// +// Created by Luis Ezcurdia on 23/09/22. +// + +#import "Checksum.h" + +@implementation Checksum +- (instancetype)init { + if (self = [super init]) { + } + return self; +} + ++(NSString *) sha1: (NSString *)input { + const char* str = [input UTF8String]; + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1(str, (CC_LONG) strlen(str), result); + + NSMutableString *ret = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH*2]; + for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { + [ret appendFormat:@"%02x",result[i]]; + } + return ret; +} + ++(NSString *) sha256: (NSString *)input { + const char* str = [input UTF8String]; + unsigned char result[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(str, (CC_LONG) strlen(str), result); + + NSMutableString *ret = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH*2]; + for(int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { + [ret appendFormat:@"%02x",result[i]]; + } + return ret; +} +@end diff --git a/RickAndMorty/ContentView.swift b/RickAndMorty/ContentView.swift index f6d8738..eba003d 100644 --- a/RickAndMorty/ContentView.swift +++ b/RickAndMorty/ContentView.swift @@ -8,22 +8,34 @@ import SwiftUI struct ContentView: View { + @EnvironmentObject var settingsStore: SettingsStore var body: some View { - VStack{ - ZStack { - Image(systemName: "circle") - .font(.system(size: 72, weight: .light)) - Image(systemName: "bus") - .font(.system(size: 40)) - }.foregroundColor(.blue) - Text("Hello SwiftUI") - .font(.system(size: 24, weight: .bold)) + TabView { + CharacterListView() + .environmentObject(settingsStore) + .tabItem { + Image(systemName: "mustache") + Text("Characters") + } + .tag(1) + EpisodeListView() + .tabItem { + Image(systemName: "film") + Text("Episodes") + } + .tag(2) + LocationListView() + .tabItem { + Image(systemName: "map") + Text("Locations") + } + .tag(3) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + ContentView().environmentObject(SettingsStore()) } } diff --git a/RickAndMorty/Models/Character.swift b/RickAndMorty/Models/Character.swift new file mode 100644 index 0000000..15eb7ca --- /dev/null +++ b/RickAndMorty/Models/Character.swift @@ -0,0 +1,74 @@ +// +// Character.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import Foundation + +struct UrlLocation: Codable { + let name: String? + let url: String? +} + +enum Gender: String, Codable, CaseIterable { + case all = "All" + case male = "Male" + case female = "Female" + case unknown = "unknown" + + func text() -> String { + return rawValue.capitalized + } +} + +struct Character: Codable, Identifiable { + enum Status: String, Codable, CaseIterable { + case all = "All" + case alive = "Alive" + case dead = "Dead" + case unknown = "unknown" + + func text() -> String { + return rawValue.capitalized + } + } + var id: Int + let name: String + let imageUrlString: String + let status: Status + let species: String + let gender: Gender + let type: String + let location: UrlLocation? + let origin: UrlLocation? + let episodes: [String] + let url: String +// let created: String + + + func imageUrl() -> URL? { + return URL(string: imageUrlString) + } + + private enum CodingKeys: String, CodingKey { + case id + case name + case imageUrlString = "image" + case status + case species + case gender + case type + case episodes = "episode" + case location + case origin + case url + } +} + +extension Character: Equatable { + static func == (lhs: Character, rhs: Character) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/RickAndMorty/Models/Episode.swift b/RickAndMorty/Models/Episode.swift new file mode 100644 index 0000000..219ded7 --- /dev/null +++ b/RickAndMorty/Models/Episode.swift @@ -0,0 +1,24 @@ +// +// Episode.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import Foundation + +struct Episode: Codable, Identifiable { + let id: Int + let name: String + let airDate: String + let episode: String + var imageUrlString: String? +// let created: String +// let characters: [String] +} + +extension Episode: Equatable { + static func == (lhs: Episode, rhs: Episode) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/RickAndMorty/Models/Location.swift b/RickAndMorty/Models/Location.swift new file mode 100644 index 0000000..edcb1ae --- /dev/null +++ b/RickAndMorty/Models/Location.swift @@ -0,0 +1,17 @@ +// +// Location.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import Foundation + +struct Location: Codable, Identifiable { + let id: Int + let name: String +// let residents: [String] + let dimension: String + let type: String +// let created: String +} diff --git a/RickAndMorty/Models/TvMazeEpisode.swift b/RickAndMorty/Models/TvMazeEpisode.swift new file mode 100644 index 0000000..480b1cb --- /dev/null +++ b/RickAndMorty/Models/TvMazeEpisode.swift @@ -0,0 +1,19 @@ +// +// TvMazeEpisode.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 23/09/22. +// + +import Foundation + +struct TvMazeEpisode: Codable, Identifiable { + struct Image: Codable { + let medium: String? + let original: String? + } + let id: Int + let season: Int + let number: Int + let image: Image? +} diff --git a/RickAndMorty/RickAndMorty-Bridging-Header.h b/RickAndMorty/RickAndMorty-Bridging-Header.h new file mode 100644 index 0000000..fd2d375 --- /dev/null +++ b/RickAndMorty/RickAndMorty-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "Checksum.m" diff --git a/RickAndMorty/RickAndMortyApp.swift b/RickAndMorty/RickAndMortyApp.swift index 234db35..8a799db 100644 --- a/RickAndMorty/RickAndMortyApp.swift +++ b/RickAndMorty/RickAndMortyApp.swift @@ -9,9 +9,10 @@ import SwiftUI @main struct RickAndMortyApp: App { + var settingsStore = SettingsStore() var body: some Scene { WindowGroup { - ContentView() + ContentView().environmentObject(settingsStore) } } } diff --git a/RickAndMorty/Storage/DataCache.swift b/RickAndMorty/Storage/DataCache.swift new file mode 100644 index 0000000..f7dfa20 --- /dev/null +++ b/RickAndMorty/Storage/DataCache.swift @@ -0,0 +1,80 @@ +// +// DataCache.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 20/09/22. +// + +import Foundation +import SwiftUI + +protocol DataCacheManager { + func read(key: String) -> Data? + func write(key: String, data: Data) +} + +extension DataCacheManager { + subscript(index: String) -> Data? { + get { + read(key: index) + } + set(newValue) { + guard let data = newValue else { return } + write(key: index, data: data) + } + } +} + +struct DataCache: DataCacheManager { + static let shared = DataCache() + let memCache = DataMemoryCache() + let dskCache = DataDiskCache() + + func read(key: String) -> Data? { + if let cached = memCache.read(key: key) { + return cached + } else if let cached = dskCache.read(key: key) { + memCache.write(key: key, data: cached) + return cached + } else { + return nil + } + } + + func write(key: String, data: Data) { + memCache.write(key: key, data: data) + dskCache.write(key: key, data: data) + } +} + +struct DataMemoryCache: DataCacheManager { + typealias DataCacheType = NSCache + static let shared = DataMemoryCache() + var memcache: DataCacheType = { + let cache = DataCacheType() + cache.countLimit = 100 + cache.totalCostLimit = 1024 * 1024 * 100 // 100Mb + return cache + }() + + func read(key: String) -> Data? { + return memcache.object(forKey: key as NSString) as Data? + } + + func write(key: String, data: Data) { + memcache.setObject(data as NSData, forKey: key as NSString) + } +} + +struct DataDiskCache: DataCacheManager { + static let shared = DataDiskCache() + let bucket = StorageBucket(type: .cache) + + func read(key: String) -> Data? { + return bucket.read(key) + } + + func write(key: String, data: Data) { + _ = bucket.writeUnlessExist(key, data: data) + } +} diff --git a/RickAndMorty/Storage/SettingsStore.swift b/RickAndMorty/Storage/SettingsStore.swift new file mode 100644 index 0000000..cc6804b --- /dev/null +++ b/RickAndMorty/Storage/SettingsStore.swift @@ -0,0 +1,37 @@ +// +// SettingsStore.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 30/09/22. +// + +import Foundation +import Combine + +class SettingsStore: ObservableObject { + enum CharacerFilter: String { + case status = "view.characterListFilter.selectedStatus" + case onlyHuman = "view.characterListFilter.onlyHuman" + } + init() { + UserDefaults.standard.register(defaults: [ + CharacerFilter.status.rawValue: "All", + CharacerFilter.onlyHuman.rawValue: false, + ]) + } + + @Published + var characterFilterSelectedStatus: String = UserDefaults.standard.string(forKey: CharacerFilter.status.rawValue) ?? "All" { + didSet { + UserDefaults.standard.set(characterFilterSelectedStatus, forKey: CharacerFilter.status.rawValue) + } + } + + @Published + var characterFilterOnlyHuman: Bool = UserDefaults.standard.bool(forKey: CharacerFilter.onlyHuman.rawValue) { + didSet { + UserDefaults.standard.set(characterFilterOnlyHuman, forKey: CharacerFilter.onlyHuman.rawValue) + } + } + +} diff --git a/RickAndMorty/Storage/StorageBucket.swift b/RickAndMorty/Storage/StorageBucket.swift new file mode 100644 index 0000000..481fc9c --- /dev/null +++ b/RickAndMorty/Storage/StorageBucket.swift @@ -0,0 +1,50 @@ +// +// StorageBucket.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 04/09/22. +// + +import Foundation + +struct StorageBucket { + let baseUrl: URL + + init(type: StorageType) { + type.ensureExists() + baseUrl = type.url + } + + func remove(_ filename: String) { + try? FileManager.default.removeItem(at: url(for: filename)) + } + + func read(_ filename: String) -> Data? { + return try? Data(contentsOf: url(for: filename)) + } + + func writeUnlessExist(_ filename: String, data: Data) -> Bool { + if FileManager.default.fileExists(atPath: url(for: filename).path) { + return true + } + return write(filename, data: data) + } + + func write(_ filename: String, data: Data) -> Bool { + do { + try data.write(to: url(for: filename)) + return true + } catch let err { + #if DEBUG + debugPrint(err) + #endif + return false + } + } + + private func url(for filename: String) -> URL { + var url = baseUrl + url.appendPathComponent(filename) + return url + } +} diff --git a/RickAndMorty/Storage/StorageType.swift b/RickAndMorty/Storage/StorageType.swift new file mode 100644 index 0000000..f888d2c --- /dev/null +++ b/RickAndMorty/Storage/StorageType.swift @@ -0,0 +1,60 @@ +// +// StorageType.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 04/09/22. +// + +import Foundation + +enum StorageType { + static func ensureExist() { + cache.ensureExists() + application.ensureExists() + user.ensureExists() + } + + case cache + case application + case user + + var pathDirectory: FileManager.SearchPathDirectory { + switch self { + case .cache: + return .cachesDirectory + default: + return .documentDirectory + } + } + + var domainMask: FileManager.SearchPathDomainMask { + switch self { + case .cache: + return .localDomainMask + case .application: + return .localDomainMask + case .user: + return .userDomainMask + } + } + + var url: URL { + var url = FileManager.default.urls(for: pathDirectory, in: domainMask).first! + let applicationPath = "com.3zcurdia.plasticfishes" + url.appendPathComponent(applicationPath) + return url + } + + var path: String { + return url.path + } + + func ensureExists() { + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) { + if isDir.boolValue { return } + try? FileManager.default.removeItem(at: url) + } + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) + } +} diff --git a/RickAndMorty/TestData.swift b/RickAndMorty/TestData.swift new file mode 100644 index 0000000..c55921c --- /dev/null +++ b/RickAndMorty/TestData.swift @@ -0,0 +1,93 @@ +// +// TestData.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import Foundation + +#if DEBUG +struct TestData { + let characters = [ + Character( + id: 1, + name: "Rick Sanchez", + imageUrlString: "https://rickandmortyapi.com/api/character/avatar/1.jpeg", + status: Character.Status.alive, + species: "Human", + gender: .male, + type: "lorem ipsum dolor", + location: UrlLocation(name: "Location", url: "https://example.com/something.png"), + origin: UrlLocation(name: "Location", url: "https://example.com/something.png"), + episodes: [""], + url: "" + ), + Character( + id: 2, + name: "Morty Smith", + imageUrlString: "https://rickandmortyapi.com/api/character/avatar/2.jpeg", + status: Character.Status.alive, + species: "Human", + gender: .male, + type: "", + location: UrlLocation(name: "Location", url: "https://example.com/something.png"), + origin: UrlLocation(name: "Location", url: "https://example.com/something.png"), + episodes: [""], + url: "" + ) + ] + let locations = [ + Location( + id: 1, + name: "Earth (C-137)", + dimension: "Dimension C-137", + type: "Planet" + ), + Location( + id: 2, + name: "Abadango", + dimension: "unknown", + type: "Cluster" + ), + Location( + id: 3, + name: "Citadel of Ricks", + dimension: "unknown", + type: "Space station" + ), + Location( + id: 4, + name: "Worldender's lair", + dimension: "unknown", + type: "Planet" + ) + ] + let episodes = [ + Episode( + id: 1, + name: "Pilot", + airDate: "December 2, 2013", + episode: "S01E01" + ), + Episode( + id: 2, + name: "Lawnmower Dog", + airDate: "December 9, 2013", + episode: "S01E02" + ), + Episode( + id: 3, + name: "Anatomy Park", + airDate: "December 16, 2013", + episode: "S01E03" + ), + Episode( + id: 4, + name: "M. Night Shaym-Aliens!", + airDate: "January 13, 2014", + episode: "S01E04" + ) + ] +} +#endif diff --git a/RickAndMorty/Utils/Api.swift b/RickAndMorty/Utils/Api.swift new file mode 100644 index 0000000..08e0eef --- /dev/null +++ b/RickAndMorty/Utils/Api.swift @@ -0,0 +1,13 @@ +// +// Api.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 20/09/22. +// + +import Foundation + +struct Api { + static let rickAndMorty = "https://rickandmortyapi.com/" + static let tvMaze = "https://api.tvmaze.com/" +} diff --git a/RickAndMorty/Utils/PaginatedIndexEndpoint.swift b/RickAndMorty/Utils/PaginatedIndexEndpoint.swift new file mode 100644 index 0000000..6e70bf6 --- /dev/null +++ b/RickAndMorty/Utils/PaginatedIndexEndpoint.swift @@ -0,0 +1,49 @@ +// +// PaginatedIndexEndpoint.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 20/09/22. +// + +import Foundation +import Amaca + +struct PaginatedList: Codable { + struct Info: Codable { + let count: Int + let pages: Int + } + let info: Info + let results: [T] +} + +struct PaginatedListEndpoint where T: Codable { + let client: Amaca.Client + let route: String + let decoder: JSONDecoder = { + var dec = JSONDecoder() + dec.keyDecodingStrategy = .convertFromSnakeCase + return dec + }() + + init(client: Amaca.Client, route: String) { + self.client = client + self.route = route + } + + func show(page: Int? = nil) async throws -> PaginatedList? { + let data = try await client.get(path: route, queryItems: ["page": "\(page ?? 1)"]) + guard let data = data else { return nil } + + do { + let json = try decoder.decode(PaginatedList.self, from: data) + return json + } catch let err { + #if DEBUG + debugPrint(err) + debugPrint(String(data: data, encoding: .utf8) ?? "") + #endif + throw Amaca.EndpointError.invalidDecoding("Unable to decode response") + } + } +} diff --git a/RickAndMorty/ViewModels/CachedAsyncImageViewModel.swift b/RickAndMorty/ViewModels/CachedAsyncImageViewModel.swift new file mode 100644 index 0000000..62c9bda --- /dev/null +++ b/RickAndMorty/ViewModels/CachedAsyncImageViewModel.swift @@ -0,0 +1,70 @@ +// +// CachedAsyncImageViewModel.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 19/09/22. +// + +import Foundation +import SwiftUI +import Combine + +class CachedAsyncImageViewModel: ObservableObject { + enum CachedAsyncImageError: Error { + case loading(String) + } + + @Published var phase: AsyncImagePhase + + private let url: URL? + private var manager: DataCacheManager = DataCache.shared + private lazy var urlHash: String = { + guard let url = url else { return "" } + return Checksum.sha1(url.absoluteString) + }() + var cancellableSet = Set() + + init(url: URL?) { + self.url = url + self.phase = .empty + load() + } + + private func load() { + guard let url = url else { return } + if let data = manager[urlHash], + let image = image(from: data) { + self.phase = .success(image) + return + } + + URLSession + .shared + .dataTaskPublisher(for: url) + .map(\.data) + .receive(on: RunLoop.main) + .sink { + print("Completed: \($0)") + } receiveValue: { data in + if let image = self.image(from: data) { + self.phase = .success(image) + self.manager[self.urlHash] = data + } else { + self.phase = .failure(CachedAsyncImageError.loading("Unable to load image")) + } + } + .store(in: &cancellableSet) + } + + private func image(from data: Data) -> Image? { +#if os(macOS) + guard let nsImage = NSImage(data: data) else { return nil } + + return Image(nsImage: nsImage) +#else + guard let uiImage = UIImage(data: data) else { return nil } + + return Image(uiImage: uiImage) +#endif + } +} diff --git a/RickAndMorty/ViewModels/CharacterListViewModel.swift b/RickAndMorty/ViewModels/CharacterListViewModel.swift new file mode 100644 index 0000000..8d0ac6e --- /dev/null +++ b/RickAndMorty/ViewModels/CharacterListViewModel.swift @@ -0,0 +1,31 @@ +// +// CharacterListViewModel.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 20/09/22. +// + +import Foundation +import Amaca + +@MainActor +class CharacterListViewModel: ObservableObject { + @Published var characters: [Character] = [] + var currentPage = 1 + let apiClient = Amaca.Client(Api.rickAndMorty) + lazy var endpoint: PaginatedListEndpoint = { + return PaginatedListEndpoint(client: apiClient, route: "/api/character") + }() + + func load() async { + guard let response = try? await endpoint.show(page: currentPage) else { return } + self.characters.append(contentsOf: response.results) + self.currentPage += 1 + } + + func isLast(character: Character) -> Bool { + if self.characters.isEmpty { return false } + + return self.characters.last == character + } +} diff --git a/RickAndMorty/ViewModels/EpisodeListViewModel.swift b/RickAndMorty/ViewModels/EpisodeListViewModel.swift new file mode 100644 index 0000000..c1ad71f --- /dev/null +++ b/RickAndMorty/ViewModels/EpisodeListViewModel.swift @@ -0,0 +1,48 @@ +// +// EpisodeListViewModel.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 23/09/22. +// + +import Foundation +import Amaca + +@MainActor +class EpisodeListViewModel: ObservableObject { + @Published var episodes: [Episode] = [] + @Published var tvMazeEpisodes: [TvMazeEpisode] = [] + var currentPage = 1 + let apiClient = Amaca.Client(Api.rickAndMorty) + lazy var endpoint: PaginatedListEndpoint = { + return PaginatedListEndpoint(client: apiClient, route: "/api/episode") + }() + + let tvMazeEndpoint = Amaca.Endpoint(client: Amaca.Client(Api.tvMaze), route: "/shows/216/episodes") + + func load() async { + guard let response = try? await endpoint.show(page: currentPage) else { return } + self.episodes.append(contentsOf: response.results) + self.currentPage += 1 + } + + func loadTvMaze() async { + guard let tvMazeEpisodes = try? await tvMazeEndpoint.show() else { return } + + //let enco = JSONEncoder() + //let jsonData = try! enco.encode(tvMazeEpisodes.first?.image) + //debugPrint( String(data: jsonData, encoding: String.Encoding.utf8) ?? "fail" ) + + //tvMazeEpisodes.forEach{ tv in + // debugPrint(tv.image?.medium ?? "fail") + //} + + self.tvMazeEpisodes.append(contentsOf: tvMazeEpisodes) + } + + func isLast(episode: Episode) -> Bool { + if self.episodes.isEmpty { return false } + + return self.episodes.last == episode + } +} diff --git a/RickAndMorty/Views/CachedAsyncImage.swift b/RickAndMorty/Views/CachedAsyncImage.swift new file mode 100644 index 0000000..0a5a9aa --- /dev/null +++ b/RickAndMorty/Views/CachedAsyncImage.swift @@ -0,0 +1,40 @@ +// +// CachedAsyncImage.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 19/09/22. +// + +import SwiftUI + +struct CachedAsyncImage: View where Content: View { + @ObservedObject var viewModel: CachedAsyncImageViewModel + private let content: (AsyncImagePhase) -> Content + + init( + url: URL?, + @ViewBuilder content: @escaping (AsyncImagePhase) -> Content + ) { + _viewModel = ObservedObject(wrappedValue: CachedAsyncImageViewModel(url: url)) + self.content = content + } + + var body: some View { + content(viewModel.phase) + } +} + +struct CachedAsyncImage_Previews: PreviewProvider { + static var previews: some View { + CachedAsyncImage(url: URL(string: "https://rickandmortyapi.com/api/character/avatar/20.jpeg")! ) { phase in + switch phase { + case .empty: + Image("character-placeholder") + case .success(let image): + image + default: + fatalError() + } + } + } +} diff --git a/RickAndMorty/Views/Characters/CharacterDetail.swift b/RickAndMorty/Views/Characters/CharacterDetail.swift new file mode 100644 index 0000000..490fea3 --- /dev/null +++ b/RickAndMorty/Views/Characters/CharacterDetail.swift @@ -0,0 +1,97 @@ +// +// CharacterDetail.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 23/09/22. +// + +import SwiftUI + +struct CharacterDetail: View { + @Environment(\.dismiss) var dismiss + let character: Character + var body: some View { + ScrollView { + CachedAsyncImage(url: character.imageUrl()) { phase in + switch phase { + case .empty: + Color("SteelBlue") + .opacity(0.3) + .frame(height: 350) + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(height: 350) + .clipped() + default: + Image(systemName: "xmark.icloud") + .resizable() + .scaledToFit() + .frame(height: 350) + .clipped() + } + } + VStack(alignment: .leading) { + Text(character.name) + .font(.system(size: 32, weight: .bold, design: .rounded)) + if !character.type.isEmpty { + Text(character.type) + .font(.system(size: 12, weight: .light, design: .monospaced)) + .italic() + } + HStack { + Text(character.status.rawValue) + Circle() + .frame(width: 15) + .foregroundColor(statusColor(character.status)) + Text("\(character.species) - \(character.gender.text())") + Spacer() + } + .font(.title2) + HStack{ + Text("Origin") + .fontWeight(.bold) + Text("\(character.origin?.name ?? "unkown")") + } + HStack{ + Text("Location") + .fontWeight(.bold) + Text("\(character.location?.name ?? "unkown")") + } + HStack{ + Text("Episodes") + .fontWeight(.bold) + Text("\(character.episodes.count)") + } + }.padding() + } + .ignoresSafeArea(.all, edges: .top) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left.circle.fill") + .font(.largeTitle) + .foregroundColor(Color("BabyPownder")) + } + } + } + } + + func statusColor(_ status: Character.Status) -> Color { + switch status { + case .alive: return Color("GreenPigment") + case .dead: return .black + default: return Color("SteelBlue") + } + } +} + +struct CharacterDetail_Previews: PreviewProvider { + static var previews: some View { + CharacterDetail(character: TestData().characters[0]) + } +} diff --git a/RickAndMorty/Views/Characters/CharacterListFilterView.swift b/RickAndMorty/Views/Characters/CharacterListFilterView.swift new file mode 100644 index 0000000..5c82b69 --- /dev/null +++ b/RickAndMorty/Views/Characters/CharacterListFilterView.swift @@ -0,0 +1,66 @@ +// +// CharacterListFilterView.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 30/09/22. +// + +import SwiftUI + +struct CharacterListFilterView: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var settingsStore: SettingsStore + @State var selectedStatus: Character.Status = .all + @State var onlyHuman: Bool = false + + var body: some View { + NavigationView { + Form { + Section(header: Text("Properties")) { + Picker("Status", selection: $selectedStatus) { + ForEach(Character.Status.allCases, id: \.self) { status in + Text(status.text()) + } + } + } + Section(header: Text("Species")) { + Toggle(isOn: $onlyHuman) { + Text("Only Humans") + } + } + } + .navigationBarTitle("Charaters Filter") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Text("Cancel") + .foregroundColor(.accentColor) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.settingsStore.characterFilterSelectedStatus = self.selectedStatus.rawValue + self.settingsStore.characterFilterOnlyHuman = self.onlyHuman + dismiss() + } label: { + Text("Filter") + .foregroundColor(.accentColor) + } + } + } + .onAppear { + self.selectedStatus = Character.Status(rawValue: self.settingsStore.characterFilterSelectedStatus) ?? .all + self.onlyHuman = self.settingsStore.characterFilterOnlyHuman + } + } + + } +} + +struct CharacterListFilterView_Previews: PreviewProvider { + static var previews: some View { + CharacterListFilterView().environmentObject(SettingsStore()) + } +} diff --git a/RickAndMorty/Views/Characters/CharacterListView.swift b/RickAndMorty/Views/Characters/CharacterListView.swift new file mode 100644 index 0000000..f6e6420 --- /dev/null +++ b/RickAndMorty/Views/Characters/CharacterListView.swift @@ -0,0 +1,91 @@ +// +// CharacterListView.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import SwiftUI + +struct CharacterListView: View { + @EnvironmentObject var settingsStore: SettingsStore + @StateObject var viewModel = CharacterListViewModel() + @State var showFilter: Bool = false + + private var data: [Int] = Array(1...20) + private let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + + var body: some View { + NavigationView { + ScrollView{ + LazyVGrid(columns: columns) { + ForEach(viewModel.characters) { item in + NavigationLink{ + CharacterDetail(character: item) + } label: { + CharacterRowView(character: item) + .listRowSeparator(.hidden) + .onAppear { + if viewModel.isLast(character: item) { + Task { await viewModel.load() } + } + } + } + + + } + } + } + .navigationTitle("Characters") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.showFilter = true + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .font(.title2) + .foregroundColor(.accentColor) + } + + } + }.sheet(isPresented: $showFilter) { + CharacterListFilterView().environmentObject(settingsStore) + } + }.task { + await viewModel.load() + } + } + + func shouldShowCharacter(character: Character) -> Bool { + return shouldShowCharacterStatus(character: character) && shouldShowHumanCharacter(character: character) + } + + func shouldShowCharacterStatus(character: Character) -> Bool { + let selectedStatus = Character.Status(rawValue: settingsStore.characterFilterSelectedStatus) + if selectedStatus == .all { + return true + } else { + return character.status == selectedStatus + } + } + + func shouldShowHumanCharacter(character: Character) ->Bool { + if settingsStore.characterFilterOnlyHuman { + return character.species == "Human" + } else { + return true + } + + } +} + +struct CharacterListView_Previews: PreviewProvider { + static var previews: some View { + CharacterListView().environmentObject(SettingsStore()) + } +} diff --git a/RickAndMorty/Views/Characters/CharacterRowView.swift b/RickAndMorty/Views/Characters/CharacterRowView.swift new file mode 100644 index 0000000..b7adbef --- /dev/null +++ b/RickAndMorty/Views/Characters/CharacterRowView.swift @@ -0,0 +1,60 @@ +// +// CharacterRowView.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import SwiftUI + +struct CharacterRowView: View { + let character: Character + var body: some View { + VStack { + ZStack{ + CachedAsyncImage(url: character.imageUrl()) { phase in + switch phase { + case .empty: + SquareImage(image: Image("character-placeholder"), size: 120, contentMode: .fill) + .cornerRadius(5) + case .success(let image): + SquareImage(image: image, size: 120, contentMode: .fill) + .cornerRadius(20) + + default: + SquareImage(image: Image(systemName: "xmark.icloud"), size: 120, contentMode: .fit) + } + } + statusColor(character.status).opacity(0.7).cornerRadius(20) + .frame(width: 120, height: 120) + } + Text(character.name) + .font(.headline) + .fontWeight(.bold) + .lineLimit(1) + + Text(character.species) + .font(.subheadline) + + Text(character.origin?.name ?? "Unkown") + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(1) + + } + } + + func statusColor(_ status: Character.Status) -> Color { + switch status { + case .dead: return .gray + default: return .white.opacity(0) + } + } + +} + +struct CharacterRowView_Previews: PreviewProvider { + static var previews: some View { + CharacterRowView(character: TestData().characters[0]) + } +} diff --git a/RickAndMorty/Views/Episodes/EpisodeListView.swift b/RickAndMorty/Views/Episodes/EpisodeListView.swift new file mode 100644 index 0000000..341dd21 --- /dev/null +++ b/RickAndMorty/Views/Episodes/EpisodeListView.swift @@ -0,0 +1,79 @@ +// +// EpisodeListView.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import SwiftUI + +struct EpisodeListView: View { + @StateObject var viewModel = EpisodeListViewModel() + + var body: some View { + NavigationView { + List(viewModel.episodes) { episode in + VStack(alignment: .leading) { + Text(episode.name) + .font(.headline) + Text(episode.episode) + .font(.subheadline) + + ScrollView(.horizontal) { + HStack (spacing: 20) { + ForEach(viewModel.tvMazeEpisodes.suffix(10)) { tv in + EpisodeImage(urlImg: tv.image?.original ?? "" ) + } + } + } +// ScrollView(.horizontal) { +// HStack (spacing: 20){ +// ForEach( +// 0...10, +// id: \.self +// ) {_ in +// EpisodeImage(urlImg: "https://www.unionjalisco.mx/wp-content/uploads/2021/05/cintura_ariadne_4.jpg") +// +// } +// } +// } + } + } + .listStyle(.plain) + .navigationTitle("Episodes") + } + .task { + await viewModel.load() + await viewModel.loadTvMaze() + } + } +} + +struct EpisodeListView_Previews: PreviewProvider { + static var previews: some View { + EpisodeListView() + } +} + +struct EpisodeImage: View { + let urlImg: String + + var body: some View { + + debugPrint(urlImg) + let urlObject = URL(string: urlImg) + + return CachedAsyncImage(url: urlObject) { phase in + switch phase { + case .empty: + SquareImage(image: Image("character-placeholder"), size: 120, contentMode: .fill) + .cornerRadius(5) + case .success(let image): + SquareImage(image: image, size: 120, contentMode: .fill) + .cornerRadius(5) + default: + SquareImage(image: Image(systemName: "xmark.icloud"), size: 120, contentMode: .fit) + } + } + } +} diff --git a/RickAndMorty/Views/LocationListView.swift b/RickAndMorty/Views/LocationListView.swift new file mode 100644 index 0000000..5c7da27 --- /dev/null +++ b/RickAndMorty/Views/LocationListView.swift @@ -0,0 +1,20 @@ +// +// LocationListView.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 09/09/22. +// + +import SwiftUI + +struct LocationListView: View { + var body: some View { + Text("Locations") + } +} + +struct LocationListView_Previews: PreviewProvider { + static var previews: some View { + LocationListView() + } +} diff --git a/RickAndMorty/Views/SquareImage.swift b/RickAndMorty/Views/SquareImage.swift new file mode 100644 index 0000000..9462cfa --- /dev/null +++ b/RickAndMorty/Views/SquareImage.swift @@ -0,0 +1,32 @@ +// +// SquareImage.swift +// RickAndMorty +// +// Created by Luis Ezcurdia on 21/09/22. +// + +import SwiftUI + +struct SquareImage: View { + let image: Image + let size: CGFloat + let contentMode: ContentMode + + var body: some View { + image + .resizable() + .aspectRatio(contentMode: contentMode) + .frame(width: size, height: size) + .clipped() + } +} + +struct SquareImage_Previews: PreviewProvider { + static var previews: some View { + SquareImage( + image: Image(systemName: "xmark.icloud"), + size: 120, + contentMode: .fit + ) + } +}