Skip to content

Commit

Permalink
✨ Fixed searchview
Browse files Browse the repository at this point in the history
  • Loading branch information
qeude committed May 18, 2020
1 parent 2eddacc commit 8c15f2a
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 30 deletions.
Binary file modified .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions Notflix.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
010C40562472F74F001EA0F9 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010C40552472F74F001EA0F9 /* UIApplication.swift */; };
0115B062247021A0007D0AAE /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0115B061247021A0007D0AAE /* Actor.swift */; };
0115B06424702921007D0AAE /* MovieCastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0115B06324702921007D0AAE /* MovieCastViewModel.swift */; };
0115B068247029D3007D0AAE /* MovieCastListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0115B067247029D3007D0AAE /* MovieCastListView.swift */; };
Expand Down Expand Up @@ -93,6 +94,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
010C40552472F74F001EA0F9 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
0115B061247021A0007D0AAE /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = "<group>"; };
0115B06324702921007D0AAE /* MovieCastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCastViewModel.swift; sourceTree = "<group>"; };
0115B067247029D3007D0AAE /* MovieCastListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCastListView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -346,6 +348,7 @@
isa = PBXGroup;
children = (
01D00F84241E5F31007EA0B9 /* Color.swift */,
010C40552472F74F001EA0F9 /* UIApplication.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -710,6 +713,7 @@
01553B41234BCA550018CDF3 /* AppDelegate.swift in Sources */,
0115B068247029D3007D0AAE /* MovieCastListView.swift in Sources */,
018401B2241D2F3100EBAB3A /* TVShowCell.swift in Sources */,
010C40562472F74F001EA0F9 /* UIApplication.swift in Sources */,
01751D662424EDB500B04BF4 /* Genre.swift in Sources */,
01553B43234BCA550018CDF3 /* SceneDelegate.swift in Sources */,
011A8F8F23DDD36900C83627 /* APIRequest.swift in Sources */,
Expand Down
19 changes: 19 additions & 0 deletions Notflix/Sources/Extensions/UIApplication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// UIApplication.swift
// Notflix
//
// Created by Quentin Eude on 18/05/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
import UIKit

extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
1 change: 1 addition & 0 deletions Notflix/Sources/View Models/ImageLoaderViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ImageLoaderViewModel: ObservableObject {
}

cancellable = URLSession.shared.dataTaskPublisher(for: url)
.retry(10)
.subscribe(on: ImageLoaderViewModel.imageProcessingQueue)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
Expand Down
62 changes: 46 additions & 16 deletions Notflix/Sources/View Models/Search/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct SearchItemViewModel {
init(tvShow: TVShow) {
self.type = .tvShow
self.id = "\(tvShow.id)_\(self.type)"
self.sourceId = tvShow.id
self.title = tvShow.title
self.posterPath = tvShow.posterPath
self.popularity = tvShow.popularity
Expand All @@ -27,12 +28,14 @@ struct SearchItemViewModel {
init(movie: Movie) {
self.type = .movie
self.id = "\(movie.id)_\(self.type)"
self.sourceId = movie.id
self.title = movie.title
self.posterPath = movie.posterPath
self.popularity = movie.popularity
}

let id: String
let sourceId: Int
let type: SearchItemType
let title: String
let posterPath: String?
Expand All @@ -55,28 +58,55 @@ class SearchViewModel: ObservableObject {
case data
}

@Published var searchText = "" {
didSet {
if !searchText.isEmpty {
//TODO: Add delay to debounce api call
self.performSearch(for: searchText)
} else {
self.items = []
}
}
}
@Published var items = [SearchItemViewModel]()
@Published var searchText = ""
@Published var items = [[SearchItemViewModel]]()
@Published var state: State = .initial

private var disposables = Set<AnyCancellable>()

init() {
$searchText.removeDuplicates()
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.sink { searchText in
if !searchText.isEmpty {
self.performSearch(for: searchText)
} else {
self.items = []
}
}.store(in: &disposables)
}

func performSearch(for text: String) {
self.state = .loading
self.items = []
APIClient().send(APIEndpoints.searchMovies(for: text)).flatMap { response -> AnyPublisher<APIResponseList<TVShow>, Error> in
self.items = response.results.map { SearchItemViewModel(movie: $0)}
var movies = [SearchItemViewModel]()
var tvShows = [SearchItemViewModel]()
APIClient().send(APIEndpoints.searchMovies(for: text)).mapError { error -> Error in
self.state = .error
self.items = []
return error
}
.flatMap { response -> AnyPublisher<APIResponseList<TVShow>, Error> in
movies = response.results.map { SearchItemViewModel(movie: $0)}
return APIClient().send(APIEndpoints.searchTVShows(for: text))
}.sink(receiveCompletion: { (completion) in
}
.mapError { error -> Error in
self.state = .error
self.items = []
return error
}
.map { response -> [SearchItemViewModel] in
tvShows = response.results.map { SearchItemViewModel(tvShow: $0)}
let concatItems = tvShows + movies
if concatItems.isEmpty {
self.state = .data
self.items = []
}
return concatItems.sorted { $0.popularity > $1.popularity }
}
.flatMap { $0.publisher.setFailureType(to: Error.self) }
.collect(3)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure:
self.state = .error
Expand All @@ -86,8 +116,8 @@ class SearchViewModel: ObservableObject {
}
}, receiveValue: { (response) in
self.state = .data
self.items.append(contentsOf: response.results.map { SearchItemViewModel(tvShow: $0)})
self.items += Array(arrayLiteral: response)
})
.store(in: &disposables)
.store(in: &disposables)
}
}
122 changes: 108 additions & 14 deletions Notflix/Sources/Views/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,120 @@ import SwiftUI

struct SearchView: View {
@ObservedObject var searchViewModel = SearchViewModel()
// @State private var searchText = "" {
// didSet {
// if searchText != "" {
// self.searchViewModel.performSearch(for: self.searchText)
// }
// }
// }

init() {
UITableView.appearance().backgroundColor = .black
UITableView.appearance().separatorColor = .black
UITableViewCell.appearance().backgroundColor = .black
}

var body: some View {
ZStack {
Color(.black)
.edgesIgnoringSafeArea(.all)
NavigationView {
ZStack(alignment: .topLeading) {
Color(.black)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .center, spacing: 10) {
SearchBar(text: $searchViewModel.searchText)
if searchViewModel.state == .loading {
loadingView
} else {
if !searchViewModel.items.isEmpty {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 10) {
ForEach(0..<searchViewModel.items.count, id: \.self) { index in
HStack(alignment: .center, spacing: 10) {
ForEach(self.searchViewModel.items[index], id: \.id) { item in
self.cellFor(item)
}
}.frame(maxWidth: .infinity)
}
}
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.endEditing(true)
})
} else {
EmptyView()
}
}
}
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.5)))
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}

var loadingView: some View {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 10) {
SearchBar(text: $searchViewModel.searchText)
List(searchViewModel.items, id: \.id) {
Text("\($0.title)")
ForEach(0..<10, id: \.self) { index in
HStack(alignment: .center, spacing: 10) {
ForEach(0..<3, id: \.self) { item in
self.loadingCell()
}
}.frame(maxWidth: .infinity)
}
}
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.endEditing(true)
})
}

func loadingCell() -> some View {
return VStack(alignment: .leading) {
ShimmerView()
.frame(width: 100, height: 180)
.cornerRadius(8.0)
}
}

func cellFor(_ item: SearchItemViewModel) -> some View {
return Group {
if item.type == SearchItemViewModel.SearchItemType.tvShow {
tvShowCellFor(item)
} else {
movieCellFor(item)
}
}
}

func tvShowCellFor(_ item: SearchItemViewModel) -> some View {
return NavigationLink(destination: TVShowDetails(tvShowId: item.sourceId)) { cellUiFor(item) }
}

func movieCellFor(_ item: SearchItemViewModel) -> some View {
return NavigationLink(destination: MovieDetails(movieId: item.sourceId)) { cellUiFor(item) }
}

func cellUiFor(_ item: SearchItemViewModel) -> some View {
return VStack(alignment: .leading) {
Group {
if item.posterUrl != nil {
AsyncImage(url: item.posterUrl!,
configuration: {AnyView($0.resizable())},
defaultView: {
AnyView(
Text(item.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(.white)
)
}).clipped()
} else {
Text(item.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(.white)
}
}
Text(L10n.Tab.search)
.frame(width: 100, height: 160)
.background(Color.darkGray)
.cornerRadius(8.0)
Text(item.title)
.font(.system(size: 12, weight: .bold))
.lineLimit(1)
.foregroundColor(.white)
.frame(width: 100)
}
}
}
Expand Down

0 comments on commit 8c15f2a

Please sign in to comment.