From 5bf373a62530136810b89e3c4561b172aa45a61b Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Fri, 13 Aug 2021 11:03:51 +0100 Subject: [PATCH 1/5] Built viewModel and cardState factory ViewModel provides characterCardStates --- Rick-and-Morty/CharacterRepository.swift | 2 +- .../Rick And Morty.xcodeproj/project.pbxproj | 24 +++++++++++++ .../Factories/CharacterCardStateFactory.swift | 35 +++++++++++++++++++ .../Rick And Morty/Model/Character.swift | 2 +- .../ViewModels/CharacterListViewModel.swift | 29 +++++++++++++++ .../Rick And Morty/Views/CharacterCard.swift | 12 +++---- .../Views/CharacterListView.swift | 5 +++ 7 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift create mode 100644 Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift diff --git a/Rick-and-Morty/CharacterRepository.swift b/Rick-and-Morty/CharacterRepository.swift index 183e722..8ce7431 100644 --- a/Rick-and-Morty/CharacterRepository.swift +++ b/Rick-and-Morty/CharacterRepository.swift @@ -11,7 +11,7 @@ final class CharacterRepository: CharacterRepositoryProtocol { func getCharacters(completion: @escaping (([Character]) -> Void)) { if let url = characterPageURL { - rickAndMortyService.fetchData(url: url) { (charactersResponse: CharactersResponse) in + rickAndMortyService.fetchData(url: url) { (charactersResponse: CharacterResponse) in if let nextURLString = charactersResponse.info.next { if let nextURL = URL(string: nextURLString) { self.characterPageURL = nextURL diff --git a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj index f0b34cc..2df73f3 100644 --- a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj +++ b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B064A26C6611D0080ADD0 /* RickAndMortyService.swift */; }; 174B064D26C661470080ADD0 /* RickAndMortyServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B064C26C661470080ADD0 /* RickAndMortyServiceProtocol.swift */; }; 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065026C671580080ADD0 /* CharacterRepository.swift */; }; + 174B065426C6740E0080ADD0 /* CharacterListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */; }; + 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */; }; 17588BAC26C1750B008ECC31 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAB26C1750B008ECC31 /* Character.swift */; }; 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAE26C273BB008ECC31 /* CharacterCard.swift */; }; B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B811686C1CFF1C9900301A0A /* AppDelegate.swift */; }; @@ -34,6 +36,8 @@ 174B064A26C6611D0080ADD0 /* RickAndMortyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyService.swift; sourceTree = ""; }; 174B064C26C661470080ADD0 /* RickAndMortyServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyServiceProtocol.swift; sourceTree = ""; }; 174B065026C671580080ADD0 /* CharacterRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRepository.swift; sourceTree = ""; }; + 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListViewModel.swift; sourceTree = ""; }; + 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCardStateFactory.swift; sourceTree = ""; }; 17588BAB26C1750B008ECC31 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; 17588BAE26C273BB008ECC31 /* CharacterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCard.swift; sourceTree = ""; }; B81168691CFF1C9900301A0A /* Rick And Morty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rick And Morty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -81,6 +85,22 @@ name = Repositories; sourceTree = ""; }; + 174B065226C673FB0080ADD0 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 174B065526C674FD0080ADD0 /* Factories */ = { + isa = PBXGroup; + children = ( + 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */, + ); + path = Factories; + sourceTree = ""; + }; 17588BAA26C174FB008ECC31 /* Model */ = { isa = PBXGroup; children = ( @@ -120,6 +140,8 @@ B811686B1CFF1C9900301A0A /* Rick And Morty */ = { isa = PBXGroup; children = ( + 174B065526C674FD0080ADD0 /* Factories */, + 174B065226C673FB0080ADD0 /* ViewModels */, 174B064926C660FF0080ADD0 /* Services */, 17588BAD26C273A2008ECC31 /* Views */, 17588BAA26C174FB008ECC31 /* Model */, @@ -247,7 +269,9 @@ 174B064D26C661470080ADD0 /* RickAndMortyServiceProtocol.swift in Sources */, 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */, 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */, + 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */, 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */, + 174B065426C6740E0080ADD0 /* CharacterListViewModel.swift in Sources */, 17588BAC26C1750B008ECC31 /* Character.swift in Sources */, 1711B39E26B1898100BE935B /* CharacterListView.swift in Sources */, B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */, diff --git a/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift new file mode 100644 index 0000000..83c6a4d --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift @@ -0,0 +1,35 @@ +// +// CharacterCardStateFactory.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import Foundation + +final class CharacterCardStateFactory { + private func getIsAlive(character: Character) -> Bool { + if character.status == "Alive" { + return true + } + + return false + } + + private func getFirstEpisode(character: Character) -> String? { + if let firstEpisode = character.episodeURLs.first { + return firstEpisode + } + + return nil + } + + func createCharacterCardState(from character: Character) -> CharacterCardState { + let isAlive = getIsAlive(character: character) + let firstEpisode = getFirstEpisode(character: character) ?? "Unknown" + let characterCardState = CharacterCardState(id: character.id, name: character.name, imageURL: character.imageURL, isAlive: isAlive, species: character.species, lastLocation: character.lastLocation.name, firstEpisode: firstEpisode) + + return characterCardState + } +} diff --git a/Rick-and-Morty/Rick And Morty/Model/Character.swift b/Rick-and-Morty/Rick And Morty/Model/Character.swift index e061096..d81793c 100644 --- a/Rick-and-Morty/Rick And Morty/Model/Character.swift +++ b/Rick-and-Morty/Rick And Morty/Model/Character.swift @@ -8,7 +8,7 @@ import Foundation -struct CharactersResponse: Codable { +struct CharacterResponse: Codable { enum CodingKeys: String, CodingKey { case info = "info" case characters = "results" diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift new file mode 100644 index 0000000..1bb13a7 --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -0,0 +1,29 @@ +// +// CharacterListViewModel.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import Foundation + +final class CharacterListViewModel: ObservableObject { + @Published var characterListViewState: CharacterListViewState = CharacterListViewState(characterCardStates: []) + + private let characterRepository: CharacterRepositoryProtocol = CharacterRepository() + private let characterCardStateFactory = CharacterCardStateFactory() + + init() { + getCardStates() + } + + func getCardStates() { + characterRepository.getCharacters { characters in + for character in characters { + let cardState = self.characterCardStateFactory.createCharacterCardState(from: character) + self.characterListViewState.characterCardStates.append(cardState) + } + } + } +} diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift index 47384f3..4e5706a 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift @@ -12,7 +12,7 @@ import Foundation struct CharacterCardState { var id: Int var name: String - var image: UIImage + var imageURL: String var isAlive: Bool var species: String var lastLocation: String @@ -35,9 +35,9 @@ struct CharacterCard: View { var body: some View { HStack(alignment: .top) { - Image(uiImage: characterCardState.image) - .resizable() - .aspectRatio(contentMode: .fit) + //Image(uiImage: characterCardState.image) + //.resizable() + //.aspectRatio(contentMode: .fit) VStack(alignment: .leading, spacing: constants.VSpacing) { VStack(alignment: .leading, spacing: constants.noSpacing) { @@ -81,11 +81,11 @@ struct CharacterTileView_Previews: PreviewProvider { static var previews: some View { Group { VStack { - CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) + //CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) } .preferredColorScheme(.light) VStack { - CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) + //CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) } .preferredColorScheme(.dark) diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift index d913853..adf1ade 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift @@ -8,6 +8,11 @@ import SwiftUI +struct CharacterListViewState { + let title: String = "Characters" + var characterCardStates: [CharacterCardState] +} + struct CharacterListView: View { var body: some View { From 2dc1b80ae50b2b4e1609bf01a70c0a26bbefafc9 Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Fri, 13 Aug 2021 11:05:57 +0100 Subject: [PATCH 2/5] Renamed func Changed from get to load considering function does not return anything --- .../Rick And Morty/ViewModels/CharacterListViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift index 1bb13a7..59ad018 100644 --- a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -15,10 +15,10 @@ final class CharacterListViewModel: ObservableObject { private let characterCardStateFactory = CharacterCardStateFactory() init() { - getCardStates() + loadCardStates() } - func getCardStates() { + func loadCardStates() { characterRepository.getCharacters { characters in for character in characters { let cardState = self.characterCardStateFactory.createCharacterCardState(from: character) From 746e5efa967e3e420f5a7eba909bbda6d745732c Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Fri, 13 Aug 2021 11:39:51 +0100 Subject: [PATCH 3/5] Built episode repository Provides an episode for an episode url --- Rick-and-Morty/EpisodeRepository.swift | 34 +++++++++++++++++++ .../Rick And Morty.xcodeproj/project.pbxproj | 11 +++++- .../Rick And Morty/Model/Episode.swift | 13 +++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 Rick-and-Morty/EpisodeRepository.swift create mode 100644 Rick-and-Morty/Rick And Morty/Model/Episode.swift diff --git a/Rick-and-Morty/EpisodeRepository.swift b/Rick-and-Morty/EpisodeRepository.swift new file mode 100644 index 0000000..93b57b2 --- /dev/null +++ b/Rick-and-Morty/EpisodeRepository.swift @@ -0,0 +1,34 @@ +// +// EpisodeRepository.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import Foundation + +protocol EpisodeRepositoryProtocol { + func getEpisode(from urlString: String, completion: @escaping ((Episode) -> Void)) +} + +final class EpisodeRepository: EpisodeRepositoryProtocol { + private let rickAndMortyService: RickAndMortyServiceProtocol = RickAndMortyService() + private var cachedEpisodeNames: [String : Episode] = [:] + + func getEpisode(from urlString: String, completion: @escaping ((Episode) -> Void)) { + if let episode = cachedEpisodeNames[urlString] { + completion(episode) + } else { + if let url = URL(string: urlString) { + rickAndMortyService.fetchData(url: url) { (episode: Episode) in + self.cachedEpisodeNames[urlString] = episode + completion(episode) + } error: { error in + print(error.debugDescription) + return + } + } + } + } +} diff --git a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj index 2df73f3..622757a 100644 --- a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj +++ b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065026C671580080ADD0 /* CharacterRepository.swift */; }; 174B065426C6740E0080ADD0 /* CharacterListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */; }; 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */; }; + 174B065926C680850080ADD0 /* EpisodeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065826C680850080ADD0 /* EpisodeRepository.swift */; }; + 174B065B26C680E20080ADD0 /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065A26C680E20080ADD0 /* Episode.swift */; }; 17588BAC26C1750B008ECC31 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAB26C1750B008ECC31 /* Character.swift */; }; 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAE26C273BB008ECC31 /* CharacterCard.swift */; }; B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B811686C1CFF1C9900301A0A /* AppDelegate.swift */; }; @@ -38,6 +40,8 @@ 174B065026C671580080ADD0 /* CharacterRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRepository.swift; sourceTree = ""; }; 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListViewModel.swift; sourceTree = ""; }; 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCardStateFactory.swift; sourceTree = ""; }; + 174B065826C680850080ADD0 /* EpisodeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeRepository.swift; sourceTree = ""; }; + 174B065A26C680E20080ADD0 /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = ""; }; 17588BAB26C1750B008ECC31 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; 17588BAE26C273BB008ECC31 /* CharacterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCard.swift; sourceTree = ""; }; B81168691CFF1C9900301A0A /* Rick And Morty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rick And Morty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -81,8 +85,10 @@ isa = PBXGroup; children = ( 174B065026C671580080ADD0 /* CharacterRepository.swift */, + 174B065826C680850080ADD0 /* EpisodeRepository.swift */, ); name = Repositories; + path = ..; sourceTree = ""; }; 174B065226C673FB0080ADD0 /* ViewModels */ = { @@ -105,6 +111,7 @@ isa = PBXGroup; children = ( 17588BAB26C1750B008ECC31 /* Character.swift */, + 174B065A26C680E20080ADD0 /* Episode.swift */, ); path = Model; sourceTree = ""; @@ -124,7 +131,6 @@ B811686B1CFF1C9900301A0A /* Rick And Morty */, B81168821CFF1C9900301A0A /* Rick And MortyTests */, B811686A1CFF1C9900301A0A /* Products */, - 174B064E26C66FAD0080ADD0 /* Repositories */, ); sourceTree = ""; }; @@ -146,6 +152,7 @@ 17588BAD26C273A2008ECC31 /* Views */, 17588BAA26C174FB008ECC31 /* Model */, B811686C1CFF1C9900301A0A /* AppDelegate.swift */, + 174B064E26C66FAD0080ADD0 /* Repositories */, B81168751CFF1C9900301A0A /* Assets.xcassets */, B81168771CFF1C9900301A0A /* LaunchScreen.storyboard */, B811687A1CFF1C9900301A0A /* Info.plist */, @@ -271,10 +278,12 @@ 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */, 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */, 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */, + 174B065B26C680E20080ADD0 /* Episode.swift in Sources */, 174B065426C6740E0080ADD0 /* CharacterListViewModel.swift in Sources */, 17588BAC26C1750B008ECC31 /* Character.swift in Sources */, 1711B39E26B1898100BE935B /* CharacterListView.swift in Sources */, B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */, + 174B065926C680850080ADD0 /* EpisodeRepository.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Rick-and-Morty/Rick And Morty/Model/Episode.swift b/Rick-and-Morty/Rick And Morty/Model/Episode.swift new file mode 100644 index 0000000..d0bc439 --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/Model/Episode.swift @@ -0,0 +1,13 @@ +// +// Episode.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import Foundation + +struct Episode: Codable { + let name: String +} From 54725bec194852a69b7c7fd3ee8734d3627af8a1 Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Fri, 13 Aug 2021 12:33:03 +0100 Subject: [PATCH 4/5] Cards are now shown A character Card is shown for each character from the R and M Api. The RemoteImage view is from stack overflow. --- .DS_Store | Bin 8196 -> 8196 bytes Rick-and-Morty/EpisodeRepository.swift | 6 +- .../Rick And Morty.xcodeproj/project.pbxproj | 4 + .../default-card-image.imageset/66.jpeg | Bin 0 -> 21320 bytes .../default-card-image.imageset/Contents.json | 21 ++++++ .../Factories/CharacterCardStateFactory.swift | 2 +- .../ViewModels/CharacterListViewModel.swift | 4 + .../Rick And Morty/Views/CharacterCard.swift | 38 +++++++--- .../Views/CharacterListView.swift | 12 ++- .../Rick And Morty/Views/RemoteImage.swift | 69 ++++++++++++++++++ 10 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/66.jpeg create mode 100644 Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/Contents.json create mode 100644 Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift diff --git a/.DS_Store b/.DS_Store index dcbfa2d67d5e322e4bf51f732066af6e85a0e0db..f90c3c6a02699eebd29dbd9c89a6a6197343ba11 100644 GIT binary patch delta 20 bcmZp1XmQvOFT!qWrlVkJVYxX?WF9vFK~@Fg delta 20 bcmZp1XmQvOFT!qQuA^XNXt6m>WF9vFK@J7k diff --git a/Rick-and-Morty/EpisodeRepository.swift b/Rick-and-Morty/EpisodeRepository.swift index 93b57b2..065d765 100644 --- a/Rick-and-Morty/EpisodeRepository.swift +++ b/Rick-and-Morty/EpisodeRepository.swift @@ -14,15 +14,15 @@ protocol EpisodeRepositoryProtocol { final class EpisodeRepository: EpisodeRepositoryProtocol { private let rickAndMortyService: RickAndMortyServiceProtocol = RickAndMortyService() - private var cachedEpisodeNames: [String : Episode] = [:] + static private var cachedEpisodeNames: [String : Episode] = [:] func getEpisode(from urlString: String, completion: @escaping ((Episode) -> Void)) { - if let episode = cachedEpisodeNames[urlString] { + if let episode = EpisodeRepository.cachedEpisodeNames[urlString] { completion(episode) } else { if let url = URL(string: urlString) { rickAndMortyService.fetchData(url: url) { (episode: Episode) in - self.cachedEpisodeNames[urlString] = episode + EpisodeRepository.cachedEpisodeNames[urlString] = episode completion(episode) } error: { error in print(error.debugDescription) diff --git a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj index 622757a..187e631 100644 --- a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj +++ b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */; }; 174B065926C680850080ADD0 /* EpisodeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065826C680850080ADD0 /* EpisodeRepository.swift */; }; 174B065B26C680E20080ADD0 /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065A26C680E20080ADD0 /* Episode.swift */; }; + 174B065D26C6861E0080ADD0 /* RemoteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065C26C6861E0080ADD0 /* RemoteImage.swift */; }; 17588BAC26C1750B008ECC31 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAB26C1750B008ECC31 /* Character.swift */; }; 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAE26C273BB008ECC31 /* CharacterCard.swift */; }; B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B811686C1CFF1C9900301A0A /* AppDelegate.swift */; }; @@ -42,6 +43,7 @@ 174B065626C6751F0080ADD0 /* CharacterCardStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCardStateFactory.swift; sourceTree = ""; }; 174B065826C680850080ADD0 /* EpisodeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeRepository.swift; sourceTree = ""; }; 174B065A26C680E20080ADD0 /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = ""; }; + 174B065C26C6861E0080ADD0 /* RemoteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImage.swift; sourceTree = ""; }; 17588BAB26C1750B008ECC31 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; 17588BAE26C273BB008ECC31 /* CharacterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCard.swift; sourceTree = ""; }; B81168691CFF1C9900301A0A /* Rick And Morty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rick And Morty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -121,6 +123,7 @@ children = ( 1711B39D26B1898100BE935B /* CharacterListView.swift */, 17588BAE26C273BB008ECC31 /* CharacterCard.swift */, + 174B065C26C6861E0080ADD0 /* RemoteImage.swift */, ); path = Views; sourceTree = ""; @@ -276,6 +279,7 @@ 174B064D26C661470080ADD0 /* RickAndMortyServiceProtocol.swift in Sources */, 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */, 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */, + 174B065D26C6861E0080ADD0 /* RemoteImage.swift in Sources */, 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */, 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */, 174B065B26C680E20080ADD0 /* Episode.swift in Sources */, diff --git a/Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/66.jpeg b/Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/66.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..66d4bf5869e8a959870234ce63e41e237375603f GIT binary patch literal 21320 zcmbrl2T)V}_Ab0bKsurKP7su)RHgSKB8bwPg7hW`(n6QsK|ny7fOM1+dXe5mI)ok& zX~6_3QHnR7_x;~<&Uen8x%1sHnM{VvPS##)J000odU)Kiyet_`6_ypGo ziHJ!^aUXzF0(c-W7!MyzK!A^r`*a}g{{TJ}0X5fcrE4?~pAvF=(29np6cX_$*LKtC zPokk>)}CR+B=ii7Ow7F3`S@?#6qk^cx^q`rMO95*LsRSCqsIn@M#d(lHqUJB>>V7P zUU+%G^zrrc4}TRA85JE9o0|4IJtOl?R`$E1;*$5JW#tuh^$qaGrskH`p5DIxfx)5S zk*VpK*}3_JMZ^YjbL;E&x1HTR%+ZhIlhZTo&tHG+0s-Lvearqb?EhmI?k0fn@bSU; zgn#S;;rZa6U@Ck9uG`nBl^zm4^`PMv4JD#gPARPICgu^-N7Gq*PLj|=#W#2{e@y#_ zW&d}Eh5i4s?0*~fPrDER85o3f9+(PH0It^gfdae`65zkQ+|9Kg$b+DFz31BvHddIX zS&C*Qa-|3Kbd|oEFKckwUD1&kpI!@*?RtD!(n;#5mU3ZFeo*;Vqh#0ra{&t>q3d{O z-ke5WRtpm$4^K9*=@#rF@+w)9JFY*o=+263k?Fh5{G3a%#|3BEM5nNJg=;?8k$(u6!2k-al<) zW_GKt*qJBwDR%OYOn+HS*E;R)=Hf=1sAN+8ELKtd!L4QcSm5+GKo&F|%qS~9Dd3v` zQ*&g(U&Ln2$!Tf=oG}vkl8EllFY-}bLxkr`A@Z-$QK-U#RYp|1>qguPv$BDi{+Dg$ zl0?m)CD{X;2lI`S;h&6oXSTLH=0fB@pi7frESqsJyf<{IcKs2YD}s_uAa~c9R8ZS= zDU}t%jq~7Kr@?GcAY~lzp83D<3W#8K7M{?4mf3X6)YMGB!4KHR4$up=Ie=2)Rehyz+hT0ysGx5 zsovYm*ZT&M!zaR?D`{*S28_oZC9NeNixd!{G#vmeCRYa;n`R{A+pZUaEgoRW%avQi zL1+2BzkxV;0&JGc6xPgG%iTI!+wa|K>B314FBETjQAR34zI#4X@6) zqoa3iPe_o%#XaogW`3-c$Yonujs?@RHOGx{iQ=XyPLi+h{0djC^jeeREmj+*1l-Q= zG$phbfVOZUho7P*cr-{xr6oSqI!Jq)M@z8oE=dfl18G}5CEa6sYegU%IGZ308e5}> zS$WJPsl<>SqX(MW^Mp_U0#!oQ>IzEe3u>MZGSf!7x z@B1~-a))HfboZgzP9w_^sp@zB8F#yAveGQpIemq-CHj{D#OF&VK8zcU*8S)UQO;A< zlSNssob1Mt+36r=GB==5doN|V;^a^U5S!&wW)r|BWKPyEA*DK%DB8dGI1S?Os0uN_maT7 zjYmWlR%4#mhGJX`z}w_Lz1hEki0@`P82W^Ahs+VFz|2SWd7lmM<3Z!^NApHU64*w! zyAa*%qf%sS0Q3mjSk703kJj|2-40zus(DRN?t5Kz#6r(6<1SvhP> zxgyLY1^}*ULD2wfsxMv$E@?N;A9cTH=^}yMZi)S_$8OtMf}B~DNKd)>aN~>Lv-UPa zsATa*6>8)+@J?u{Id1{{WAEtbyIyN^XID#eM@L(>c3fp!d2w?~OP-;K;PixmcI6@{*llR410EdE#-~})C5KmuE-x=@CU?8zyNmxA=fO|pw~iN2)#6Dmi>?~$yBVfQ z3p$zawOVB858|P83X*9JCUO)u7|3GQ@IY#pHABU9I;S-$s+2pvA!;_gY;&{~4TcC; zg05k8);Xf|1cMmSmn9vm3IUZXWu0^OP&(^h37`Bn)yZDCJGr9N9}F?@ak3};C#!;z zYJ7v0F+y>>d7M+?D7&8fY0l-QDn3NkRe8^xMHSNd4WHDj>OUS{z~SJgd-QLh_H$;y zw>>LzZ-s-nv{qfLhIf`-%QWi|td0B;jg<*nve>A+jAv*~Vx>wY1QIE0vff4O%knso zrQcL;H7Tf|4!Xfk`X+QR*WL%L?d*5W_eb^nbkI-ZNCHJd5!?^-U+d|L?-a&9Di>^3 z>s{MD+#R?5GE{)`bsQov6hIiLCmZ&7p z&nA+{#?LO>$QT*RKhutLX>!USGv4O|TnLRR;(sCBo=1VswJdxq%zgs|Sc*e!D^igx z$BKPD`o#S{V;S49Ii*HzSGtdrjZ<}dDH+u1b;;d#2nTVRhOpv71S_`*iEWK(`cf@F zTD&ww^9VzCq?v6HiPxrD2WmaV7I7u^%){2&6Sc!#T}(Fsg$MEs7Z9{Y&yvH|)WJY0 zl7@Y$i^l1p#`PjDZ#?4zBXagkhi{x_i2U1MDpr

WKNl3p@X30s5T=tJ~WtHuZi| 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 literal 0 HcmV?d00001 diff --git a/Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/Contents.json b/Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/Contents.json new file mode 100644 index 0000000..eb0475c --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/Assets.xcassets/default-card-image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "66.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift index 83c6a4d..6c55581 100644 --- a/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift +++ b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift @@ -28,7 +28,7 @@ final class CharacterCardStateFactory { func createCharacterCardState(from character: Character) -> CharacterCardState { let isAlive = getIsAlive(character: character) let firstEpisode = getFirstEpisode(character: character) ?? "Unknown" - let characterCardState = CharacterCardState(id: character.id, name: character.name, imageURL: character.imageURL, isAlive: isAlive, species: character.species, lastLocation: character.lastLocation.name, firstEpisode: firstEpisode) + let characterCardState = CharacterCardState(id: character.id, name: character.name, imageURL: character.imageURL, isAlive: isAlive, species: character.species, lastLocation: character.lastLocation.name, firstEpisodeURL: firstEpisode) return characterCardState } diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift index 59ad018..9775d6f 100644 --- a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -26,4 +26,8 @@ final class CharacterListViewModel: ObservableObject { } } } + + func isLastCard(characterCardState: CharacterCardState) -> Bool { + return characterCardState.id == characterListViewState.characterCardStates.last?.id + } } diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift index 4e5706a..245f27f 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift @@ -16,7 +16,7 @@ struct CharacterCardState { var isAlive: Bool var species: String var lastLocation: String - var firstEpisode: String + var firstEpisodeURL: String } private struct Constants { @@ -24,7 +24,7 @@ private struct Constants { let noSpacing: CGFloat = 0 let VSpacing: CGFloat = 10 let borderWidth: CGFloat = 0.25 - let cardHeight: CGFloat = 200 + let cardHeight: CGFloat = 175 let lastLocationCaption = "Last known location:" let firstEpisodeCaption = "First seen in:" } @@ -35,9 +35,8 @@ struct CharacterCard: View { var body: some View { HStack(alignment: .top) { - //Image(uiImage: characterCardState.image) - //.resizable() - //.aspectRatio(contentMode: .fit) + RemoteImage(url: characterCardState.imageURL) + .aspectRatio(contentMode: .fit) VStack(alignment: .leading, spacing: constants.VSpacing) { VStack(alignment: .leading, spacing: constants.noSpacing) { @@ -54,7 +53,12 @@ struct CharacterCard: View { characterDetailsVStack(caption: constants.lastLocationCaption, text: characterCardState.lastLocation) - characterDetailsVStack(caption: constants.firstEpisodeCaption, text: characterCardState.firstEpisode) + VStack(alignment: .leading, spacing: constants.noSpacing) { + Text(constants.firstEpisodeCaption) + .font(.caption) + .foregroundColor(.secondary) + FirstEpisodeText(characterCardState: characterCardState) + } } Spacer() } @@ -63,7 +67,6 @@ struct CharacterCard: View { RoundedRectangle(cornerRadius: constants.cornerRadius) .stroke(Color(.lightGray), lineWidth: constants.borderWidth) ) - .padding() .frame(height: constants.cardHeight) } @@ -77,18 +80,35 @@ struct CharacterCard: View { } } +struct FirstEpisodeText: View { + let characterCardState: CharacterCardState + @State var firstEpisode: Episode = Episode(name: "") + private let episodeRepository: EpisodeRepositoryProtocol = EpisodeRepository() + + var body: some View { + Text(firstEpisode.name) + .onAppear(perform: { + episodeRepository.getEpisode(from: characterCardState.firstEpisodeURL) { episode in + self.firstEpisode = episode + } + }) + } +} + + struct CharacterTileView_Previews: PreviewProvider { static var previews: some View { Group { VStack { - //CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) + CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", imageURL: " ", isAlive: true, species: "Human", lastLocation: "Earth", firstEpisodeURL: "Episode 1")) } .preferredColorScheme(.light) VStack { - //CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", image: UIImage(named: "morty-image")!, isAlive: true, species: "Human", lastLocation: "Earth", firstEpisode: "Episode 1")) + CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", imageURL: " ", isAlive: true, species: "Human", lastLocation: "Earth", firstEpisodeURL: "Episode 1")) } .preferredColorScheme(.dark) } } } + diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift index adf1ade..f251d23 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift @@ -14,13 +14,19 @@ struct CharacterListViewState { } struct CharacterListView: View { + @ObservedObject var viewModel: CharacterListViewModel = CharacterListViewModel() var body: some View { NavigationView { - VStack { - + List(viewModel.characterListViewState.characterCardStates, id: \.id) { cardState in + CharacterCard(characterCardState: cardState) + .onAppear(perform: { + if viewModel.isLastCard(characterCardState: cardState) { + viewModel.loadCardStates() + } + }) } - .navigationTitle("Characters") + .navigationTitle(viewModel.characterListViewState.title) } } } diff --git a/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift b/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift new file mode 100644 index 0000000..a97f9b9 --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift @@ -0,0 +1,69 @@ +// +// RemoteImage.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import SwiftUI + +struct RemoteImage: View { + private enum LoadState { + case loading, success, failure + } + + private class Loader: ObservableObject { + var data = Data() + var state = LoadState.loading + + init(url: String) { + guard let parsedURL = URL(string: url) else { + fatalError("Invalid URL: \(url)") + } + + URLSession.shared.dataTask(with: parsedURL) { data, response, error in + if let data = data, data.count > 0 { + self.data = data + self.state = .success + } else { + self.state = .failure + } + + DispatchQueue.main.async { + self.objectWillChange.send() + } + }.resume() + } + } + + @StateObject private var loader: Loader + var loading: Image + var failure: Image + + var body: some View { + selectImage() + .resizable() + } + + init(url: String, loading: Image = Image("default-card-image"), failure: Image = Image("default-card-image")) { + _loader = StateObject(wrappedValue: Loader(url: url)) + self.loading = loading + self.failure = failure + } + + private func selectImage() -> Image { + switch loader.state { + case .loading: + return loading + case .failure: + return failure + default: + if let image = UIImage(data: loader.data) { + return Image(uiImage: image) + } else { + return failure + } + } + } +} From 774636861e12410d1f977118b1d8a2b0480cfd2e Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Tue, 17 Aug 2021 10:13:26 +0100 Subject: [PATCH 5/5] Cards are visable Character Cards are displayed on the app and more are loaded as the user scrolls. --- .DS_Store | Bin 8196 -> 8196 bytes .../Rick And Morty.xcodeproj/project.pbxproj | 4 + .../Factories/CharacterCardStateFactory.swift | 41 +++++--- .../Rick And Morty/Model/Character.swift | 8 +- .../ViewModels/CharacterCardViewModel.swift | 46 +++++++++ .../ViewModels/CharacterListViewModel.swift | 21 ++-- .../Rick And Morty/Views/CharacterCard.swift | 94 ++++++++---------- .../Views/CharacterListView.swift | 13 +-- .../Rick And Morty/Views/RemoteImage.swift | 2 +- 9 files changed, 147 insertions(+), 82 deletions(-) create mode 100644 Rick-and-Morty/Rick And Morty/ViewModels/CharacterCardViewModel.swift diff --git a/.DS_Store b/.DS_Store index f90c3c6a02699eebd29dbd9c89a6a6197343ba11..dcbfa2d67d5e322e4bf51f732066af6e85a0e0db 100644 GIT binary patch delta 20 bcmZp1XmQvOFT!qQuA^XNXt6m>WF9vFK@J7k delta 20 bcmZp1XmQvOFT!qWrlVkJVYxX?WF9vFK~@Fg diff --git a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj index 187e631..58de5a1 100644 --- a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj +++ b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 174B065926C680850080ADD0 /* EpisodeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065826C680850080ADD0 /* EpisodeRepository.swift */; }; 174B065B26C680E20080ADD0 /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065A26C680E20080ADD0 /* Episode.swift */; }; 174B065D26C6861E0080ADD0 /* RemoteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065C26C6861E0080ADD0 /* RemoteImage.swift */; }; + 174B065F26C6ABB00080ADD0 /* CharacterCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065E26C6ABB00080ADD0 /* CharacterCardViewModel.swift */; }; 17588BAC26C1750B008ECC31 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAB26C1750B008ECC31 /* Character.swift */; }; 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17588BAE26C273BB008ECC31 /* CharacterCard.swift */; }; B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B811686C1CFF1C9900301A0A /* AppDelegate.swift */; }; @@ -44,6 +45,7 @@ 174B065826C680850080ADD0 /* EpisodeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeRepository.swift; sourceTree = ""; }; 174B065A26C680E20080ADD0 /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = ""; }; 174B065C26C6861E0080ADD0 /* RemoteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImage.swift; sourceTree = ""; }; + 174B065E26C6ABB00080ADD0 /* CharacterCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCardViewModel.swift; sourceTree = ""; }; 17588BAB26C1750B008ECC31 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; 17588BAE26C273BB008ECC31 /* CharacterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCard.swift; sourceTree = ""; }; B81168691CFF1C9900301A0A /* Rick And Morty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rick And Morty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -97,6 +99,7 @@ isa = PBXGroup; children = ( 174B065326C6740E0080ADD0 /* CharacterListViewModel.swift */, + 174B065E26C6ABB00080ADD0 /* CharacterCardViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -280,6 +283,7 @@ 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */, 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */, 174B065D26C6861E0080ADD0 /* RemoteImage.swift in Sources */, + 174B065F26C6ABB00080ADD0 /* CharacterCardViewModel.swift in Sources */, 174B065726C6751F0080ADD0 /* CharacterCardStateFactory.swift in Sources */, 17588BAF26C273BB008ECC31 /* CharacterCard.swift in Sources */, 174B065B26C680E20080ADD0 /* Episode.swift in Sources */, diff --git a/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift index 6c55581..8d29525 100644 --- a/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift +++ b/Rick-and-Morty/Rick And Morty/Factories/CharacterCardStateFactory.swift @@ -7,28 +7,45 @@ // import Foundation +import SwiftUI final class CharacterCardStateFactory { - private func getIsAlive(character: Character) -> Bool { - if character.status == "Alive" { - return true + private func getStatusColor(character: Character) -> Color { + switch character.status { + case .alive: + return .green + case .dead: + return .red + default: + return .yellow } - - return false } - private func getFirstEpisode(character: Character) -> String? { - if let firstEpisode = character.episodeURLs.first { - return firstEpisode + private func getStatusText(character: Character) -> String { + var str = "" + switch character.status { + case .alive: + str = "Alive" + case .dead: + str = "Dead" + default: + str = "unknown" } - return nil + return str + " - " + character.species } func createCharacterCardState(from character: Character) -> CharacterCardState { - let isAlive = getIsAlive(character: character) - let firstEpisode = getFirstEpisode(character: character) ?? "Unknown" - let characterCardState = CharacterCardState(id: character.id, name: character.name, imageURL: character.imageURL, isAlive: isAlive, species: character.species, lastLocation: character.lastLocation.name, firstEpisodeURL: firstEpisode) + let characterCardState = CharacterCardState( + id: character.id, + name: character.name, + imageURL: character.imageURL, + statusColor: getStatusColor(character: character), + species: character.species, + lastLocation: character.lastLocation.name, + firstEpisodeName: "", + statusText: getStatusText(character: character) + ) return characterCardState } diff --git a/Rick-and-Morty/Rick And Morty/Model/Character.swift b/Rick-and-Morty/Rick And Morty/Model/Character.swift index d81793c..6c36cd6 100644 --- a/Rick-and-Morty/Rick And Morty/Model/Character.swift +++ b/Rick-and-Morty/Rick And Morty/Model/Character.swift @@ -26,6 +26,12 @@ struct CharacterResponseInfo: Codable { } struct Character: Codable { + enum Status: String, Codable { + case alive = "Alive" + case dead = "Dead" + case unkown = "unknown" + } + enum CodingKeys: String, CodingKey { case id = "id" case name = "name" @@ -40,7 +46,7 @@ struct Character: Codable { let name: String let species: String let lastLocation: LastLocation - let status: String + let status: Status let imageURL: String let episodeURLs: [String] } diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterCardViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterCardViewModel.swift new file mode 100644 index 0000000..e18069b --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterCardViewModel.swift @@ -0,0 +1,46 @@ +// +// CharacterCardViewModel.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-13. +// Copyright © 2021 Novoda. All rights reserved. +// + +import Foundation +import SwiftUI + + +struct CharacterCardState { + var id: Int + var name: String + var imageURL: String + var statusColor: Color + var species: String + var lastLocation: String + var firstEpisodeName: String + var statusText: String +} + +final class CharacterCardViewModel: ObservableObject { + @Published var cardState: CharacterCardState + + let character: Character + private let episodeRepository: EpisodeRepositoryProtocol = EpisodeRepository() + + init(character: Character) { + let cardStateFactory = CharacterCardStateFactory() + + self.character = character + self.cardState = cardStateFactory.createCharacterCardState(from: character) + + loadFirstEpisodeName() + } + + func loadFirstEpisodeName() { + if let firstURL = character.episodeURLs.first { + episodeRepository.getEpisode(from: firstURL) { episode in + self.cardState.firstEpisodeName = episode.name + } + } + } +} diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift index 9775d6f..3110528 100644 --- a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -8,26 +8,33 @@ import Foundation + +struct CharacterListViewState { + let title: String = "Characters" + var characters: [Character] +} + final class CharacterListViewModel: ObservableObject { - @Published var characterListViewState: CharacterListViewState = CharacterListViewState(characterCardStates: []) + @Published var characterListViewState: CharacterListViewState = CharacterListViewState(characters: []) private let characterRepository: CharacterRepositoryProtocol = CharacterRepository() private let characterCardStateFactory = CharacterCardStateFactory() init() { - loadCardStates() + loadCharacters() } - func loadCardStates() { + func loadCharacters() { characterRepository.getCharacters { characters in for character in characters { - let cardState = self.characterCardStateFactory.createCharacterCardState(from: character) - self.characterListViewState.characterCardStates.append(cardState) + self.characterListViewState.characters.append(character) } } } - func isLastCard(characterCardState: CharacterCardState) -> Bool { - return characterCardState.id == characterListViewState.characterCardStates.last?.id + func loadIfNeeded(characterID: Int) { + if characterID == characterListViewState.characters.last?.id { + loadCharacters() + } } } diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift index 245f27f..b64148c 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterCard.swift @@ -9,16 +9,7 @@ import SwiftUI import Foundation -struct CharacterCardState { - var id: Int - var name: String - var imageURL: String - var isAlive: Bool - var species: String - var lastLocation: String - var firstEpisodeURL: String -} - +//enums private struct Constants { let cornerRadius: CGFloat = 10 let noSpacing: CGFloat = 0 @@ -30,35 +21,20 @@ private struct Constants { } struct CharacterCard: View { - var characterCardState: CharacterCardState + @ObservedObject var cardViewModel: CharacterCardViewModel private let constants: Constants = Constants() var body: some View { HStack(alignment: .top) { - RemoteImage(url: characterCardState.imageURL) + RemoteImage(url: cardViewModel.cardState.imageURL) .aspectRatio(contentMode: .fit) VStack(alignment: .leading, spacing: constants.VSpacing) { - VStack(alignment: .leading, spacing: constants.noSpacing) { - Text(characterCardState.name) - .font(.title) - .fontWeight(.black) - HStack { - Circle() - .foregroundColor(characterCardState.isAlive ? .green : .red) - .frame(width: 10, height: 10) - Text(characterCardState.isAlive ? "Alive - \(characterCardState.species)" : "Dead - \(characterCardState.species)") - } - } + NameAndStatusView(name: cardViewModel.cardState.name, statusColor: cardViewModel.cardState.statusColor, statusText: cardViewModel.cardState.statusText) - characterDetailsVStack(caption: constants.lastLocationCaption, text: characterCardState.lastLocation) + DescriptionDetailView(title: constants.lastLocationCaption, text: cardViewModel.cardState.lastLocation) - VStack(alignment: .leading, spacing: constants.noSpacing) { - Text(constants.firstEpisodeCaption) - .font(.caption) - .foregroundColor(.secondary) - FirstEpisodeText(characterCardState: characterCardState) - } + DescriptionDetailView(title: constants.firstEpisodeCaption, text: cardViewModel.cardState.firstEpisodeName) } Spacer() } @@ -69,42 +45,58 @@ struct CharacterCard: View { ) .frame(height: constants.cardHeight) } - - func characterDetailsVStack(caption: String, text: String) -> some View { - return VStack(alignment: .leading, spacing: constants.noSpacing) { - Text(caption) - .font(.caption) - .foregroundColor(.secondary) - Text(text) - } - } } -struct FirstEpisodeText: View { - let characterCardState: CharacterCardState - @State var firstEpisode: Episode = Episode(name: "") - private let episodeRepository: EpisodeRepositoryProtocol = EpisodeRepository() +extension CharacterCard { + private struct DescriptionDetailView: View { + let title: String + let text: String + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(title) + .font(.caption) + .foregroundColor(.secondary) + Text(text) + } + } + } - var body: some View { - Text(firstEpisode.name) - .onAppear(perform: { - episodeRepository.getEpisode(from: characterCardState.firstEpisodeURL) { episode in - self.firstEpisode = episode + private struct NameAndStatusView: View { + let name: String + let statusColor: Color + let statusText: String + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(name) + .font(.title) + .fontWeight(.black) + HStack { + Circle() + .foregroundColor(statusColor) + .frame(width: 10, height: 10) + Text(statusText) } - }) + } + } } } + + + + struct CharacterTileView_Previews: PreviewProvider { static var previews: some View { Group { VStack { - CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", imageURL: " ", isAlive: true, species: "Human", lastLocation: "Earth", firstEpisodeURL: "Episode 1")) + CharacterCard(cardViewModel: CharacterCardViewModel(character: Character(id: 1, name: "Morty", species: "Human", lastLocation: LastLocation(name: "Earth", url: ""), status: .alive, imageURL: "", episodeURLs: []))) } .preferredColorScheme(.light) VStack { - CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", imageURL: " ", isAlive: true, species: "Human", lastLocation: "Earth", firstEpisodeURL: "Episode 1")) + //CharacterCard(characterCardState: CharacterCardState(id: 2, name: "Morty", imageURL: " ", isAlive: true, species: "Human", lastLocation: "Earth", firstEpisodeURL: "Episode 1")) } .preferredColorScheme(.dark) diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift index f251d23..ee13bd6 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift @@ -8,22 +8,15 @@ import SwiftUI -struct CharacterListViewState { - let title: String = "Characters" - var characterCardStates: [CharacterCardState] -} - struct CharacterListView: View { @ObservedObject var viewModel: CharacterListViewModel = CharacterListViewModel() var body: some View { NavigationView { - List(viewModel.characterListViewState.characterCardStates, id: \.id) { cardState in - CharacterCard(characterCardState: cardState) + List(viewModel.characterListViewState.characters, id: \.id) { character in + CharacterCard(cardViewModel: CharacterCardViewModel(character: character)) .onAppear(perform: { - if viewModel.isLastCard(characterCardState: cardState) { - viewModel.loadCardStates() - } + viewModel.loadIfNeeded(characterID: character.id) }) } .navigationTitle(viewModel.characterListViewState.title) diff --git a/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift b/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift index a97f9b9..bef74d9 100644 --- a/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift +++ b/Rick-and-Morty/Rick And Morty/Views/RemoteImage.swift @@ -29,7 +29,7 @@ struct RemoteImage: View { } else { self.state = .failure } - + DispatchQueue.main.async { self.objectWillChange.send() }