Skip to content

Commit

Permalink
Building out ViewModels
Browse files Browse the repository at this point in the history
  • Loading branch information
adamahrens committed May 11, 2022
1 parent eed0f87 commit 5d8dd2e
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 27 deletions.
32 changes: 32 additions & 0 deletions RealiOSApps/CoreDataImpl/PetSave.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
4303EB16282210AE000124D9 /* CoreDataPersistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4303EB15282210AE000124D9 /* CoreDataPersistable.swift */; };
4303EB18282212E9000124D9 /* Breed+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4303EB17282212E9000124D9 /* Breed+CoreData.swift */; };
431E768A2822146F00799A6D /* UserDefaultsKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E76892822146F00799A6D /* UserDefaultsKeys.swift */; };
431E768F282B47CE00799A6D /* AnimalsNearYouViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E768E282B47CE00799A6D /* AnimalsNearYouViewModel.swift */; };
431E7692282B4A6400799A6D /* FetchAnimalsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E7691282B4A6400799A6D /* FetchAnimalsService.swift */; };
431E7694282B4C1B00799A6D /* AnimalStoreService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E7693282B4C1B00799A6D /* AnimalStoreService.swift */; };
431E7696282B50BF00799A6D /* AnimalAttributesCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431E7695282B50BF00799A6D /* AnimalAttributesCard.swift */; };
8451378E279623C800A4DA57 /* EmptyResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8451378B279623C800A4DA57 /* EmptyResultsView.swift */; };
8451378F279623C800A4DA57 /* SuggestionsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8451378C279623C800A4DA57 /* SuggestionsGrid.swift */; };
84513790279623C800A4DA57 /* AnimalTypeSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8451378D279623C800A4DA57 /* AnimalTypeSuggestionView.swift */; };
Expand Down Expand Up @@ -114,6 +118,10 @@
4303EB15282210AE000124D9 /* CoreDataPersistable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataPersistable.swift; sourceTree = "<group>"; };
4303EB17282212E9000124D9 /* Breed+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Breed+CoreData.swift"; sourceTree = "<group>"; };
431E76892822146F00799A6D /* UserDefaultsKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsKeys.swift; sourceTree = "<group>"; };
431E768E282B47CE00799A6D /* AnimalsNearYouViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalsNearYouViewModel.swift; sourceTree = "<group>"; };
431E7691282B4A6400799A6D /* FetchAnimalsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnimalsService.swift; sourceTree = "<group>"; };
431E7693282B4C1B00799A6D /* AnimalStoreService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalStoreService.swift; sourceTree = "<group>"; };
431E7695282B50BF00799A6D /* AnimalAttributesCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalAttributesCard.swift; sourceTree = "<group>"; };
8451378B279623C800A4DA57 /* EmptyResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyResultsView.swift; sourceTree = "<group>"; };
8451378C279623C800A4DA57 /* SuggestionsGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestionsGrid.swift; sourceTree = "<group>"; };
8451378D279623C800A4DA57 /* AnimalTypeSuggestionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimalTypeSuggestionView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -260,6 +268,23 @@
path = "Preview Content";
sourceTree = "<group>";
};
431E768D282B47C000799A6D /* viewModels */ = {
isa = PBXGroup;
children = (
431E768E282B47CE00799A6D /* AnimalsNearYouViewModel.swift */,
);
path = viewModels;
sourceTree = "<group>";
};
431E7690282B4A5700799A6D /* services */ = {
isa = PBXGroup;
children = (
431E7691282B4A6400799A6D /* FetchAnimalsService.swift */,
431E7693282B4C1B00799A6D /* AnimalStoreService.swift */,
);
path = services;
sourceTree = "<group>";
};
847F49F426D310C600B94FB8 /* animals */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -374,6 +399,8 @@
AE6A376B269A86560054C842 /* AnimalsNearYou */ = {
isa = PBXGroup;
children = (
431E7690282B4A5700799A6D /* services */,
431E768D282B47C000799A6D /* viewModels */,
AE6A376E269B54A70054C842 /* views */,
);
path = AnimalsNearYou;
Expand Down Expand Up @@ -403,6 +430,7 @@
children = (
8F05DF3926D5797D001ACE04 /* AnimalRow.swift */,
8472DFCB26BF93B3000FEDAF /* AnimalsNearYouView.swift */,
431E7695282B50BF00799A6D /* AnimalAttributesCard.swift */,
);
path = views;
sourceTree = "<group>";
Expand Down Expand Up @@ -769,6 +797,7 @@
AE6A3782269B57940054C842 /* SearchView.swift in Sources */,
8F05DF1A26D56BD6001ACE04 /* RequestType.swift in Sources */,
847F49F626D3403B00B94FB8 /* APIConstants.swift in Sources */,
431E768F282B47CE00799A6D /* AnimalsNearYouViewModel.swift in Sources */,
AE8550C226D5C56100D21317 /* Organization+CoreData.swift in Sources */,
8F05DF2426D56ED1001ACE04 /* APIManager.swift in Sources */,
4303EB16282210AE000124D9 /* CoreDataPersistable.swift in Sources */,
Expand Down Expand Up @@ -796,9 +825,11 @@
AE6A3786269B58350054C842 /* AnimalDetailsView.swift in Sources */,
84A59F7E269E9BDB00237A83 /* AdoptionStatus.swift in Sources */,
847F49F826D3614E00B94FB8 /* Animal.swift in Sources */,
431E7694282B4C1B00799A6D /* AnimalStoreService.swift in Sources */,
AE6A3816269B5DCD0054C842 /* Gender.swift in Sources */,
18D775C522AD944300AE281E /* ContentView.swift in Sources */,
8472DFAB26BF66EF000FEDAF /* LocationManager.swift in Sources */,
431E7696282B50BF00799A6D /* AnimalAttributesCard.swift in Sources */,
AE8550C026D5C56100D21317 /* Address+CoreData.swift in Sources */,
AE8550C526D5C56100D21317 /* PhotoSizes+CoreData.swift in Sources */,
431E768A2822146F00799A6D /* UserDefaultsKeys.swift in Sources */,
Expand All @@ -812,6 +843,7 @@
847F49FA26D3621C00B94FB8 /* AnimalMock.swift in Sources */,
AE8550CB26D5C56100D21317 /* APIColors+CoreData.swift in Sources */,
8472150826C1EC33001FD40B /* AnimalHeaderView.swift in Sources */,
431E7692282B4A6400799A6D /* FetchAnimalsService.swift in Sources */,
8FDD2CA326B311AC00DBE33F /* Pagination.swift in Sources */,
8472DFCC26BF93B3000FEDAF /* AnimalsNearYouView.swift in Sources */,
AE8550CA26D5C56100D21317 /* Contact+CoreData.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Copyright (c) 2022 Razeware LLC
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology. Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
///
/// This project and source code may use libraries or frameworks that are
/// released under various Open-Source licenses. Use of those libraries and
/// frameworks are governed by their own individual licenses.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.

import CoreData

struct AnimalStoreService {
private let context: NSManagedObjectContext

init(context: NSManagedObjectContext) {
self.context = context
}
}

// MARK: - AnimalStore
extension AnimalStoreService: AnimalStore {
func save(animals: [Animal]) async throws {
for var animal in animals {
animal.toManagedObject(context: context)
}

try context.save()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/// Copyright (c) 2022 Razeware LLC
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology. Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
///
/// This project and source code may use libraries or frameworks that are
/// released under various Open-Source licenses. Use of those libraries and
/// frameworks are governed by their own individual licenses.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.

import Foundation

struct FetchAnimalsService {
private let requestManager: RequestManagerProtocol

init(requestManager: RequestManagerProtocol) {
self.requestManager = requestManager
}
}

extension FetchAnimalsService: AnimalsFetcher {
func fetchAnimals(page: Int) async -> [Animal] {
let requestData = AnimalsRequest.getAnimalsWith(page: page, latitude: nil, longitude: nil)
do {
let animalsContainer: AnimalsContainer = try await
requestManager.perform(requestData)
return animalsContainer.animals
} catch {
print(error.localizedDescription)
return []
}
}
}

struct AnimalsFetcherMock: AnimalsFetcher {
func fetchAnimals(page: Int) async -> [Animal] {
Animal.mock
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/// Copyright (c) 2022 Razeware LLC
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology. Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
///
/// This project and source code may use libraries or frameworks that are
/// released under various Open-Source licenses. Use of those libraries and
/// frameworks are governed by their own individual licenses.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.

import Foundation

protocol AnimalsFetcher {
func fetchAnimals(page: Int) async -> [Animal]
}

protocol AnimalStore {
func save(animals: [Animal]) async throws
}

@MainActor
final class AnimalsNearYouViewModel: ObservableObject {
@Published var isLoading = false
@Published var hasMoreAnimals = true

private let animalFetcher: AnimalsFetcher
private let animalStore: AnimalStore
private(set) var page = 1

init(isLoading: Bool = true, animalFetcher: AnimalsFetcher, animalStore: AnimalStore) {
self.isLoading = isLoading
self.animalFetcher = animalFetcher
self.animalStore = animalStore
}

func fetchMoreAnimals() async {
page += 1
await fetchAnimals()
}

func fetchAnimals() async {
let animals = await animalFetcher.fetchAnimals(page: page)

do {
try await animalStore.save(animals: animals)
} catch {
print("Error storing animals... \(error.localizedDescription)")
}

isLoading = false
hasMoreAnimals = !animals.isEmpty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// Copyright (c) 2022 Razeware LLC
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology. Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
///
/// This project and source code may use libraries or frameworks that are
/// released under various Open-Source licenses. Use of those libraries and
/// frameworks are governed by their own individual licenses.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.

import SwiftUI

struct AnimalAttributesCard: ViewModifier {
let color: Color

func body(content: Content) -> some View {
content
.padding(4)
.background(color.opacity(0.2))
.cornerRadius(8)
.foregroundColor(color)
.font(.subheadline)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ import SwiftUI

struct AnimalRow: View {
let animal: AnimalEntity

var animalType: String {
animal.type ?? ""
}

var animalBreedAndType: String {
"\(animal.breed) \(animalType)"
}

var body: some View {
HStack {
Expand All @@ -59,6 +67,24 @@ struct AnimalRow: View {
Text(animal.name ?? "No Name Available")
.multilineTextAlignment(.center)
.font(.title3)

Text(animalBreedAndType)
.font(.callout)

if let description = animal.desc {
Text(description)
.lineLimit(2)
.font(.footnote)
}

HStack {
Text(animal.age.rawValue)
.modifier(AnimalAttributesCard(color: animal.age.color))


Text(animal.gender.rawValue)
.modifier(AnimalAttributesCard(color: .pink))
}
}
.lineLimit(1)
}
Expand Down
Loading

0 comments on commit 5d8dd2e

Please sign in to comment.