diff --git a/FlinkChallenge/FlinkChallenge.xcodeproj/project.pbxproj b/FlinkChallenge/FlinkChallenge.xcodeproj/project.pbxproj index 2d129bf..2d94bb5 100644 --- a/FlinkChallenge/FlinkChallenge.xcodeproj/project.pbxproj +++ b/FlinkChallenge/FlinkChallenge.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ BD810CEF23FDB95F00D7853A /* AdvancedFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD810CEE23FDB95F00D7853A /* AdvancedFilterView.swift */; }; BD810D3723FDD82B00D7853A /* CharacterFilteringController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD810D3623FDD82B00D7853A /* CharacterFilteringController.swift */; }; BD810D3923FDDC4200D7853A /* CharacterFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD810D3823FDDC4200D7853A /* CharacterFilterView.swift */; }; + BD810D3C23FDFDFE00D7853A /* Color+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD810D3B23FDFDFE00D7853A /* Color+UIColor.swift */; }; EB91B40C69510498EB2574BC /* Pods_FlinkChallengeTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFA73F2EF25F32DFDB17F885 /* Pods_FlinkChallengeTests.framework */; }; /* End PBXBuildFile section */ @@ -67,6 +68,7 @@ BD810CEE23FDB95F00D7853A /* AdvancedFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedFilterView.swift; sourceTree = ""; }; BD810D3623FDD82B00D7853A /* CharacterFilteringController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterFilteringController.swift; sourceTree = ""; }; BD810D3823FDDC4200D7853A /* CharacterFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterFilterView.swift; sourceTree = ""; }; + BD810D3B23FDFDFE00D7853A /* Color+UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+UIColor.swift"; sourceTree = ""; }; DFA73F2EF25F32DFDB17F885 /* Pods_FlinkChallengeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FlinkChallengeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -124,6 +126,7 @@ BD6A5E3F23FC7FF7003B1E4D /* FlinkChallenge */ = { isa = PBXGroup; children = ( + BD810D3A23FDFDE000D7853A /* Extensions */, BD6A5E6F23FCEB3A003B1E4D /* Controller */, BD6A5E6723FC8EEC003B1E4D /* Views */, BD6A5E6223FC81E2003B1E4D /* Model */, @@ -187,6 +190,14 @@ path = Controller; sourceTree = ""; }; + BD810D3A23FDFDE000D7853A /* Extensions */ = { + isa = PBXGroup; + children = ( + BD810D3B23FDFDFE00D7853A /* Color+UIColor.swift */, + ); + path = Extensions; + sourceTree = ""; + }; DC42C48C82B2AC45F8F56004 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -372,6 +383,7 @@ BD6A5E7323FCEBA0003B1E4D /* Episode.swift in Sources */, BD6A5E7723FD003A003B1E4D /* SearchBarUIView.swift in Sources */, BD6A5E7123FCEB6C003B1E4D /* EpisodeController.swift in Sources */, + BD810D3C23FDFDFE00D7853A /* Color+UIColor.swift in Sources */, BD6A5E4123FC7FF7003B1E4D /* AppDelegate.swift in Sources */, BD6A5E4323FC7FF7003B1E4D /* SceneDelegate.swift in Sources */, BD6A5E6423FC823D003B1E4D /* APICharacter.swift in Sources */, diff --git a/FlinkChallenge/FlinkChallenge/Controller/CharacterFeedController.swift b/FlinkChallenge/FlinkChallenge/Controller/CharacterFeedController.swift index 9d4eb21..a3d4eaf 100644 --- a/FlinkChallenge/FlinkChallenge/Controller/CharacterFeedController.swift +++ b/FlinkChallenge/FlinkChallenge/Controller/CharacterFeedController.swift @@ -33,15 +33,21 @@ class CharacterFeed : ObservableObject, RandomAccessCollection { return characterListItems[position] } + /** + Loads Characters from URL + - Parameter currentItem: Receives the currentItem that is displayed to determine if it should load more characters + */ func loadMoreCharacters(currentItem: APICharacter? = nil) { + // Check if data should or shouldn't be loaded if !shouldLoadMoreData(currentItem: currentItem) { return } + // Check if the page we need to load is ready to be loaded guard case let .ready(page) = loadStatus else { return } - + // Start downloading using URLSession loadStatus = .loading(page: page) let completeURL = "\(baseURL)\(page)" let url = URL(string: completeURL)! @@ -49,11 +55,17 @@ class CharacterFeed : ObservableObject, RandomAccessCollection { task.resume() } + /** + Determines if more characters are needed to be downloaded + - Parameter currentItem: Receives the currentItem that is displayed to determine if it should load more characters + - Returns: True if more characters should be loaded or false otherwise + */ func shouldLoadMoreData(currentItem : APICharacter? = nil) -> Bool{ guard let currentItem = currentItem else { return true } + // If the current character retrieved is within 4 of the last element of the page, we should load more for n in (characterListItems.count - 4)...(characterListItems.count - 1) { if n >= 0 && currentItem.id == characterListItems[n].id { return true @@ -62,33 +74,54 @@ class CharacterFeed : ObservableObject, RandomAccessCollection { return false } + /** + Parses the response from Rick and Morty's API to the required structure + - Parameters: + - data: Response data from the URLRequest Closure + - response: URLResponse object to check for any data we should find useful + - error: Error object to retrieve possible mistakes + */ func parseCharactersFromResponse(data: Data?, response : URLResponse?, error : Error?) { + // Check if any error occurred and pass the status as a bad state guard error == nil else { print("Error: \(error?.localizedDescription ?? "Unknown error produced")") loadStatus = .parseError return } - + // Check if data is present, else, pass it as a bad state guard let data = data else { print("Error: \(error?.localizedDescription ?? "Unknown error produced")") loadStatus = .parseError return } + // Parse Data into an array of characters let apiCharacters = parseCharactersFromData(data: data) + + // Pass characters through main.async to display them on the List DispatchQueue.main.async { + // Pass all new characters to the list self.characterListItems.append(contentsOf: apiCharacters) + // If no more characters are available, we're done if apiCharacters.count == 0 { self.loadStatus = .done } else { + // If we passed all characters and we still HAVE some, something weird is happening guard case let .loading(page) = self.loadStatus else { fatalError("Load status is in a bad state") } + // If everything is done, we can load the next page self.loadStatus = .ready(nextPage: page + 1) } } } + /** + Parses the Data from Rick and Morty's request to an array of characters + - Parameters: + - data: Response data from the URLRequest Closure + - Returns: Array of APICharacters + */ func parseCharactersFromData(data: Data) -> [APICharacter] { var response : APICharactersResponse do { diff --git a/FlinkChallenge/FlinkChallenge/Controller/CharacterFilteringController.swift b/FlinkChallenge/FlinkChallenge/Controller/CharacterFilteringController.swift index 6165036..065a172 100644 --- a/FlinkChallenge/FlinkChallenge/Controller/CharacterFilteringController.swift +++ b/FlinkChallenge/FlinkChallenge/Controller/CharacterFilteringController.swift @@ -26,6 +26,11 @@ class CharacterFiltering : ObservableObject, RandomAccessCollection { return characterListItems[position] } + /** + Retrieves a character array based on passed parameters + - Parameters: + - params: Array of possible search params (name, status, species, type, and gender) + */ func loadFilteredCharacters(params : [String]) { var leadingQueryString : String = "" let labels = ["name","status","species","type","gender"] @@ -44,6 +49,13 @@ class CharacterFiltering : ObservableObject, RandomAccessCollection { task.resume() } + /** + Parses characters from response and error handles any possible issue + - Parameters: + - data: Response data from the URLRequest Closure + - response: URLResponse object to check for any data we should find useful + - error: Error object to retrieve possible mistakes + */ func parseCharactersFromResponse(data: Data?, response : URLResponse?, error : Error?) { guard error == nil else { print("Error: \(error?.localizedDescription ?? "Unknown error produced")") @@ -66,6 +78,12 @@ class CharacterFiltering : ObservableObject, RandomAccessCollection { } } + /** + Parses the Data from Rick and Morty's request to an array of characters + - Parameters: + - data: Response data from the URLRequest Closure + - Returns: Array of APICharacters + */ func parseCharactersFromData(data: Data) -> [APICharacter] { var response : APICharactersResponse do { diff --git a/FlinkChallenge/FlinkChallenge/Controller/EpisodeController.swift b/FlinkChallenge/FlinkChallenge/Controller/EpisodeController.swift index 61c7d41..a1b6b8f 100644 --- a/FlinkChallenge/FlinkChallenge/Controller/EpisodeController.swift +++ b/FlinkChallenge/FlinkChallenge/Controller/EpisodeController.swift @@ -23,7 +23,12 @@ class EpisodeController : ObservableObject, RandomAccessCollection{ getEpisodes(episodes: episodes) } - func getEpisodeFromURL(with url : String) { + /** + Retrieves an episode from a given URL and appends it to the episode list + - Parameters: + - url: Episode URL + */ + private func getEpisodeFromURL(with url : String) { if let url = URL(string: url) { URLSession.shared.dataTask(with: url) { data, response, error in if let data = data { @@ -38,6 +43,11 @@ class EpisodeController : ObservableObject, RandomAccessCollection{ } } + /** + Retrieves all episodes and appends it to the list observable + - Parameters: + - episodes: Episode Array + */ func getEpisodes(episodes : [String]) { for episode in episodes { DispatchQueue.main.async { diff --git a/FlinkChallenge/FlinkChallenge/Extensions/Color+UIColor.swift b/FlinkChallenge/FlinkChallenge/Extensions/Color+UIColor.swift new file mode 100644 index 0000000..a09304b --- /dev/null +++ b/FlinkChallenge/FlinkChallenge/Extensions/Color+UIColor.swift @@ -0,0 +1,130 @@ +// +// Color+UIColor.swift +// FlinkChallenge +// +// Created by Fernando Martin Garcia Del Angel on 19/02/20. +// Copyright © 2020 Fernando Martin Garcia Del Angel. All rights reserved. +// Color bride created by: https://medium.com/@masamichiueta/bridging-uicolor-system-color-to-swiftui-color-ef98f6e21206 +// + +import UIKit +import SwiftUI + +extension Color { + + static var label: Color { + return Color(UIColor.label) + } + + static var secondaryLabel: Color { + return Color(UIColor.secondaryLabel) + } + + static var tertiaryLabel: Color { + return Color(UIColor.tertiaryLabel) + } + + static var quaternaryLabel: Color { + return Color(UIColor.quaternaryLabel) + } + + static var systemFill: Color { + return Color(UIColor.systemFill) + } + + static var secondarySystemFill: Color { + return Color(UIColor.secondarySystemFill) + } + + static var tertiarySystemFill: Color { + return Color(UIColor.tertiarySystemFill) + } + + static var quaternarySystemFill: Color { + return Color(UIColor.quaternarySystemFill) + } + + static var systemBackground: Color { + return Color(UIColor.systemBackground) + } + + static var secondarySystemBackground: Color { + return Color(UIColor.secondarySystemBackground) + } + + static var tertiarySystemBackground: Color { + return Color(UIColor.tertiarySystemBackground) + } + + static var systemGroupedBackground: Color { + return Color(UIColor.systemGroupedBackground) + } + + static var secondarySystemGroupedBackground: Color { + return Color(UIColor.secondarySystemGroupedBackground) + } + + static var tertiarySystemGroupedBackground: Color { + return Color(UIColor.tertiarySystemGroupedBackground) + } + + static var systemRed: Color { + return Color(UIColor.systemRed) + } + + static var systemBlue: Color { + return Color(UIColor.systemBlue) + } + + static var systemPink: Color { + return Color(UIColor.systemPink) + } + + static var systemTeal: Color { + return Color(UIColor.systemTeal) + } + + static var systemGreen: Color { + return Color(UIColor.systemGreen) + } + + static var systemIndigo: Color { + return Color(UIColor.systemIndigo) + } + + static var systemOrange: Color { + return Color(UIColor.systemOrange) + } + + static var systemPurple: Color { + return Color(UIColor.systemPurple) + } + + static var systemYellow: Color { + return Color(UIColor.systemYellow) + } + + static var systemGray: Color { + return Color(UIColor.systemGray) + } + + static var systemGray2: Color { + return Color(UIColor.systemGray2) + } + + static var systemGray3: Color { + return Color(UIColor.systemGray3) + } + + static var systemGray4: Color { + return Color(UIColor.systemGray4) + } + + static var systemGray5: Color { + return Color(UIColor.systemGray5) + } + + static var systemGray6: Color { + return Color(UIColor.systemGray6) + } +} diff --git a/FlinkChallenge/FlinkChallenge/Views/CardView.swift b/FlinkChallenge/FlinkChallenge/Views/CardView.swift index ba0ea31..7e16380 100644 --- a/FlinkChallenge/FlinkChallenge/Views/CardView.swift +++ b/FlinkChallenge/FlinkChallenge/Views/CardView.swift @@ -9,22 +9,26 @@ import SwiftUI import SDWebImageSwiftUI -struct Card: View { - var character : APICharacter +struct Card: View { + var image : String? + var name : String? + var status : String? + var episodeCount : Int? + var body: some View { GeometryReader { geometry in VStack(alignment: .leading) { - WebImage(url: URL(string: self.character.image ?? "")) + WebImage(url: URL(string: self.image ?? "")) .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.height * 0.5) .clipped() HStack { VStack(alignment: .leading, spacing: 6) { - Text(self.character.name ?? "Not nameable character").font(.title).bold() - Text(self.character.status ?? "Unknown").font(.subheadline) - Text("Appears in \(self.character.episode?.count ?? 0) episodes") + Text(self.name ?? "Not nameable character").font(.title).bold() + Text(self.status ?? "Unknown").font(.subheadline) + Text("Appears in \(self.episodeCount ?? 0) episodes") .font(.subheadline) .foregroundColor(.gray) } @@ -32,10 +36,16 @@ struct Card: View { }.padding(.horizontal) }.padding(.bottom) - .background(Color.white) + .background(Color.systemGray6) .cornerRadius(10) .shadow(radius: 5) } } } + +struct CardView_Previews: PreviewProvider { + static var previews: some View { + Card(image: "https://rickandmortyapi.com/api/character/avatar/17.jpeg", name: "Annie", status: "Alive", episodeCount: 5) + } +} diff --git a/FlinkChallenge/FlinkChallenge/Views/CharacterDetailView.swift b/FlinkChallenge/FlinkChallenge/Views/CharacterDetailView.swift index 97d484e..7b59f5d 100644 --- a/FlinkChallenge/FlinkChallenge/Views/CharacterDetailView.swift +++ b/FlinkChallenge/FlinkChallenge/Views/CharacterDetailView.swift @@ -91,7 +91,7 @@ struct DataCard : View { }.padding() }.padding().frame(width: 300, height: 500, alignment: .center) } - .background(Color.white) + .background(Color.systemGray6) .cornerRadius(10) .shadow(radius: 5) .padding(.bottom) @@ -106,7 +106,7 @@ struct EpisodeView : View { Text("(\(episode.episode!))").font(.caption) } .frame(width: 300, height: 100, alignment: .center) - .background(Color.white) + .background(Color.systemGray6) .cornerRadius(10) .shadow(radius: 5) .padding() @@ -139,7 +139,9 @@ struct CharacterDetail: View { MainCharacterAttributes(character: character) }.padding() DataCard(character: character) - Text("Episodes").font(.title).bold() + if episodes.count != 0 { + Text("Episodes").font(.title).bold() + } ScrollView { VStack { ForEach (episodes ,id: \.id) { episode in diff --git a/FlinkChallenge/FlinkChallenge/Views/CharacterFeedView.swift b/FlinkChallenge/FlinkChallenge/Views/CharacterFeedView.swift index 0c414a6..93bc5a2 100644 --- a/FlinkChallenge/FlinkChallenge/Views/CharacterFeedView.swift +++ b/FlinkChallenge/FlinkChallenge/Views/CharacterFeedView.swift @@ -20,7 +20,10 @@ struct CharacterFeedView: View { self.searchText.isEmpty ? true : $0.name!.lowercased().contains(self.searchText.lowercased()) }, id: \.id) { character in ZStack { - Card(character: character).frame(width: 300, height: 300) + Card(image: character.image, + name: character.name, + status: character.status, + episodeCount: character.episode?.count).frame(width: 300, height: 300) .onAppear { self.characterFeed.loadMoreCharacters(currentItem: character) } diff --git a/FlinkChallenge/FlinkChallenge/Views/CharacterFilterView.swift b/FlinkChallenge/FlinkChallenge/Views/CharacterFilterView.swift index a3ae180..629e306 100644 --- a/FlinkChallenge/FlinkChallenge/Views/CharacterFilterView.swift +++ b/FlinkChallenge/FlinkChallenge/Views/CharacterFilterView.swift @@ -24,7 +24,10 @@ struct CharacterFilterView: View { self.searchText.isEmpty ? true : $0.name!.lowercased().contains(self.searchText.lowercased()) }, id: \.id) { character in ZStack { - Card(character: character).frame(width: 300, height: 300) + Card(image: character.image, + name: character.name, + status: character.status, + episodeCount: character.episode?.count).frame(width: 300, height: 300) NavigationLink(destination: CharacterDetail(character: character)) { EmptyView() }.buttonStyle(PlainButtonStyle())