Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature [RM61] Created and Implemented CharactersViewModel #65

Merged
merged 5 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
1711B39C26B16D6200BE935B /* CharactersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1711B39B26B16D6200BE935B /* CharactersView.swift */; };
1711B39E26B1898100BE935B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1711B39D26B1898100BE935B /* ContentView.swift */; };
171752C126B3FD50007B1A60 /* CharacterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171752C026B3FD50007B1A60 /* CharacterDetailView.swift */; };
171FE19626B8251000D31075 /* CharactersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171FE19526B8251000D31075 /* CharactersViewModel.swift */; };
1758BFE226BBEEB900C18BC2 /* CharacterViewStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1758BFE126BBEEB900C18BC2 /* CharacterViewStateFactory.swift */; };
1758BFE426BBEECC00C18BC2 /* CharactersViewStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1758BFE326BBEECC00C18BC2 /* CharactersViewStateFactory.swift */; };
1758BFE726BBEF2C00C18BC2 /* CharactersManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1758BFE626BBEF2C00C18BC2 /* CharactersManager.swift */; };
178A555626BACB2A00D25282 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178A555526BACB2A00D25282 /* ContentViewModel.swift */; };
17CAB4FA26A824470048F2F1 /* CharacterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CAB4F926A824470048F2F1 /* CharacterCell.swift */; };
17F1E38626A1AF6A009C1CDB /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1E38526A1AF6A009C1CDB /* Character.swift */; };
17F1E38D26A1DCF0009C1CDB /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1E38C26A1DCF0009C1CDB /* Data.swift */; };
Expand All @@ -35,6 +40,11 @@
1711B39B26B16D6200BE935B /* CharactersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersView.swift; sourceTree = "<group>"; };
1711B39D26B1898100BE935B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
171752C026B3FD50007B1A60 /* CharacterDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterDetailView.swift; sourceTree = "<group>"; };
171FE19526B8251000D31075 /* CharactersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersViewModel.swift; sourceTree = "<group>"; };
1758BFE126BBEEB900C18BC2 /* CharacterViewStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterViewStateFactory.swift; sourceTree = "<group>"; };
1758BFE326BBEECC00C18BC2 /* CharactersViewStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersViewStateFactory.swift; sourceTree = "<group>"; };
1758BFE626BBEF2C00C18BC2 /* CharactersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersManager.swift; sourceTree = "<group>"; };
178A555526BACB2A00D25282 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
17CAB4F926A824470048F2F1 /* CharacterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCell.swift; sourceTree = "<group>"; };
17F1E38526A1AF6A009C1CDB /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = "<group>"; };
17F1E38C26A1DCF0009C1CDB /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -68,6 +78,32 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
171FE19426B8233600D31075 /* ViewModels */ = {
isa = PBXGroup;
children = (
171FE19526B8251000D31075 /* CharactersViewModel.swift */,
178A555526BACB2A00D25282 /* ContentViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
1758BFE026BBEE7D00C18BC2 /* Factories */ = {
isa = PBXGroup;
children = (
1758BFE126BBEEB900C18BC2 /* CharacterViewStateFactory.swift */,
1758BFE326BBEECC00C18BC2 /* CharactersViewStateFactory.swift */,
);
path = Factories;
sourceTree = "<group>";
};
1758BFE526BBEF1700C18BC2 /* misc */ = {
isa = PBXGroup;
children = (
1758BFE626BBEF2C00C18BC2 /* CharactersManager.swift */,
);
path = misc;
sourceTree = "<group>";
};
17F1E38026A184F3009C1CDB /* Models */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -111,8 +147,11 @@
B811686B1CFF1C9900301A0A /* Rick And Morty */ = {
isa = PBXGroup;
children = (
171FE19426B8233600D31075 /* ViewModels */,
17F1E38E26A5724A009C1CDB /* Views */,
17F1E38026A184F3009C1CDB /* Models */,
1758BFE026BBEE7D00C18BC2 /* Factories */,
1758BFE526BBEF1700C18BC2 /* misc */,
B811686C1CFF1C9900301A0A /* AppDelegate.swift */,
B81168751CFF1C9900301A0A /* Assets.xcassets */,
B81168771CFF1C9900301A0A /* LaunchScreen.storyboard */,
Expand Down Expand Up @@ -235,14 +274,19 @@
buildActionMask = 2147483647;
files = (
171752C126B3FD50007B1A60 /* CharacterDetailView.swift in Sources */,
1758BFE726BBEF2C00C18BC2 /* CharactersManager.swift in Sources */,
17F1E38D26A1DCF0009C1CDB /* Data.swift in Sources */,
1711B39E26B1898100BE935B /* ContentView.swift in Sources */,
B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */,
171FE19626B8251000D31075 /* CharactersViewModel.swift in Sources */,
17CAB4FA26A824470048F2F1 /* CharacterCell.swift in Sources */,
B81168931CFF234900301A0A /* Rick.swift in Sources */,
1758BFE426BBEECC00C18BC2 /* CharactersViewStateFactory.swift in Sources */,
178A555626BACB2A00D25282 /* ContentViewModel.swift in Sources */,
1711B39C26B16D6200BE935B /* CharactersView.swift in Sources */,
17F1E38626A1AF6A009C1CDB /* Character.swift in Sources */,
B81168951CFF235600301A0A /* Morty.swift in Sources */,
1758BFE226BBEEB900C18BC2 /* CharacterViewStateFactory.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

final class CharacterViewStateFactory {
func createCharacterViewState(from character: Character) -> CharacterViewState {
let shortDescription = getShortDescription(for: character)
let imagePosition = getImagePosition(for: character)

return CharacterViewState(character: character, name: character.name, description: character.description, shortDescription: shortDescription, imageName: character.image, imagePosition: imagePosition)
}

private func getShortDescription(for character: Character) -> String? {
if let character = character as? Morty {
return character.shortDescription
}

return nil
}

private func getImagePosition(for character: Character) -> CharacterImagePosition {
if character is Morty {
return .left
} else {
return .right
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

final class CharactersViewStateFactory {
func createCharactersViewState(characterType: CharacterType, characterViewStates: [CharacterViewState]) -> CharactersViewState {
let title = getTitle(for: characterType)
let iconName = getIconName(for: characterType)

return CharactersViewState(title: title, iconName: iconName, characterViewStates: characterViewStates)
}

private func getTitle(for characterType: CharacterType) -> String {
return characterType == .rick ? "Ricks" : "Morties"
}

private func getIconName(for characterType: CharacterType) -> String {
return characterType == .rick ? "rick-icon" : "morty-icon"
}
}
36 changes: 36 additions & 0 deletions Rick-and-Morty/Rick And Morty/ViewModels/CharactersViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

final class CharactersViewModel: ObservableObject {
let characterType: CharacterType

@Published var characters: [Character] = []
var title: String = ""
var imagePosition: CharacterImagePosition = .right

init(characterType: CharacterType) {
self.characterType = characterType
self.title = getTitle()
self.imagePosition = getImagePosition()

self.characters = fetchCharacters()
}

private func fetchCharacters() -> [Character] {
return characterType == .rick ? ricks : morties
}

private func getTitle() -> String {
return characterType == .rick ? "Ricks" : "Morties"
}

private func getImagePosition() -> CharacterImagePosition {
return characterType == .rick ? .right : .left
}
}

extension CharactersViewModel {
enum CharacterType {
case rick
case morty
}
}
Comment on lines +31 to +36
Copy link
Member

@gbasile gbasile Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting: this is violating the Open Closed principle :P
You would have to modify the ViewModel if you want to add Summer or Jerry

Why don't you define a struct instead?

struct CharacterViewState {
    let title: String
    let name: String
    let description: String
    let imageName: String
    let imagePosition: CharacterImagePosition
}

If you feed your ViewModel with an array of this model, you won't need to take decisions anymore inside your ViewModel ;)

Copy link
Contributor Author

@swg99 swg99 Aug 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a CharacterViewState and a CharactersViewState which holds an array of CharacterViewState's (naming could probably be better).

A CharactersViewState is made for each CharacterType in the new ContentViewModel. I don't think there is a need for a CharactersViewModel anymore. Check out my new commit, it should make more sense.

61 changes: 61 additions & 0 deletions Rick-and-Morty/Rick And Morty/ViewModels/ContentViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation

final class ContentViewModel: ObservableObject {
@Published var charatersViewStates: [CharactersViewState] = []

private let charactersManager = CharactersManager()
private let characterViewStateFactory = CharacterViewStateFactory()
private let charactersViewStateFactory = CharactersViewStateFactory()

init() {
self.charatersViewStates = getCharactersViewStates()
}

private func getCharactersViewStates() -> [CharactersViewState] {
var charactersViewStates: [CharactersViewState] = []

for characterType in CharacterType.allCases {
let characters = charactersManager.fetchCharacters(characterType: characterType)
let characterViewStates = getCharacterViewStates(from: characters)

let charactersViewState = charactersViewStateFactory.createCharactersViewState(characterType: characterType, characterViewStates: characterViewStates)

charactersViewStates.append(charactersViewState)
}

return charactersViewStates
}

private func getCharacterViewStates(from characters: [Character]) -> [CharacterViewState] {
var characterViewStates: [CharacterViewState] = []

for character in characters {
let characterViewState = characterViewStateFactory.createCharacterViewState(from: character)
characterViewStates.append(characterViewState)
}

return characterViewStates
}
}

struct CharactersViewState: Identifiable {
let id = UUID()
let title: String
let iconName: String
let characterViewStates: [CharacterViewState]
}

struct CharacterViewState: Identifiable {
let id = UUID()
let character: Character
let name: String
let description: String
let shortDescription: String?
let imageName: String
let imagePosition: CharacterImagePosition
}

enum CharacterType: CaseIterable {
case rick
case morty
}
24 changes: 12 additions & 12 deletions Rick-and-Morty/Rick And Morty/Views/CharacterCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,22 @@ enum CharacterImagePosition {
}

struct CharacterCell: View {
let character: Character
let imagePosition: CharacterImagePosition
let characterViewState: CharacterViewState

var body: some View {
NavigationLink(destination: CharacterDetailView(character: character)) {
NavigationLink(destination: CharacterDetailView(character: characterViewState.character)) {
HStack(spacing: 8) {
if imagePosition == .left {
CharacterCellImage(character: character)
if characterViewState.imagePosition == .left {
CharacterCellImage(imageName: characterViewState.character.image)
}

VStack(alignment: .leading, spacing: 8) {
Text(character.name)
description(for: character)
Text(characterViewState.character.name)
description(for: characterViewState.character)
}

if imagePosition == .right {
CharacterCellImage(character: character)
if characterViewState.imagePosition == .right {
CharacterCellImage(imageName: characterViewState.character.image)
}
Spacer()
}
Expand All @@ -49,18 +48,19 @@ struct CharacterCell: View {
}

struct CharacterCellImage: View {
let character: Character
let imageName: String

var body: some View {
Image(character.image)
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70, height: 70)
}
}

/*
struct CharacterCell_Previews: PreviewProvider {
static var previews: some View {
CharacterCell(character: ricks[0], imagePosition: .left)
}
}
*/
18 changes: 8 additions & 10 deletions Rick-and-Morty/Rick And Morty/Views/CharactersView.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import SwiftUI

struct CharactersView: View {
let characters: [Character]
let title: String
let charactersViewState: CharactersViewState

var body: some View {
NavigationView {
ScrollView(.vertical) {
VStack(alignment: .leading) {
ForEach(characters, id: \.id) { character in
CharacterCell(character: character, imagePosition: getImagePosition(character: character))
ForEach(charactersViewState.characterViewStates) { characterViewState in
CharacterCell(characterViewState: characterViewState)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 120)
}
}
}
.navigationBarTitle(title)
.navigationBarTitle(charactersViewState.title)
}
}

func getImagePosition(character: Character) -> CharacterImagePosition {
character is Rick ? .right : .left
}
}

/*
struct CharactersView_Previews: PreviewProvider {
static var previews: some View {
CharactersView(characters: ricks, title: "Ricks")
//CharactersView(viewModel: CharactersViewModel(characterType: .rick))
//CharactersView(viewModel: CharactersViewModel(characterType: .morty))
}
}
*/
Comment on lines +21 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed. there is no need for commented out code. You have a repo to check for old versions if you really need to :)

15 changes: 7 additions & 8 deletions Rick-and-Morty/Rick And Morty/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@
import SwiftUI

struct ContentView: View {
@ObservedObject var contentViewModel = ContentViewModel()

var body: some View {
TabView {
CharactersView(characters: ricks, title: "Ricks")
.tabItem {
Label("Ricks", image: "rick-icon")
}
CharactersView(characters: morties, title: "Morties")
.tabItem {
Label("Morties", image: "morty-icon")
}
ForEach(contentViewModel.charatersViewStates) { charactersViewState in
CharactersView(charactersViewState: charactersViewState)
.tabItem {
Label(charactersViewState.title, image: charactersViewState.iconName)
}
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions Rick-and-Morty/Rick And Morty/misc/CharactersManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

final class CharactersManager {
func fetchCharacters(characterType: CharacterType) -> [Character] {
return characterType == .rick ? ricks : morties
}
}