From f2d944e88197c1ba9675c529e6d8e164343ce602 Mon Sep 17 00:00:00 2001 From: Jesus Sanz Date: Sat, 7 Jan 2023 12:37:55 +0100 Subject: [PATCH] - Search View (with new episode cell for search) - Search working! - Seasons Watched --- BBTGuide.xcodeproj/project.pbxproj | 12 +++++++ BBTGuide/AllEpisodesView.swift | 15 ++++---- BBTGuide/EpisodesViewModel.swift | 54 ++++++++++++++++++++++------ BBTGuide/FavoriteCell.swift | 56 ++++++++++++++++++++++++++++++ BBTGuide/FavoritesView.swift | 21 ++++++++++- BBTGuide/ModelDefinition.swift | 4 +++ BBTGuide/ModelPersistence.swift | 21 +++++++++++ BBTGuide/SearchEpisodeCell.swift | 52 +++++++++++++++++++++++++++ BBTGuide/SearchView.swift | 43 +++++++++++++++++++++++ BBTGuide/SeasonCell.swift | 4 ++- BBTGuide/TabController.swift | 4 +++ 11 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 BBTGuide/FavoriteCell.swift create mode 100644 BBTGuide/SearchEpisodeCell.swift create mode 100644 BBTGuide/SearchView.swift diff --git a/BBTGuide.xcodeproj/project.pbxproj b/BBTGuide.xcodeproj/project.pbxproj index 2ad979e..57eab0e 100644 --- a/BBTGuide.xcodeproj/project.pbxproj +++ b/BBTGuide.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 097041232947AC7B007910B8 /* SeasonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 097041222947AC7B007910B8 /* SeasonsView.swift */; }; 097041252947AD1B007910B8 /* SeasonsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 097041242947AD1B007910B8 /* SeasonsDetailView.swift */; }; 09A127A329689C65006502F7 /* AllEpisodesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A127A229689C65006502F7 /* AllEpisodesView.swift */; }; + 09A127A529697546006502F7 /* FavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A127A429697546006502F7 /* FavoriteCell.swift */; }; + 09A127A7296980AC006502F7 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A127A6296980AC006502F7 /* SearchView.swift */; }; + 09A127AA2969869A006502F7 /* SearchEpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A127A92969869A006502F7 /* SearchEpisodeCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -47,6 +50,9 @@ 097041222947AC7B007910B8 /* SeasonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonsView.swift; sourceTree = ""; }; 097041242947AD1B007910B8 /* SeasonsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonsDetailView.swift; sourceTree = ""; }; 09A127A229689C65006502F7 /* AllEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllEpisodesView.swift; sourceTree = ""; }; + 09A127A429697546006502F7 /* FavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCell.swift; sourceTree = ""; }; + 09A127A6296980AC006502F7 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + 09A127A92969869A006502F7 /* SearchEpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEpisodeCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -116,6 +122,7 @@ 09418CD0293BBCFB000B34EF /* EpisodeDetailView.swift */, 09418CD2293BBD19000B34EF /* TabController.swift */, 09418CD4293BBD25000B34EF /* FavoritesView.swift */, + 09A127A6296980AC006502F7 /* SearchView.swift */, ); name = Views; sourceTree = ""; @@ -127,6 +134,8 @@ 09418CCE293B7D1D000B34EF /* SeasonCell.swift */, 09395CC92953657100F3EC3D /* RatingView.swift */, 09395CCB295368C000F3EC3D /* RatingViewCell.swift */, + 09A127A429697546006502F7 /* FavoriteCell.swift */, + 09A127A92969869A006502F7 /* SearchEpisodeCell.swift */, ); name = ViewComponents; sourceTree = ""; @@ -237,6 +246,7 @@ buildActionMask = 2147483647; files = ( 09418CD5293BBD25000B34EF /* FavoritesView.swift in Sources */, + 09A127AA2969869A006502F7 /* SearchEpisodeCell.swift in Sources */, 09418CD1293BBCFB000B34EF /* EpisodeDetailView.swift in Sources */, 09395CCA2953657100F3EC3D /* RatingView.swift in Sources */, 09A127A329689C65006502F7 /* AllEpisodesView.swift in Sources */, @@ -249,6 +259,8 @@ 09418CC9293B6728000B34EF /* ModelPersistence.swift in Sources */, 09418CD3293BBD19000B34EF /* TabController.swift in Sources */, 09418CD7293BBD37000B34EF /* DetailViewModel.swift in Sources */, + 09A127A7296980AC006502F7 /* SearchView.swift in Sources */, + 09A127A529697546006502F7 /* FavoriteCell.swift in Sources */, 09418CCD293B7ABC000B34EF /* EpisodeCell.swift in Sources */, 09395CCC295368C000F3EC3D /* RatingViewCell.swift in Sources */, ); diff --git a/BBTGuide/AllEpisodesView.swift b/BBTGuide/AllEpisodesView.swift index a57166c..df1614f 100644 --- a/BBTGuide/AllEpisodesView.swift +++ b/BBTGuide/AllEpisodesView.swift @@ -24,21 +24,22 @@ struct AllEpisodesView: View { } label: { SeasonCell(season: episodes) } - } - .swipeActions(edge: .leading, allowsFullSwipe: true) { - Button { - } label: { - Image(systemName: "eye.circle.fill") + .swipeActions(edge: .leading, allowsFullSwipe: true) { + Button { + episodesVM.toggleWatched(number: episodes.first!.season) + } label: { + Image(systemName: "eye.circle.fill") + } } + .tint(episodesVM.seasonWatched(number: episodes.first!.season) ? .red: .green) } - .tint(.green) + } .listStyle(.sidebar) .navigationDestination(for: Episode.self) { episode in EpisodeDetailView(detailVM: DetailViewModel(episode: episode)) } .navigationTitle("The Big Bang Theory") - .searchable(text: $episodesVM.search) } } } diff --git a/BBTGuide/EpisodesViewModel.swift b/BBTGuide/EpisodesViewModel.swift index 94f6686..c425f66 100644 --- a/BBTGuide/EpisodesViewModel.swift +++ b/BBTGuide/EpisodesViewModel.swift @@ -18,6 +18,12 @@ final class EpisodesViewModel:ObservableObject { @Published var search = "" + @Published var watchedSeasons:WatchedSeasons { + didSet { + persistence.saveWatched(watched: watchedSeasons) + } + } + var orderedEpisodes:[Episode] { return episodes.sorted { $0.number < $1.number @@ -28,18 +34,26 @@ final class EpisodesViewModel:ObservableObject { Dictionary(grouping: episodes) { episode in episode.season }.values.sorted { - $0.first!.season < $1.first!.season - }.map { episodes in - episodes.filter { episode in - if search.isEmpty { - return true - } else { - return episode.name.lowercased().hasPrefix(search.lowercased()) - } - }.sorted { episode1, episode2 in - return episode1.number < episode2.number + $0.first?.season ?? 0 < $1.first?.season ?? 0 + } + } + + var searchSection:[Episode] { + episodes.filter { episode in + return searchEpisode(episode: episode.name.lowercased(), searchField: search.lowercased()) + }.sorted { episode1, episode2 in + return episode1.number < episode2.number + } + } + + var favoriteEpisodes:[Episode] { + var favorites:[Episode] = [] + for episode in episodes { + if episode.favorite { + favorites.append(episode) } } + return favorites } var seasons:[Int] { @@ -48,6 +62,7 @@ final class EpisodesViewModel:ObservableObject { init() { self.episodes = persistence.loadData() + self.watchedSeasons = persistence.loadWatched() } func refresh() { @@ -67,4 +82,23 @@ final class EpisodesViewModel:ObservableObject { func updateView(){ self.objectWillChange.send() } + + func searchEpisode(episode:String, searchField:String) -> Bool { + guard !episode.isEmpty, !searchField.isEmpty, let _ = episode.range(of: searchField) else { + return false + } + return true + } + + func seasonWatched(number:Int) -> Bool { + watchedSeasons.watched.contains(where: { $0 == number }) + } + + func toggleWatched(number:Int) { + if watchedSeasons.watched.contains(where: {$0 == number }) { + watchedSeasons.watched.removeAll(where: {$0 == number }) + } else { + watchedSeasons.watched.append(number) + } + } } diff --git a/BBTGuide/FavoriteCell.swift b/BBTGuide/FavoriteCell.swift new file mode 100644 index 0000000..d3b6642 --- /dev/null +++ b/BBTGuide/FavoriteCell.swift @@ -0,0 +1,56 @@ +// +// FavoriteCell.swift +// BBTGuide +// +// Created by Jesus Sanz on 7/1/23. +// + +import SwiftUI + +struct FavoriteCell: View { + @ObservedObject var detailVM:DetailViewModel + + @Environment(\.colorScheme) var colorScheme + var fillColor: Color { + if colorScheme == .dark { + return Color.black + } else { + return Color.white + } + } + + var body: some View { + HStack { + ZStack(alignment: .bottom) { + Image(detailVM.episode.image) + .resizable() + .scaledToFit() + HStack (){ + Text("Season \(detailVM.episode.season) - Episode \(detailVM.episode.number)") + .bold() + Spacer() + HStack { + RatingViewCell(rating: detailVM.score) + if (detailVM.watched) { + Image(systemName: "eye.circle.fill") + } + } + .bold() + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background { + Rectangle() + .fill(fillColor.opacity(0.9)) + } + } + .cornerRadius(10) + } + } +} + +struct FavoriteCell_Previews: PreviewProvider { + static var previews: some View { + FavoriteCell(detailVM: DetailViewModel(episode: .episodeTest)) + } +} diff --git a/BBTGuide/FavoritesView.swift b/BBTGuide/FavoritesView.swift index 7ec4673..ed86c3a 100644 --- a/BBTGuide/FavoritesView.swift +++ b/BBTGuide/FavoritesView.swift @@ -11,7 +11,26 @@ struct FavoritesView: View { @EnvironmentObject var episodesVM:EpisodesViewModel var body: some View { - ScrollView { + if episodesVM.favoriteEpisodes.isEmpty { + Text("Add some Episodes to your Favorites!") + .bold() + } else { + NavigationStack { + ScrollView { + ForEach(episodesVM.favoriteEpisodes, id:\.self) { episode in + NavigationLink(value: episode) { + FavoriteCell(detailVM: DetailViewModel(episode: episode)) + } + } + } + .buttonStyle(.plain) + .padding() + .navigationTitle("Favorite Episodes") + .navigationDestination(for: Episode.self) { episode in + EpisodeDetailView(detailVM: DetailViewModel(episode: episode)) + } + } + } } } diff --git a/BBTGuide/ModelDefinition.swift b/BBTGuide/ModelDefinition.swift index 240395c..5e0b646 100644 --- a/BBTGuide/ModelDefinition.swift +++ b/BBTGuide/ModelDefinition.swift @@ -33,6 +33,10 @@ struct TempEpisode:Codable, Identifiable, Hashable { let summary: String } +struct WatchedSeasons:Codable { + var watched:[Int] +} + typealias TempEpisodes = [TempEpisode] typealias Episodes = [Episode] diff --git a/BBTGuide/ModelPersistence.swift b/BBTGuide/ModelPersistence.swift index da56cd6..af20697 100644 --- a/BBTGuide/ModelPersistence.swift +++ b/BBTGuide/ModelPersistence.swift @@ -10,6 +10,7 @@ import SwiftUI extension URL { static let episodeDataURL = Bundle.main.url(forResource: "BigBang", withExtension: "json")! static let userDataURL = URL.documentsDirectory.appending(component: "userdata").appendingPathExtension("json") + static let watchedSeasonsURL = URL.documentsDirectory.appending(component: "watchedSeasons").appendingPathExtension("json") } @@ -57,4 +58,24 @@ final class ModelPersistence { saveData(episodes: episodes) } + func loadWatched() -> WatchedSeasons { + do { + let data = try Data(contentsOf: .watchedSeasonsURL) + return try JSONDecoder().decode(WatchedSeasons.self, from: data) + } catch { + print("Error en la carga \(error)") + return WatchedSeasons(watched: []) + } + } + + func saveWatched(watched:WatchedSeasons) { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(watched) + try data.write(to: .watchedSeasonsURL, options: [.atomic, .completeFileProtection]) + } catch { + print("Error saving the watched Seasons data \(error)") + } + } + } diff --git a/BBTGuide/SearchEpisodeCell.swift b/BBTGuide/SearchEpisodeCell.swift new file mode 100644 index 0000000..ce04da8 --- /dev/null +++ b/BBTGuide/SearchEpisodeCell.swift @@ -0,0 +1,52 @@ +// +// SearchEpisodeCell.swift +// BBTGuide +// +// Created by Jesus Sanz on 7/1/23. +// + +import SwiftUI + +struct SearchEpisodeCell: View { + @ObservedObject var detailVM:DetailViewModel + + var body: some View { + HStack { + VStack(alignment: .leading) { + HStack { + Text(detailVM.episode.name) + .font(.headline) + } + Text("Season: \(detailVM.episode.season) - Episode \(detailVM.episode.number)") + .font(.caption) + .padding(.bottom, 10) + Text(detailVM.episode.summary) + .lineLimit(3) + .font(.caption) + .padding(.bottom, 10) + HStack { + VStack (alignment: .leading) { + Text("Runtime: \(detailVM.episode.runtime)") + Text("Air date: \(detailVM.episode.airdate)") + } + .font(.caption) + Spacer() + RatingViewCell(rating: detailVM.score) + Spacer() + if (detailVM.watched) { + Image(systemName: "eye.circle.fill") + } + if (detailVM.favorite) { + Image(systemName: "star.circle.fill") + } + } + } + } + } +} + +struct SearchEpisodeCell_Previews: PreviewProvider { + static var previews: some View { + SearchEpisodeCell(detailVM: DetailViewModel(episode: .episodeTest)) + } +} diff --git a/BBTGuide/SearchView.swift b/BBTGuide/SearchView.swift new file mode 100644 index 0000000..0e72e27 --- /dev/null +++ b/BBTGuide/SearchView.swift @@ -0,0 +1,43 @@ +// +// SearchVIew.swift +// BBTGuide +// +// Created by Jesus Sanz on 7/1/23. +// + +import SwiftUI + +struct SearchView: View { + @EnvironmentObject var episodesVM:EpisodesViewModel + @State var path = NavigationPath() + + var body: some View { + NavigationStack(path: $path) { + if episodesVM.search.isEmpty { + Text("Search for an Episode.") + .bold() + } else { + List { + ForEach(episodesVM.searchSection, id:\.self) { episode in + NavigationLink(value: episode) { + SearchEpisodeCell(detailVM: DetailViewModel(episode: episode)) + } + } + } + .listStyle(.sidebar) + .navigationDestination(for: Episode.self) { episode in + EpisodeDetailView(detailVM: DetailViewModel(episode: episode)) + } + .navigationTitle("Search") + } + } + .searchable(text: $episodesVM.search) + } +} + +struct SearchView_Previews: PreviewProvider { + static var previews: some View { + SearchView() + .environmentObject(EpisodesViewModel()) + } +} diff --git a/BBTGuide/SeasonCell.swift b/BBTGuide/SeasonCell.swift index f0d95a7..53a2d1f 100644 --- a/BBTGuide/SeasonCell.swift +++ b/BBTGuide/SeasonCell.swift @@ -8,6 +8,7 @@ import SwiftUI struct SeasonCell: View { + @EnvironmentObject var episodesVM:EpisodesViewModel let season:[Episode] @Environment(\.colorScheme) var colorScheme @@ -46,7 +47,7 @@ struct SeasonCell: View { .bold() Spacer() HStack { - if (SeasonWatched) { + if (episodesVM.seasonWatched(number: season.first!.season)) { Image(systemName: "eye.circle.fill") } } @@ -67,5 +68,6 @@ struct SeasonCell: View { struct SeasonCell_Previews: PreviewProvider { static var previews: some View { SeasonCell(season: [.episodeTest]) + .environmentObject(EpisodesViewModel()) } } diff --git a/BBTGuide/TabController.swift b/BBTGuide/TabController.swift index e86b53e..1f1037e 100644 --- a/BBTGuide/TabController.swift +++ b/BBTGuide/TabController.swift @@ -18,6 +18,10 @@ struct TabController: View { .tabItem { Label("Favourites", systemImage: "star") } + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } } } }