├── .DS_Store ├── Netflix Clone ├── .DS_Store ├── Managers │ ├── .DS_Store │ ├── PersistanceDataManager.swift │ ├── DataPersistenceManager.swift │ └── APICaller.swift ├── Assets.xcassets │ ├── Contents.json │ ├── netflixLogo.imageset │ │ ├── netflix_logo.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── heroImage.imageset │ │ ├── https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewModels │ ├── TitleViewModel.swift │ └── TitlePreviewViewModel.swift ├── Resources │ ├── Extensions.swift │ ├── SceneDelegate.swift │ └── AppDelegate.swift ├── NetflixDataModel.xcdatamodeld │ └── NetflixDataModel.xcdatamodel │ │ └── contents ├── Models │ ├── YoutubeSearchResponse.swift │ └── Title.swift ├── Info.plist ├── Views │ ├── TitleCollectionViewCell.swift │ ├── HeroHeaderUIView.swift │ ├── TitleTableViewCell.swift │ └── CollectionViewTableViewCell.swift ├── Controllers │ ├── Core │ │ ├── MainTabBarViewController.swift │ │ ├── UpcomingViewController.swift │ │ ├── DownloadsViewController.swift │ │ ├── SearchViewController.swift │ │ └── HomeViewController.swift │ └── General │ │ ├── SearchResultsViewController.swift │ │ └── TitlePreviewViewController.swift ├── NetflixCloneModel.xcdatamodeld │ └── NetflixCloneModel.xcdatamodel │ │ └── contents └── Base.lproj │ └── LaunchScreen.storyboard ├── Netflix Clone.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── project.pbxproj └── .gitignore /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amrhossam96/Netflix-clone/HEAD/.DS_Store -------------------------------------------------------------------------------- /Netflix Clone/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amrhossam96/Netflix-clone/HEAD/Netflix Clone/.DS_Store -------------------------------------------------------------------------------- /Netflix Clone/Managers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amrhossam96/Netflix-clone/HEAD/Netflix Clone/Managers/.DS_Store -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/netflixLogo.imageset/netflix_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amrhossam96/Netflix-clone/HEAD/Netflix Clone/Assets.xcassets/netflixLogo.imageset/netflix_logo.png -------------------------------------------------------------------------------- /Netflix Clone.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Netflix Clone/ViewModels/TitleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleViewModel.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 24/12/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct TitleViewModel { 12 | let titleName: String 13 | let posterURL: String 14 | } 15 | -------------------------------------------------------------------------------- /Netflix Clone.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Netflix Clone/ViewModels/TitlePreviewViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitlePreviewViewModel.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 07/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TitlePreviewViewModel { 11 | let title: String 12 | let youtubeView: VideoElement 13 | let titleOverview: String 14 | } 15 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/heroImage.imageset/https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amrhossam96/Netflix-clone/HEAD/Netflix Clone/Assets.xcassets/heroImage.imageset/https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg -------------------------------------------------------------------------------- /Netflix Clone/Resources/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 14/12/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension String { 12 | func capitalizeFirstLetter() -> String { 13 | return self.prefix(1).uppercased() + self.lowercased().dropFirst() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Netflix Clone/NetflixDataModel.xcdatamodeld/NetflixDataModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Netflix Clone/Managers/PersistanceDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadManager.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 19/01/2022. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | 12 | 13 | 14 | class DownloadManager { 15 | static let shared = DownloadManager() 16 | 17 | func downloadItem(with model: Title) { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/netflixLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "netflix_logo.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Netflix Clone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SDWebImage", 6 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "8d558cb79f44c00bc75e71a509021b9551941aed", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Netflix Clone/Models/YoutubeSearchResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YoutubeSearchResponse.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 06/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | struct YoutubeSearchResponse: Codable { 13 | let items: [VideoElement] 14 | } 15 | 16 | 17 | struct VideoElement: Codable { 18 | let id: IdVideoElement 19 | } 20 | 21 | 22 | struct IdVideoElement: Codable { 23 | let kind: String 24 | let videoId: String 25 | } 26 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/heroImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Netflix Clone/Models/Title.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Movie.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 08/12/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TrendingTitleResponse: Codable { 11 | let results: [Title] 12 | } 13 | 14 | struct Title: Codable { 15 | let id: Int 16 | let media_type: String? 17 | let original_name: String? 18 | let original_title: String? 19 | let poster_path: String? 20 | let overview: String? 21 | let vote_count: Int 22 | let release_date: String? 23 | let vote_average: Double 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Netflix Clone/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Netflix Clone/Views/TitleCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleCollectionViewCell.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 16/12/2021. 6 | // 7 | 8 | import UIKit 9 | import SDWebImage 10 | 11 | class TitleCollectionViewCell: UICollectionViewCell { 12 | 13 | 14 | static let identifier = "TitleCollectionViewCell" 15 | 16 | private let posterImageView: UIImageView = { 17 | let imageView = UIImageView() 18 | imageView.contentMode = .scaleAspectFill 19 | return imageView 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | contentView.addSubview(posterImageView) 25 | 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | fatalError() 30 | } 31 | 32 | override func layoutSubviews() { 33 | super.layoutSubviews() 34 | posterImageView.frame = contentView.bounds 35 | } 36 | 37 | 38 | public func configure(with model: String) { 39 | 40 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(model)") else { 41 | return 42 | } 43 | 44 | posterImageView.sd_setImage(with: url, completed: nil) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/Core/MainTabBarViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class MainTabBarViewController: UITabBarController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | view.backgroundColor = .systemYellow 15 | 16 | let vc1 = UINavigationController(rootViewController: HomeViewController()) 17 | let vc2 = UINavigationController(rootViewController: UpcomingViewController()) 18 | let vc3 = UINavigationController(rootViewController: SearchViewController()) 19 | let vc4 = UINavigationController(rootViewController: DownloadsViewController()) 20 | 21 | 22 | 23 | vc1.tabBarItem.image = UIImage(systemName: "house") 24 | vc2.tabBarItem.image = UIImage(systemName: "play.circle") 25 | vc3.tabBarItem.image = UIImage(systemName: "magnifyingglass") 26 | vc4.tabBarItem.image = UIImage(systemName: "arrow.down.to.line") 27 | 28 | vc1.title = "Home" 29 | vc2.title = "Coming Soon" 30 | vc3.title = "Top Search" 31 | vc4.title = "Downloads" 32 | 33 | 34 | 35 | tabBar.tintColor = .label 36 | 37 | setViewControllers([vc1, vc2, vc3, vc4], animated: true) 38 | 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Netflix Clone/NetflixCloneModel.xcdatamodeld/NetflixCloneModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Netflix Clone/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Netflix Clone/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Netflix Clone/Resources/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | 17 | guard let windowScene = (scene as? UIWindowScene) else { return } 18 | window = UIWindow(frame: windowScene.coordinateSpace.bounds) 19 | window?.windowScene = windowScene 20 | window?.rootViewController = MainTabBarViewController() 21 | window?.makeKeyAndVisible() 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /Netflix Clone/Managers/DataPersistenceManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataPersistenceManager.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 20/01/2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import CoreData 11 | 12 | 13 | class DataPersistenceManager { 14 | 15 | enum DatabasError: Error { 16 | case failedToSaveData 17 | case failedToFetchData 18 | case failedToDeleteData 19 | } 20 | 21 | static let shared = DataPersistenceManager() 22 | 23 | 24 | func downloadTitleWith(model: Title, completion: @escaping (Result) -> Void) { 25 | 26 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { 27 | return 28 | } 29 | 30 | 31 | let context = appDelegate.persistentContainer.viewContext 32 | 33 | let item = TitleItem(context: context) 34 | 35 | item.original_title = model.original_title 36 | item.id = Int64(model.id) 37 | item.original_name = model.original_name 38 | item.overview = model.overview 39 | item.media_type = model.media_type 40 | item.poster_path = model.poster_path 41 | item.release_date = model.release_date 42 | item.vote_count = Int64(model.vote_count) 43 | item.vote_average = model.vote_average 44 | 45 | 46 | do { 47 | try context.save() 48 | completion(.success(())) 49 | } catch { 50 | completion(.failure(DatabasError.failedToSaveData)) 51 | } 52 | } 53 | 54 | 55 | func fetchingTitlesFromDataBase(completion: @escaping (Result<[TitleItem], Error>) -> Void) { 56 | 57 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { 58 | return 59 | } 60 | 61 | let context = appDelegate.persistentContainer.viewContext 62 | 63 | let request: NSFetchRequest 64 | 65 | request = TitleItem.fetchRequest() 66 | 67 | do { 68 | 69 | let titles = try context.fetch(request) 70 | completion(.success(titles)) 71 | 72 | } catch { 73 | completion(.failure(DatabasError.failedToFetchData)) 74 | } 75 | } 76 | 77 | func deleteTitleWith(model: TitleItem, completion: @escaping (Result)-> Void) { 78 | 79 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { 80 | return 81 | } 82 | 83 | let context = appDelegate.persistentContainer.viewContext 84 | 85 | 86 | context.delete(model) 87 | 88 | do { 89 | try context.save() 90 | completion(.success(())) 91 | } catch { 92 | completion(.failure(DatabasError.failedToDeleteData)) 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/General/SearchResultsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 31/12/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol SearchResultsViewControllerDelegate: AnyObject { 11 | func searchResultsViewControllerDidTapItem(_ viewModel: TitlePreviewViewModel) 12 | } 13 | 14 | class SearchResultsViewController: UIViewController { 15 | 16 | 17 | public var titles: [Title] = [Title]() 18 | 19 | public weak var delegate: SearchResultsViewControllerDelegate? 20 | 21 | public let searchResultsCollectionView: UICollectionView = { 22 | 23 | let layout = UICollectionViewFlowLayout() 24 | layout.itemSize = CGSize(width: UIScreen.main.bounds.width / 3 - 10, height: 200) 25 | layout.minimumInteritemSpacing = 0 26 | 27 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 28 | collectionView.register(TitleCollectionViewCell.self, forCellWithReuseIdentifier: TitleCollectionViewCell.identifier) 29 | return collectionView 30 | }() 31 | 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | 37 | view.backgroundColor = .systemBackground 38 | view.addSubview(searchResultsCollectionView) 39 | 40 | 41 | searchResultsCollectionView.delegate = self 42 | searchResultsCollectionView.dataSource = self 43 | } 44 | 45 | 46 | override func viewDidLayoutSubviews() { 47 | super.viewDidLayoutSubviews() 48 | searchResultsCollectionView.frame = view.bounds 49 | } 50 | 51 | 52 | 53 | 54 | } 55 | 56 | 57 | extension SearchResultsViewController: UICollectionViewDelegate, UICollectionViewDataSource { 58 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 59 | return titles.count 60 | } 61 | 62 | 63 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 64 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TitleCollectionViewCell.identifier, for: indexPath) as? TitleCollectionViewCell else { 65 | return UICollectionViewCell() 66 | } 67 | 68 | 69 | let title = titles[indexPath.row] 70 | cell.configure(with: title.poster_path ?? "") 71 | return cell 72 | } 73 | 74 | 75 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 76 | collectionView.deselectItem(at: indexPath, animated: true) 77 | 78 | let title = titles[indexPath.row] 79 | let titleName = title.original_title ?? "" 80 | APICaller.shared.getMovie(with: titleName) { [weak self] result in 81 | switch result { 82 | case .success(let videoElement): 83 | self?.delegate?.searchResultsViewControllerDidTapItem(TitlePreviewViewModel(title: title.original_title ?? "", youtubeView: videoElement, titleOverview: title.overview ?? "")) 84 | 85 | 86 | case .failure(let error): 87 | print(error.localizedDescription) 88 | } 89 | } 90 | 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Netflix Clone/Views/HeroHeaderUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeroHeaderUIView.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 01/12/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class HeroHeaderUIView: UIView { 11 | 12 | 13 | private let downloadButton: UIButton = { 14 | let button = UIButton() 15 | button.setTitle("Download", for: .normal) 16 | button.layer.borderColor = UIColor.white.cgColor 17 | button.layer.borderWidth = 1 18 | button.translatesAutoresizingMaskIntoConstraints = false 19 | button.layer.cornerRadius = 5 20 | return button 21 | }() 22 | 23 | private let playButton: UIButton = { 24 | 25 | let button = UIButton() 26 | button.setTitle("Play", for: .normal) 27 | button.layer.borderColor = UIColor.white.cgColor 28 | button.layer.borderWidth = 1 29 | button.translatesAutoresizingMaskIntoConstraints = false 30 | button.layer.cornerRadius = 5 31 | return button 32 | }() 33 | 34 | private let heroImageView: UIImageView = { 35 | let imageView = UIImageView() 36 | imageView.contentMode = .scaleAspectFill 37 | imageView.clipsToBounds = true 38 | imageView.image = UIImage(named: "heroImage") 39 | return imageView 40 | }() 41 | 42 | 43 | private func addGradient() { 44 | let gradientLayer = CAGradientLayer() 45 | gradientLayer.colors = [ 46 | UIColor.clear.cgColor, 47 | UIColor.systemBackground.cgColor 48 | ] 49 | gradientLayer.frame = bounds 50 | layer.addSublayer(gradientLayer) 51 | } 52 | 53 | override init(frame: CGRect) { 54 | super.init(frame: frame) 55 | addSubview(heroImageView) 56 | addGradient() 57 | addSubview(playButton) 58 | addSubview(downloadButton) 59 | applyConstraints() 60 | } 61 | 62 | private func applyConstraints() { 63 | 64 | let playButtonConstraints = [ 65 | playButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 70), 66 | playButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50), 67 | playButton.widthAnchor.constraint(equalToConstant: 120) 68 | ] 69 | 70 | let downloadButtonConstraints = [ 71 | downloadButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -70), 72 | downloadButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50), 73 | downloadButton.widthAnchor.constraint(equalToConstant: 120) 74 | ] 75 | 76 | NSLayoutConstraint.activate(playButtonConstraints) 77 | NSLayoutConstraint.activate(downloadButtonConstraints) 78 | } 79 | 80 | 81 | 82 | public func configure(with model: TitleViewModel) { 83 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(model.posterURL)") else { 84 | return 85 | } 86 | 87 | heroImageView.sd_setImage(with: url, completed: nil) 88 | } 89 | 90 | override func layoutSubviews() { 91 | super.layoutSubviews() 92 | heroImageView.frame = bounds 93 | } 94 | 95 | required init?(coder: NSCoder) { 96 | fatalError() 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Netflix Clone/Views/TitleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleTableViewCell.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 24/12/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class TitleTableViewCell: UITableViewCell { 11 | 12 | 13 | static let identifier = "TitleTableViewCell" 14 | 15 | 16 | 17 | private let playTitleButton: UIButton = { 18 | let button = UIButton() 19 | let image = UIImage(systemName: "play.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30)) 20 | button.setImage(image, for: .normal) 21 | button.translatesAutoresizingMaskIntoConstraints = false 22 | button.tintColor = .white 23 | return button 24 | }() 25 | 26 | private let titleLabel: UILabel = { 27 | let label = UILabel() 28 | label.translatesAutoresizingMaskIntoConstraints = false 29 | return label 30 | }() 31 | 32 | private let titlesPosterUIImageView: UIImageView = { 33 | let imageView = UIImageView() 34 | imageView.contentMode = .scaleAspectFill 35 | imageView.translatesAutoresizingMaskIntoConstraints = false 36 | imageView.clipsToBounds = true 37 | return imageView 38 | }() 39 | 40 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 41 | super.init(style: style, reuseIdentifier: reuseIdentifier) 42 | contentView.addSubview(titlesPosterUIImageView) 43 | contentView.addSubview(titleLabel) 44 | contentView.addSubview(playTitleButton) 45 | 46 | applyConstraints() 47 | 48 | } 49 | 50 | 51 | private func applyConstraints() { 52 | let titlesPosterUIImageViewConstraints = [ 53 | titlesPosterUIImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 54 | titlesPosterUIImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), 55 | titlesPosterUIImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), 56 | titlesPosterUIImageView.widthAnchor.constraint(equalToConstant: 100) 57 | ] 58 | 59 | 60 | let titleLabelConstraints = [ 61 | titleLabel.leadingAnchor.constraint(equalTo: titlesPosterUIImageView.trailingAnchor, constant: 20), 62 | titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 63 | ] 64 | 65 | 66 | let playTitleButtonConstraints = [ 67 | playTitleButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), 68 | playTitleButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) 69 | ] 70 | 71 | NSLayoutConstraint.activate(titlesPosterUIImageViewConstraints) 72 | NSLayoutConstraint.activate(titleLabelConstraints) 73 | NSLayoutConstraint.activate(playTitleButtonConstraints) 74 | } 75 | 76 | 77 | 78 | public func configure(with model: TitleViewModel) { 79 | 80 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(model.posterURL)") else { 81 | return 82 | } 83 | titlesPosterUIImageView.sd_setImage(with: url, completed: nil) 84 | titleLabel.text = model.titleName 85 | } 86 | 87 | 88 | 89 | required init?(coder: NSCoder) { 90 | fatalError() 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/Core/UpcomingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpcomingViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class UpcomingViewController: UIViewController { 11 | 12 | 13 | private var titles: [Title] = [Title]() 14 | 15 | private let upcomingTable: UITableView = { 16 | 17 | let table = UITableView() 18 | table.register(TitleTableViewCell.self, forCellReuseIdentifier: TitleTableViewCell.identifier) 19 | return table 20 | }() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | view.backgroundColor = .systemBackground 25 | title = "Upcoming" 26 | navigationController?.navigationBar.prefersLargeTitles = true 27 | navigationController?.navigationItem.largeTitleDisplayMode = .always 28 | 29 | 30 | view.addSubview(upcomingTable) 31 | upcomingTable.delegate = self 32 | upcomingTable.dataSource = self 33 | 34 | fetchUpcoming() 35 | 36 | } 37 | 38 | override func viewDidLayoutSubviews() { 39 | super.viewDidLayoutSubviews() 40 | upcomingTable.frame = view.bounds 41 | } 42 | 43 | 44 | 45 | private func fetchUpcoming() { 46 | APICaller.shared.getUpcomingMovies { [weak self] result in 47 | switch result { 48 | case .success(let titles): 49 | self?.titles = titles 50 | DispatchQueue.main.async { 51 | self?.upcomingTable.reloadData() 52 | } 53 | 54 | case .failure(let error): 55 | print(error.localizedDescription) 56 | } 57 | } 58 | } 59 | } 60 | 61 | 62 | extension UpcomingViewController: UITableViewDelegate, UITableViewDataSource { 63 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 64 | return titles.count 65 | } 66 | 67 | 68 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 69 | 70 | guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleTableViewCell.identifier, for: indexPath) as? TitleTableViewCell else { 71 | return UITableViewCell() 72 | } 73 | 74 | let title = titles[indexPath.row] 75 | cell.configure(with: TitleViewModel(titleName: (title.original_title ?? title.original_name) ?? "Unknown title name", posterURL: title.poster_path ?? "")) 76 | return cell 77 | } 78 | 79 | 80 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 81 | return 140 82 | } 83 | 84 | 85 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 86 | tableView.deselectRow(at: indexPath, animated: true) 87 | 88 | let title = titles[indexPath.row] 89 | 90 | guard let titleName = title.original_title ?? title.original_name else { 91 | return 92 | } 93 | 94 | 95 | APICaller.shared.getMovie(with: titleName) { [weak self] result in 96 | switch result { 97 | case .success(let videoElement): 98 | DispatchQueue.main.async { 99 | let vc = TitlePreviewViewController() 100 | vc.configure(with: TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: title.overview ?? "")) 101 | self?.navigationController?.pushViewController(vc, animated: true) 102 | } 103 | 104 | 105 | case .failure(let error): 106 | print(error.localizedDescription) 107 | } 108 | } 109 | } 110 | 111 | 112 | 113 | 114 | } 115 | 116 | -------------------------------------------------------------------------------- /Netflix Clone/Resources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | // MARK: - Core Data stack 37 | 38 | lazy var persistentContainer: NSPersistentContainer = { // persistent container 39 | /* 40 | The persistent container for the application. This implementation 41 | creates and returns a container, having loaded the store for the 42 | application to it. This property is optional since there are legitimate 43 | error conditions that could cause the creation of the store to fail. 44 | */ 45 | let container = NSPersistentContainer(name: "NetflixCloneModel") 46 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 47 | if let error = error as NSError? { 48 | // Replace this implementation with code to handle the error appropriately. 49 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 50 | 51 | /* 52 | Typical reasons for an error here include: 53 | * The parent directory does not exist, cannot be created, or disallows writing. 54 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 55 | * The device is out of space. 56 | * The store could not be migrated to the current model version. 57 | Check the error message to determine what the actual problem was. 58 | */ 59 | fatalError("Unresolved error \(error), \(error.userInfo)") 60 | } 61 | }) 62 | return container 63 | }() 64 | 65 | // MARK: - Core Data Saving support 66 | 67 | func saveContext () { // context manager 68 | let context = persistentContainer.viewContext 69 | if context.hasChanges { 70 | do { 71 | try context.save() 72 | } catch { 73 | // Replace this implementation with code to handle the error appropriately. 74 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 75 | let nserror = error as NSError 76 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/General/TitlePreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitlePreviewViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 07/01/2022. 6 | // 7 | 8 | import UIKit 9 | import WebKit 10 | 11 | class TitlePreviewViewController: UIViewController { 12 | 13 | 14 | 15 | private let titleLabel: UILabel = { 16 | 17 | let label = UILabel() 18 | label.translatesAutoresizingMaskIntoConstraints = false 19 | label.font = .systemFont(ofSize: 22, weight: .bold) 20 | label.text = "Harry potter" 21 | return label 22 | }() 23 | 24 | private let overviewLabel: UILabel = { 25 | 26 | let label = UILabel() 27 | label.font = .systemFont(ofSize: 18, weight: .regular) 28 | label.translatesAutoresizingMaskIntoConstraints = false 29 | label.numberOfLines = 0 30 | label.text = "This is the best movie ever to watch as a kid!" 31 | return label 32 | }() 33 | 34 | private let downloadButton: UIButton = { 35 | 36 | let button = UIButton() 37 | button.translatesAutoresizingMaskIntoConstraints = false 38 | button.backgroundColor = .red 39 | button.setTitle("Download", for: .normal) 40 | button.setTitleColor(.white, for: .normal) 41 | button.layer.cornerRadius = 8 42 | button.layer.masksToBounds = true 43 | 44 | return button 45 | }() 46 | 47 | private let webView: WKWebView = { 48 | let webView = WKWebView() 49 | webView.translatesAutoresizingMaskIntoConstraints = false 50 | return webView 51 | }() 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | view.backgroundColor = .systemBackground 56 | view.addSubview(webView) 57 | view.addSubview(titleLabel) 58 | view.addSubview(overviewLabel) 59 | view.addSubview(downloadButton) 60 | 61 | configureConstraints() 62 | 63 | 64 | } 65 | 66 | 67 | 68 | func configureConstraints() { 69 | let webViewConstraints = [ 70 | webView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50), 71 | webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 72 | webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 73 | webView.heightAnchor.constraint(equalToConstant: 300) 74 | ] 75 | 76 | let titleLabelConstraints = [ 77 | titleLabel.topAnchor.constraint(equalTo: webView.bottomAnchor, constant: 20), 78 | titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), 79 | ] 80 | 81 | let overviewLabelConstraints = [ 82 | overviewLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15), 83 | overviewLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), 84 | overviewLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor) 85 | ] 86 | 87 | let downloadButtonConstraints = [ 88 | downloadButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), 89 | downloadButton.topAnchor.constraint(equalTo: overviewLabel.bottomAnchor, constant: 25), 90 | downloadButton.widthAnchor.constraint(equalToConstant: 140), 91 | downloadButton.heightAnchor.constraint(equalToConstant: 40) 92 | ] 93 | 94 | NSLayoutConstraint.activate(webViewConstraints) 95 | NSLayoutConstraint.activate(titleLabelConstraints) 96 | NSLayoutConstraint.activate(overviewLabelConstraints) 97 | NSLayoutConstraint.activate(downloadButtonConstraints) 98 | 99 | } 100 | 101 | 102 | public func configure(with model: TitlePreviewViewModel) { 103 | titleLabel.text = model.title 104 | overviewLabel.text = model.titleOverview 105 | 106 | guard let url = URL(string: "https://www.youtube.com/embed/\(model.youtubeView.id.videoId)") else { 107 | return 108 | } 109 | 110 | webView.load(URLRequest(url: url)) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/Core/DownloadsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadsViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class DownloadsViewController: UIViewController { 11 | 12 | 13 | private var titles: [TitleItem] = [TitleItem]() 14 | 15 | private let downloadedTable: UITableView = { 16 | 17 | let table = UITableView() 18 | table.register(TitleTableViewCell.self, forCellReuseIdentifier: TitleTableViewCell.identifier) 19 | return table 20 | }() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | view.backgroundColor = .systemBackground 25 | title = "Downloads" 26 | view.addSubview(downloadedTable) 27 | navigationController?.navigationBar.prefersLargeTitles = true 28 | navigationController?.navigationItem.largeTitleDisplayMode = .always 29 | downloadedTable.delegate = self 30 | downloadedTable.dataSource = self 31 | fetchLocalStorageForDownload() 32 | NotificationCenter.default.addObserver(forName: NSNotification.Name("downloaded"), object: nil, queue: nil) { _ in 33 | self.fetchLocalStorageForDownload() 34 | } 35 | } 36 | 37 | 38 | private func fetchLocalStorageForDownload() { 39 | 40 | 41 | DataPersistenceManager.shared.fetchingTitlesFromDataBase { [weak self] result in 42 | switch result { 43 | case .success(let titles): 44 | self?.titles = titles 45 | DispatchQueue.main.async { 46 | self?.downloadedTable.reloadData() 47 | } 48 | case .failure(let error): 49 | print(error.localizedDescription) 50 | } 51 | } 52 | } 53 | 54 | 55 | override func viewDidLayoutSubviews() { 56 | super.viewDidLayoutSubviews() 57 | downloadedTable.frame = view.bounds 58 | } 59 | 60 | 61 | } 62 | 63 | 64 | extension DownloadsViewController: UITableViewDelegate, UITableViewDataSource { 65 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | return titles.count 67 | } 68 | 69 | 70 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 71 | 72 | guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleTableViewCell.identifier, for: indexPath) as? TitleTableViewCell else { 73 | return UITableViewCell() 74 | } 75 | 76 | let title = titles[indexPath.row] 77 | cell.configure(with: TitleViewModel(titleName: (title.original_title ?? title.original_name) ?? "Unknown title name", posterURL: title.poster_path ?? "")) 78 | return cell 79 | } 80 | 81 | 82 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 83 | return 140 84 | } 85 | 86 | 87 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 88 | switch editingStyle { 89 | case .delete: 90 | 91 | DataPersistenceManager.shared.deleteTitleWith(model: titles[indexPath.row]) { [weak self] result in 92 | switch result { 93 | case .success(): 94 | print("Deleted fromt the database") 95 | case .failure(let error): 96 | print(error.localizedDescription) 97 | } 98 | self?.titles.remove(at: indexPath.row) 99 | tableView.deleteRows(at: [indexPath], with: .fade) 100 | } 101 | default: 102 | break; 103 | } 104 | } 105 | 106 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 107 | tableView.deselectRow(at: indexPath, animated: true) 108 | 109 | let title = titles[indexPath.row] 110 | 111 | guard let titleName = title.original_title ?? title.original_name else { 112 | return 113 | } 114 | 115 | 116 | APICaller.shared.getMovie(with: titleName) { [weak self] result in 117 | switch result { 118 | case .success(let videoElement): 119 | DispatchQueue.main.async { 120 | let vc = TitlePreviewViewController() 121 | vc.configure(with: TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: title.overview ?? "")) 122 | self?.navigationController?.pushViewController(vc, animated: true) 123 | } 124 | 125 | 126 | case .failure(let error): 127 | print(error.localizedDescription) 128 | } 129 | } 130 | } 131 | 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Netflix Clone/Views/CollectionViewTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewTableViewCell.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 21/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | protocol CollectionViewTableViewCellDelegate: AnyObject { 12 | func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel: TitlePreviewViewModel) 13 | } 14 | 15 | class CollectionViewTableViewCell: UITableViewCell { 16 | 17 | static let identifier = "CollectionViewTableViewCell" 18 | 19 | weak var delegate: CollectionViewTableViewCellDelegate? 20 | 21 | private var titles: [Title] = [Title]() 22 | 23 | private let collectionView: UICollectionView = { 24 | let layout = UICollectionViewFlowLayout() 25 | layout.itemSize = CGSize(width: 140, height: 200) 26 | layout.scrollDirection = .horizontal 27 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 28 | collectionView.register(TitleCollectionViewCell.self, forCellWithReuseIdentifier: TitleCollectionViewCell.identifier) 29 | return collectionView 30 | }() 31 | 32 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 33 | super.init(style: style, reuseIdentifier: reuseIdentifier) 34 | contentView.addSubview(collectionView) 35 | 36 | collectionView.delegate = self 37 | collectionView.dataSource = self 38 | } 39 | 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError() 43 | } 44 | 45 | override func layoutSubviews() { 46 | super.layoutSubviews() 47 | collectionView.frame = contentView.bounds 48 | } 49 | 50 | 51 | public func configure(with titles: [Title]) { 52 | self.titles = titles 53 | DispatchQueue.main.async { [weak self] in 54 | self?.collectionView.reloadData() 55 | } 56 | } 57 | 58 | private func downloadTitleAt(indexPath: IndexPath) { 59 | 60 | 61 | DataPersistenceManager.shared.downloadTitleWith(model: titles[indexPath.row]) { result in 62 | switch result { 63 | case .success(): 64 | NotificationCenter.default.post(name: NSNotification.Name("downloaded"), object: nil) 65 | case .failure(let error): 66 | print(error.localizedDescription) 67 | } 68 | } 69 | 70 | 71 | } 72 | } 73 | 74 | 75 | extension CollectionViewTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource { 76 | 77 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 78 | 79 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TitleCollectionViewCell.identifier, for: indexPath) as? TitleCollectionViewCell else { 80 | return UICollectionViewCell() 81 | } 82 | 83 | guard let model = titles[indexPath.row].poster_path else { 84 | return UICollectionViewCell() 85 | } 86 | cell.configure(with: model) 87 | 88 | return cell 89 | } 90 | 91 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 92 | return titles.count 93 | } 94 | 95 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 96 | collectionView.deselectItem(at: indexPath, animated: true) 97 | 98 | let title = titles[indexPath.row] 99 | guard let titleName = title.original_title ?? title.original_name else { 100 | return 101 | } 102 | 103 | 104 | APICaller.shared.getMovie(with: titleName + " trailer") { [weak self] result in 105 | switch result { 106 | case .success(let videoElement): 107 | 108 | let title = self?.titles[indexPath.row] 109 | guard let titleOverview = title?.overview else { 110 | return 111 | } 112 | guard let strongSelf = self else { 113 | return 114 | } 115 | let viewModel = TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: titleOverview) 116 | self?.delegate?.collectionViewTableViewCellDidTapCell(strongSelf, viewModel: viewModel) 117 | 118 | case .failure(let error): 119 | print(error.localizedDescription) 120 | } 121 | 122 | } 123 | } 124 | 125 | func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 126 | 127 | let config = UIContextMenuConfiguration( 128 | identifier: nil, 129 | previewProvider: nil) {[weak self] _ in 130 | let downloadAction = UIAction(title: "Download", subtitle: nil, image: nil, identifier: nil, discoverabilityTitle: nil, state: .off) { _ in 131 | self?.downloadTitleAt(indexPath: indexPath) 132 | } 133 | return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [downloadAction]) 134 | } 135 | 136 | return config 137 | } 138 | 139 | 140 | 141 | 142 | 143 | } 144 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/Core/SearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SearchViewController: UIViewController { 11 | 12 | 13 | private var titles: [Title] = [Title]() 14 | 15 | private let discoverTable: UITableView = { 16 | let table = UITableView() 17 | table.register(TitleTableViewCell.self, forCellReuseIdentifier: TitleTableViewCell.identifier) 18 | return table 19 | }() 20 | 21 | private let searchController: UISearchController = { 22 | let controller = UISearchController(searchResultsController: SearchResultsViewController()) 23 | controller.searchBar.placeholder = "Search for a Movie or a Tv show" 24 | controller.searchBar.searchBarStyle = .minimal 25 | return controller 26 | }() 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | title = "Search" 31 | navigationController?.navigationBar.prefersLargeTitles = true 32 | navigationController?.navigationItem.largeTitleDisplayMode = .always 33 | 34 | view.backgroundColor = .systemBackground 35 | 36 | view.addSubview(discoverTable) 37 | discoverTable.delegate = self 38 | discoverTable.dataSource = self 39 | navigationItem.searchController = searchController 40 | 41 | navigationController?.navigationBar.tintColor = .white 42 | fetchDiscoverMovies() 43 | 44 | searchController.searchResultsUpdater = self 45 | } 46 | 47 | 48 | private func fetchDiscoverMovies() { 49 | APICaller.shared.getDiscoverMovies { [weak self] result in 50 | switch result { 51 | case .success(let titles): 52 | self?.titles = titles 53 | DispatchQueue.main.async { 54 | self?.discoverTable.reloadData() 55 | } 56 | case .failure(let error): 57 | print(error.localizedDescription) 58 | } 59 | } 60 | } 61 | 62 | 63 | override func viewDidLayoutSubviews() { 64 | super.viewDidLayoutSubviews() 65 | discoverTable.frame = view.bounds 66 | } 67 | 68 | 69 | 70 | } 71 | 72 | 73 | extension SearchViewController: UITableViewDataSource, UITableViewDelegate { 74 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 75 | return titles.count; 76 | } 77 | 78 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 79 | 80 | guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleTableViewCell.identifier, for: indexPath) as? TitleTableViewCell else { 81 | return UITableViewCell() 82 | } 83 | 84 | 85 | let title = titles[indexPath.row] 86 | let model = TitleViewModel(titleName: title.original_name ?? title.original_title ?? "Unknown name", posterURL: title.poster_path ?? "") 87 | cell.configure(with: model) 88 | 89 | return cell; 90 | } 91 | 92 | 93 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 94 | return 140 95 | } 96 | 97 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 98 | tableView.deselectRow(at: indexPath, animated: true) 99 | 100 | let title = titles[indexPath.row] 101 | 102 | guard let titleName = title.original_title ?? title.original_name else { 103 | return 104 | } 105 | 106 | 107 | APICaller.shared.getMovie(with: titleName) { [weak self] result in 108 | switch result { 109 | case .success(let videoElement): 110 | DispatchQueue.main.async { 111 | let vc = TitlePreviewViewController() 112 | vc.configure(with: TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: title.overview ?? "")) 113 | self?.navigationController?.pushViewController(vc, animated: true) 114 | } 115 | 116 | 117 | case .failure(let error): 118 | print(error.localizedDescription) 119 | } 120 | } 121 | } 122 | } 123 | 124 | extension SearchViewController: UISearchResultsUpdating, SearchResultsViewControllerDelegate { 125 | 126 | func updateSearchResults(for searchController: UISearchController) { 127 | let searchBar = searchController.searchBar 128 | 129 | guard let query = searchBar.text, 130 | !query.trimmingCharacters(in: .whitespaces).isEmpty, 131 | query.trimmingCharacters(in: .whitespaces).count >= 3, 132 | let resultsController = searchController.searchResultsController as? SearchResultsViewController else { 133 | return 134 | } 135 | resultsController.delegate = self 136 | 137 | APICaller.shared.search(with: query) { result in 138 | DispatchQueue.main.async { 139 | switch result { 140 | case .success(let titles): 141 | resultsController.titles = titles 142 | resultsController.searchResultsCollectionView.reloadData() 143 | case .failure(let error): 144 | print(error.localizedDescription) 145 | } 146 | } 147 | } 148 | } 149 | 150 | 151 | 152 | func searchResultsViewControllerDidTapItem(_ viewModel: TitlePreviewViewModel) { 153 | 154 | DispatchQueue.main.async { [weak self] in 155 | let vc = TitlePreviewViewController() 156 | vc.configure(with: viewModel) 157 | self?.navigationController?.pushViewController(vc, animated: true) 158 | } 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /Netflix Clone/Managers/APICaller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICaller.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 08/12/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct Constants { 12 | static let API_KEY = "697d439ac993538da4e3e60b54e762cd" 13 | static let baseURL = "https://api.themoviedb.org" 14 | static let YoutubeAPI_KEY = "AIzaSyDqX8axTGeNpXRiISTGL7Tya7fjKJDYi4g" 15 | static let YoutubeBaseURL = "https://youtube.googleapis.com/youtube/v3/search?" 16 | } 17 | 18 | enum APIError: Error { 19 | case failedTogetData 20 | } 21 | 22 | class APICaller { 23 | static let shared = APICaller() 24 | 25 | 26 | 27 | func getTrendingMovies(completion: @escaping (Result<[Title], Error>) -> Void) { 28 | guard let url = URL(string: "\(Constants.baseURL)/3/trending/movie/day?api_key=\(Constants.API_KEY)") else {return} 29 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 30 | guard let data = data, error == nil else { 31 | return 32 | } 33 | 34 | do { 35 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 36 | completion(.success(results.results)) 37 | 38 | } catch { 39 | completion(.failure(APIError.failedTogetData)) 40 | } 41 | } 42 | 43 | task.resume() 44 | } 45 | 46 | 47 | func getTrendingTvs(completion: @escaping (Result<[Title], Error>) -> Void) { 48 | guard let url = URL(string: "\(Constants.baseURL)/3/trending/tv/day?api_key=\(Constants.API_KEY)") else {return} 49 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 50 | guard let data = data, error == nil else { 51 | return 52 | } 53 | 54 | do { 55 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 56 | completion(.success(results.results)) 57 | } 58 | catch { 59 | completion(.failure(APIError.failedTogetData)) 60 | } 61 | } 62 | 63 | task.resume() 64 | } 65 | 66 | 67 | func getUpcomingMovies(completion: @escaping (Result<[Title], Error>) -> Void) { 68 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/upcoming?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {return} 69 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 70 | guard let data = data, error == nil else { 71 | return 72 | } 73 | 74 | do { 75 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 76 | completion(.success(results.results)) 77 | } catch { 78 | print(error.localizedDescription) 79 | } 80 | 81 | } 82 | task.resume() 83 | } 84 | 85 | func getPopular(completion: @escaping (Result<[Title], Error>) -> Void) { 86 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/popular?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {return} 87 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 88 | guard let data = data, error == nil else { 89 | return 90 | } 91 | 92 | do { 93 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 94 | completion(.success(results.results)) 95 | } catch { 96 | completion(.failure(APIError.failedTogetData)) 97 | } 98 | } 99 | 100 | task.resume() 101 | } 102 | 103 | func getTopRated(completion: @escaping (Result<[Title], Error>) -> Void) { 104 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/top_rated?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {return } 105 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 106 | guard let data = data, error == nil else { 107 | return 108 | } 109 | 110 | do { 111 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 112 | completion(.success(results.results)) 113 | 114 | } catch { 115 | completion(.failure(APIError.failedTogetData)) 116 | } 117 | 118 | } 119 | task.resume() 120 | } 121 | 122 | 123 | func getDiscoverMovies(completion: @escaping (Result<[Title], Error>) -> Void) { 124 | guard let url = URL(string: "\(Constants.baseURL)/3/discover/movie?api_key=\(Constants.API_KEY)&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_watch_monetization_types=flatrate") else {return } 125 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 126 | guard let data = data, error == nil else { 127 | return 128 | } 129 | 130 | do { 131 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 132 | completion(.success(results.results)) 133 | 134 | } catch { 135 | completion(.failure(APIError.failedTogetData)) 136 | } 137 | 138 | } 139 | task.resume() 140 | } 141 | 142 | 143 | func search(with query: String, completion: @escaping (Result<[Title], Error>) -> Void) { 144 | 145 | guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {return} 146 | guard let url = URL(string: "\(Constants.baseURL)/3/search/movie?api_key=\(Constants.API_KEY)&query=\(query)") else { 147 | return 148 | } 149 | 150 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 151 | guard let data = data, error == nil else { 152 | return 153 | } 154 | 155 | do { 156 | let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data) 157 | completion(.success(results.results)) 158 | 159 | } catch { 160 | completion(.failure(APIError.failedTogetData)) 161 | } 162 | 163 | } 164 | task.resume() 165 | } 166 | 167 | 168 | func getMovie(with query: String, completion: @escaping (Result) -> Void) { 169 | 170 | 171 | guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {return} 172 | guard let url = URL(string: "\(Constants.YoutubeBaseURL)q=\(query)&key=\(Constants.YoutubeAPI_KEY)") else {return} 173 | let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in 174 | guard let data = data, error == nil else { 175 | return 176 | } 177 | 178 | do { 179 | let results = try JSONDecoder().decode(YoutubeSearchResponse.self, from: data) 180 | 181 | completion(.success(results.items[0])) 182 | 183 | 184 | } catch { 185 | completion(.failure(error)) 186 | print(error.localizedDescription) 187 | } 188 | 189 | } 190 | task.resume() 191 | } 192 | 193 | } 194 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /Netflix Clone/Controllers/Core/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // Netflix Clone 4 | // 5 | // Created by Amr Hossam on 04/11/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | 12 | enum Sections: Int { 13 | case TrendingMovies = 0 14 | case TrendingTv = 1 15 | case Popular = 2 16 | case Upcoming = 3 17 | case TopRated = 4 18 | } 19 | 20 | 21 | 22 | class HomeViewController: UIViewController { 23 | 24 | 25 | 26 | private var randomTrendingMovie: Title? 27 | private var headerView: HeroHeaderUIView? 28 | 29 | let sectionTitles: [String] = ["Trending Movies", "Trending Tv", "Popular", "Upcoming Movies", "Top rated"] 30 | 31 | private let homeFeedTable: UITableView = { 32 | let table = UITableView(frame: .zero, style: .grouped) 33 | table.register(CollectionViewTableViewCell.self, forCellReuseIdentifier: CollectionViewTableViewCell.identifier) 34 | return table 35 | }() 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | view.backgroundColor = .systemBackground 40 | view.addSubview(homeFeedTable) 41 | homeFeedTable.delegate = self 42 | homeFeedTable.dataSource = self 43 | 44 | configureNavbar() 45 | 46 | headerView = HeroHeaderUIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 500)) 47 | homeFeedTable.tableHeaderView = headerView 48 | configureHeroHeaderView() 49 | 50 | } 51 | 52 | private func configureHeroHeaderView() { 53 | 54 | APICaller.shared.getTrendingMovies { [weak self] result in 55 | switch result { 56 | case .success(let titles): 57 | let selectedTitle = titles.randomElement() 58 | 59 | self?.randomTrendingMovie = selectedTitle 60 | self?.headerView?.configure(with: TitleViewModel(titleName: selectedTitle?.original_title ?? "", posterURL: selectedTitle?.poster_path ?? "")) 61 | 62 | case .failure(let erorr): 63 | print(erorr.localizedDescription) 64 | } 65 | } 66 | 67 | 68 | 69 | } 70 | 71 | 72 | 73 | private func configureNavbar() { 74 | var image = UIImage(named: "netflixLogo") 75 | image = image?.withRenderingMode(.alwaysOriginal) 76 | navigationItem.leftBarButtonItem = UIBarButtonItem(image: image, style: .done, target: self, action: nil) 77 | 78 | navigationItem.rightBarButtonItems = [ 79 | UIBarButtonItem(image: UIImage(systemName: "person"), style: .done, target: self, action: nil), 80 | UIBarButtonItem(image: UIImage(systemName: "play.rectangle"), style: .done, target: self, action: nil) 81 | ] 82 | navigationController?.navigationBar.tintColor = .white 83 | } 84 | 85 | 86 | 87 | override func viewDidLayoutSubviews() { 88 | super.viewDidLayoutSubviews() 89 | homeFeedTable.frame = view.bounds 90 | } 91 | 92 | 93 | 94 | } 95 | 96 | 97 | extension HomeViewController: UITableViewDelegate, UITableViewDataSource { 98 | 99 | func numberOfSections(in tableView: UITableView) -> Int { 100 | return sectionTitles.count 101 | } 102 | 103 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 104 | return 1 105 | } 106 | 107 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 108 | 109 | guard let cell = tableView.dequeueReusableCell(withIdentifier: CollectionViewTableViewCell.identifier, for: indexPath) as? CollectionViewTableViewCell else { 110 | return UITableViewCell() 111 | } 112 | 113 | cell.delegate = self 114 | 115 | switch indexPath.section { 116 | case Sections.TrendingMovies.rawValue: 117 | APICaller.shared.getTrendingMovies { result in 118 | switch result { 119 | 120 | case .success(let titles): 121 | cell.configure(with: titles) 122 | case .failure(let error): 123 | print(error.localizedDescription) 124 | } 125 | } 126 | 127 | 128 | 129 | case Sections.TrendingTv.rawValue: 130 | APICaller.shared.getTrendingTvs { result in 131 | switch result { 132 | case .success(let titles): 133 | cell.configure(with: titles) 134 | case .failure(let error): 135 | print(error.localizedDescription) 136 | } 137 | } 138 | case Sections.Popular.rawValue: 139 | APICaller.shared.getPopular { result in 140 | switch result { 141 | case .success(let titles): 142 | cell.configure(with: titles) 143 | case .failure(let error): 144 | print(error.localizedDescription) 145 | } 146 | } 147 | case Sections.Upcoming.rawValue: 148 | 149 | APICaller.shared.getUpcomingMovies { result in 150 | switch result { 151 | case .success(let titles): 152 | cell.configure(with: titles) 153 | case .failure(let error): 154 | print(error.localizedDescription) 155 | } 156 | } 157 | 158 | case Sections.TopRated.rawValue: 159 | APICaller.shared.getTopRated { result in 160 | switch result { 161 | case .success(let titles): 162 | cell.configure(with: titles) 163 | case .failure(let error): 164 | print(error) 165 | } 166 | } 167 | default: 168 | return UITableViewCell() 169 | 170 | } 171 | 172 | return cell 173 | } 174 | 175 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 176 | return 200 177 | } 178 | 179 | 180 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 181 | return 40 182 | } 183 | 184 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 185 | guard let header = view as? UITableViewHeaderFooterView else {return} 186 | header.textLabel?.font = .systemFont(ofSize: 18, weight: .semibold) 187 | header.textLabel?.frame = CGRect(x: header.bounds.origin.x + 20, y: header.bounds.origin.y, width: 100, height: header.bounds.height) 188 | header.textLabel?.textColor = .white 189 | header.textLabel?.text = header.textLabel?.text?.capitalizeFirstLetter() 190 | } 191 | 192 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 193 | return sectionTitles[section] 194 | } 195 | 196 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 197 | let defaultOffset = view.safeAreaInsets.top 198 | let offset = scrollView.contentOffset.y + defaultOffset 199 | 200 | navigationController?.navigationBar.transform = .init(translationX: 0, y: min(0, -offset)) 201 | } 202 | } 203 | 204 | 205 | 206 | extension HomeViewController: CollectionViewTableViewCellDelegate { 207 | func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel: TitlePreviewViewModel) { 208 | DispatchQueue.main.async { [weak self] in 209 | let vc = TitlePreviewViewController() 210 | vc.configure(with: viewModel) 211 | self?.navigationController?.pushViewController(vc, animated: true) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Netflix Clone.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A1942D0274A620100B3FA93 /* CollectionViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1942CF274A620100B3FA93 /* CollectionViewTableViewCell.swift */; }; 11 | 0A28420C2799EBED0023560C /* NetflixCloneModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0A28420A2799EBED0023560C /* NetflixCloneModel.xcdatamodeld */; }; 12 | 0A28420E2799EDB10023560C /* DataPersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A28420D2799EDB10023560C /* DataPersistenceManager.swift */; }; 13 | 0A5AAA9D2769057B00149719 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5AAA9C2769057B00149719 /* Extensions.swift */; }; 14 | 0A5AAAA7276AE1D500149719 /* TitleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5AAAA6276AE1D500149719 /* TitleCollectionViewCell.swift */; }; 15 | 0A5AAAAA276AE2E600149719 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 0A5AAAA9276AE2E600149719 /* SDWebImage */; }; 16 | 0A5AAAAC276AE2E600149719 /* SDWebImageMapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0A5AAAAB276AE2E600149719 /* SDWebImageMapKit */; }; 17 | 0A6209A72734603E00A88D71 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209A62734603E00A88D71 /* AppDelegate.swift */; }; 18 | 0A6209A92734603E00A88D71 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209A82734603E00A88D71 /* SceneDelegate.swift */; }; 19 | 0A6209AB2734603E00A88D71 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209AA2734603E00A88D71 /* MainTabBarViewController.swift */; }; 20 | 0A6209B02734603E00A88D71 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A6209AF2734603E00A88D71 /* Assets.xcassets */; }; 21 | 0A6209B32734603E00A88D71 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0A6209B12734603E00A88D71 /* LaunchScreen.storyboard */; }; 22 | 0A6209BC2734615B00A88D71 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209BB2734615B00A88D71 /* HomeViewController.swift */; }; 23 | 0A6209BE2734617600A88D71 /* UpcomingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209BD2734617600A88D71 /* UpcomingViewController.swift */; }; 24 | 0A6209C02734618C00A88D71 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209BF2734618C00A88D71 /* SearchViewController.swift */; }; 25 | 0A6209C2273461A800A88D71 /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6209C1273461A800A88D71 /* DownloadsViewController.swift */; }; 26 | 0A6291FA2756F1B900082232 /* HeroHeaderUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6291F92756F1B900082232 /* HeroHeaderUIView.swift */; }; 27 | 0A7C94D527764E8C0072AE85 /* TitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7C94D427764E8C0072AE85 /* TitleTableViewCell.swift */; }; 28 | 0A7C94D72776507A0072AE85 /* TitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7C94D62776507A0072AE85 /* TitleViewModel.swift */; }; 29 | 0AD8A1BE2786FDB000612F1E /* YoutubeSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD8A1BD2786FDB000612F1E /* YoutubeSearchResponse.swift */; }; 30 | 0AD9A100276047A900131EEF /* APICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD9A0FF276047A900131EEF /* APICaller.swift */; }; 31 | 0AD9A10427604E4D00131EEF /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD9A10327604E4D00131EEF /* Title.swift */; }; 32 | 0AE30378277F600E00CBC18F /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE30377277F600E00CBC18F /* SearchResultsViewController.swift */; }; 33 | 0AF8A46B2787BC2100FFE877 /* TitlePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF8A46A2787BC2100FFE877 /* TitlePreviewViewController.swift */; }; 34 | 0AF8A46D2787BEA400FFE877 /* TitlePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF8A46C2787BEA400FFE877 /* TitlePreviewViewModel.swift */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 0A1942CF274A620100B3FA93 /* CollectionViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewTableViewCell.swift; sourceTree = ""; }; 39 | 0A28420B2799EBED0023560C /* NetflixCloneModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = NetflixCloneModel.xcdatamodel; sourceTree = ""; }; 40 | 0A28420D2799EDB10023560C /* DataPersistenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPersistenceManager.swift; sourceTree = ""; }; 41 | 0A5AAA9C2769057B00149719 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 42 | 0A5AAAA6276AE1D500149719 /* TitleCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleCollectionViewCell.swift; sourceTree = ""; }; 43 | 0A6209A32734603E00A88D71 /* Netflix Clone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Netflix Clone.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 0A6209A62734603E00A88D71 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | 0A6209A82734603E00A88D71 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 46 | 0A6209AA2734603E00A88D71 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 47 | 0A6209AF2734603E00A88D71 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 0A6209B22734603E00A88D71 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 0A6209B42734603E00A88D71 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 0A6209BB2734615B00A88D71 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 51 | 0A6209BD2734617600A88D71 /* UpcomingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpcomingViewController.swift; sourceTree = ""; }; 52 | 0A6209BF2734618C00A88D71 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 53 | 0A6209C1273461A800A88D71 /* DownloadsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewController.swift; sourceTree = ""; }; 54 | 0A6291F92756F1B900082232 /* HeroHeaderUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroHeaderUIView.swift; sourceTree = ""; }; 55 | 0A7C94D427764E8C0072AE85 /* TitleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleTableViewCell.swift; sourceTree = ""; }; 56 | 0A7C94D62776507A0072AE85 /* TitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleViewModel.swift; sourceTree = ""; }; 57 | 0AD8A1BD2786FDB000612F1E /* YoutubeSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeSearchResponse.swift; sourceTree = ""; }; 58 | 0AD9A0FF276047A900131EEF /* APICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICaller.swift; sourceTree = ""; }; 59 | 0AD9A10327604E4D00131EEF /* Title.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Title.swift; sourceTree = ""; }; 60 | 0AE30377277F600E00CBC18F /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; 61 | 0AF8A46A2787BC2100FFE877 /* TitlePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlePreviewViewController.swift; sourceTree = ""; }; 62 | 0AF8A46C2787BEA400FFE877 /* TitlePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlePreviewViewModel.swift; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | 0A6209A02734603E00A88D71 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | 0A5AAAAC276AE2E600149719 /* SDWebImageMapKit in Frameworks */, 71 | 0A5AAAAA276AE2E600149719 /* SDWebImage in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 0A1942CA274A619400B3FA93 /* Views */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 0A1942CF274A620100B3FA93 /* CollectionViewTableViewCell.swift */, 82 | 0A6291F92756F1B900082232 /* HeroHeaderUIView.swift */, 83 | 0A5AAAA6276AE1D500149719 /* TitleCollectionViewCell.swift */, 84 | 0A7C94D427764E8C0072AE85 /* TitleTableViewCell.swift */, 85 | ); 86 | path = Views; 87 | sourceTree = ""; 88 | }; 89 | 0A1942CB274A619B00B3FA93 /* ViewModels */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 0A7C94D62776507A0072AE85 /* TitleViewModel.swift */, 93 | 0AF8A46C2787BEA400FFE877 /* TitlePreviewViewModel.swift */, 94 | ); 95 | path = ViewModels; 96 | sourceTree = ""; 97 | }; 98 | 0A1942CC274A61A100B3FA93 /* Models */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 0AD9A10327604E4D00131EEF /* Title.swift */, 102 | 0AD8A1BD2786FDB000612F1E /* YoutubeSearchResponse.swift */, 103 | ); 104 | path = Models; 105 | sourceTree = ""; 106 | }; 107 | 0A1942CD274A61C000B3FA93 /* Resources */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 0A5AAA9C2769057B00149719 /* Extensions.swift */, 111 | 0A6209A62734603E00A88D71 /* AppDelegate.swift */, 112 | 0A6209A82734603E00A88D71 /* SceneDelegate.swift */, 113 | ); 114 | path = Resources; 115 | sourceTree = ""; 116 | }; 117 | 0A1942CE274A61C600B3FA93 /* Managers */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 0AD9A0FF276047A900131EEF /* APICaller.swift */, 121 | 0A28420D2799EDB10023560C /* DataPersistenceManager.swift */, 122 | ); 123 | path = Managers; 124 | sourceTree = ""; 125 | }; 126 | 0A62099A2734603D00A88D71 = { 127 | isa = PBXGroup; 128 | children = ( 129 | 0A6209A52734603E00A88D71 /* Netflix Clone */, 130 | 0A6209A42734603E00A88D71 /* Products */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | 0A6209A42734603E00A88D71 /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 0A6209A32734603E00A88D71 /* Netflix Clone.app */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 0A6209A52734603E00A88D71 /* Netflix Clone */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 0A1942CE274A61C600B3FA93 /* Managers */, 146 | 0A1942CD274A61C000B3FA93 /* Resources */, 147 | 0A1942CC274A61A100B3FA93 /* Models */, 148 | 0A1942CA274A619400B3FA93 /* Views */, 149 | 0A1942CB274A619B00B3FA93 /* ViewModels */, 150 | 0A6209BA2734614400A88D71 /* Controllers */, 151 | 0A6209AF2734603E00A88D71 /* Assets.xcassets */, 152 | 0A6209B12734603E00A88D71 /* LaunchScreen.storyboard */, 153 | 0A6209B42734603E00A88D71 /* Info.plist */, 154 | 0A28420A2799EBED0023560C /* NetflixCloneModel.xcdatamodeld */, 155 | ); 156 | path = "Netflix Clone"; 157 | sourceTree = ""; 158 | }; 159 | 0A6209BA2734614400A88D71 /* Controllers */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 0AE3037A277F604600CBC18F /* General */, 163 | 0AE30379277F603200CBC18F /* Core */, 164 | ); 165 | path = Controllers; 166 | sourceTree = ""; 167 | }; 168 | 0AE30379277F603200CBC18F /* Core */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 0A6209BB2734615B00A88D71 /* HomeViewController.swift */, 172 | 0A6209BD2734617600A88D71 /* UpcomingViewController.swift */, 173 | 0A6209BF2734618C00A88D71 /* SearchViewController.swift */, 174 | 0A6209AA2734603E00A88D71 /* MainTabBarViewController.swift */, 175 | 0A6209C1273461A800A88D71 /* DownloadsViewController.swift */, 176 | ); 177 | path = Core; 178 | sourceTree = ""; 179 | }; 180 | 0AE3037A277F604600CBC18F /* General */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 0AE30377277F600E00CBC18F /* SearchResultsViewController.swift */, 184 | 0AF8A46A2787BC2100FFE877 /* TitlePreviewViewController.swift */, 185 | ); 186 | path = General; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXGroup section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | 0A6209A22734603E00A88D71 /* Netflix Clone */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = 0A6209B72734603E00A88D71 /* Build configuration list for PBXNativeTarget "Netflix Clone" */; 195 | buildPhases = ( 196 | 0A62099F2734603E00A88D71 /* Sources */, 197 | 0A6209A02734603E00A88D71 /* Frameworks */, 198 | 0A6209A12734603E00A88D71 /* Resources */, 199 | ); 200 | buildRules = ( 201 | ); 202 | dependencies = ( 203 | ); 204 | name = "Netflix Clone"; 205 | packageProductDependencies = ( 206 | 0A5AAAA9276AE2E600149719 /* SDWebImage */, 207 | 0A5AAAAB276AE2E600149719 /* SDWebImageMapKit */, 208 | ); 209 | productName = "Netflix Clone"; 210 | productReference = 0A6209A32734603E00A88D71 /* Netflix Clone.app */; 211 | productType = "com.apple.product-type.application"; 212 | }; 213 | /* End PBXNativeTarget section */ 214 | 215 | /* Begin PBXProject section */ 216 | 0A62099B2734603D00A88D71 /* Project object */ = { 217 | isa = PBXProject; 218 | attributes = { 219 | BuildIndependentTargetsInParallel = 1; 220 | LastSwiftUpdateCheck = 1300; 221 | LastUpgradeCheck = 1300; 222 | TargetAttributes = { 223 | 0A6209A22734603E00A88D71 = { 224 | CreatedOnToolsVersion = 13.0; 225 | }; 226 | }; 227 | }; 228 | buildConfigurationList = 0A62099E2734603D00A88D71 /* Build configuration list for PBXProject "Netflix Clone" */; 229 | compatibilityVersion = "Xcode 13.0"; 230 | developmentRegion = en; 231 | hasScannedForEncodings = 0; 232 | knownRegions = ( 233 | en, 234 | Base, 235 | ); 236 | mainGroup = 0A62099A2734603D00A88D71; 237 | packageReferences = ( 238 | 0A5AAAA8276AE2E600149719 /* XCRemoteSwiftPackageReference "SDWebImage" */, 239 | ); 240 | productRefGroup = 0A6209A42734603E00A88D71 /* Products */; 241 | projectDirPath = ""; 242 | projectRoot = ""; 243 | targets = ( 244 | 0A6209A22734603E00A88D71 /* Netflix Clone */, 245 | ); 246 | }; 247 | /* End PBXProject section */ 248 | 249 | /* Begin PBXResourcesBuildPhase section */ 250 | 0A6209A12734603E00A88D71 /* Resources */ = { 251 | isa = PBXResourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | 0A6209B32734603E00A88D71 /* LaunchScreen.storyboard in Resources */, 255 | 0A6209B02734603E00A88D71 /* Assets.xcassets in Resources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | /* End PBXResourcesBuildPhase section */ 260 | 261 | /* Begin PBXSourcesBuildPhase section */ 262 | 0A62099F2734603E00A88D71 /* Sources */ = { 263 | isa = PBXSourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | 0A7C94D527764E8C0072AE85 /* TitleTableViewCell.swift in Sources */, 267 | 0A6209C02734618C00A88D71 /* SearchViewController.swift in Sources */, 268 | 0A5AAAA7276AE1D500149719 /* TitleCollectionViewCell.swift in Sources */, 269 | 0A28420E2799EDB10023560C /* DataPersistenceManager.swift in Sources */, 270 | 0A6209AB2734603E00A88D71 /* MainTabBarViewController.swift in Sources */, 271 | 0A1942D0274A620100B3FA93 /* CollectionViewTableViewCell.swift in Sources */, 272 | 0AD9A100276047A900131EEF /* APICaller.swift in Sources */, 273 | 0A28420C2799EBED0023560C /* NetflixCloneModel.xcdatamodeld in Sources */, 274 | 0A6291FA2756F1B900082232 /* HeroHeaderUIView.swift in Sources */, 275 | 0A6209C2273461A800A88D71 /* DownloadsViewController.swift in Sources */, 276 | 0A6209A72734603E00A88D71 /* AppDelegate.swift in Sources */, 277 | 0AE30378277F600E00CBC18F /* SearchResultsViewController.swift in Sources */, 278 | 0A5AAA9D2769057B00149719 /* Extensions.swift in Sources */, 279 | 0AD8A1BE2786FDB000612F1E /* YoutubeSearchResponse.swift in Sources */, 280 | 0A6209A92734603E00A88D71 /* SceneDelegate.swift in Sources */, 281 | 0AF8A46D2787BEA400FFE877 /* TitlePreviewViewModel.swift in Sources */, 282 | 0A6209BE2734617600A88D71 /* UpcomingViewController.swift in Sources */, 283 | 0AF8A46B2787BC2100FFE877 /* TitlePreviewViewController.swift in Sources */, 284 | 0AD9A10427604E4D00131EEF /* Title.swift in Sources */, 285 | 0A6209BC2734615B00A88D71 /* HomeViewController.swift in Sources */, 286 | 0A7C94D72776507A0072AE85 /* TitleViewModel.swift in Sources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXSourcesBuildPhase section */ 291 | 292 | /* Begin PBXVariantGroup section */ 293 | 0A6209B12734603E00A88D71 /* LaunchScreen.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 0A6209B22734603E00A88D71 /* Base */, 297 | ); 298 | name = LaunchScreen.storyboard; 299 | sourceTree = ""; 300 | }; 301 | /* End PBXVariantGroup section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | 0A6209B52734603E00A88D71 /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_ANALYZER_NONNULL = YES; 309 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_ENABLE_OBJC_WEAK = YES; 315 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 316 | CLANG_WARN_BOOL_CONVERSION = YES; 317 | CLANG_WARN_COMMA = YES; 318 | CLANG_WARN_CONSTANT_CONVERSION = YES; 319 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 320 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 321 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = dwarf; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | ENABLE_TESTABILITY = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu11; 342 | GCC_DYNAMIC_NO_PIC = NO; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_OPTIMIZATION_LEVEL = 0; 345 | GCC_PREPROCESSOR_DEFINITIONS = ( 346 | "DEBUG=1", 347 | "$(inherited)", 348 | ); 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 356 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 357 | MTL_FAST_MATH = YES; 358 | ONLY_ACTIVE_ARCH = YES; 359 | SDKROOT = iphoneos; 360 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 361 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 362 | }; 363 | name = Debug; 364 | }; 365 | 0A6209B62734603E00A88D71 /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | CLANG_ANALYZER_NONNULL = YES; 370 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 372 | CLANG_CXX_LIBRARY = "libc++"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_ENABLE_OBJC_WEAK = YES; 376 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_COMMA = YES; 379 | CLANG_WARN_CONSTANT_CONVERSION = YES; 380 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 382 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 389 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 391 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 392 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 393 | CLANG_WARN_STRICT_PROTOTYPES = YES; 394 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 395 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | COPY_PHASE_STRIP = NO; 399 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 400 | ENABLE_NS_ASSERTIONS = NO; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | GCC_C_LANGUAGE_STANDARD = gnu11; 403 | GCC_NO_COMMON_BLOCKS = YES; 404 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 405 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 406 | GCC_WARN_UNDECLARED_SELECTOR = YES; 407 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 408 | GCC_WARN_UNUSED_FUNCTION = YES; 409 | GCC_WARN_UNUSED_VARIABLE = YES; 410 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 411 | MTL_ENABLE_DEBUG_INFO = NO; 412 | MTL_FAST_MATH = YES; 413 | SDKROOT = iphoneos; 414 | SWIFT_COMPILATION_MODE = wholemodule; 415 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 416 | VALIDATE_PRODUCT = YES; 417 | }; 418 | name = Release; 419 | }; 420 | 0A6209B82734603E00A88D71 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 424 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 425 | CODE_SIGN_STYLE = Automatic; 426 | CURRENT_PROJECT_VERSION = 1; 427 | DEVELOPMENT_TEAM = DJCKM4F4RZ; 428 | GENERATE_INFOPLIST_FILE = YES; 429 | INFOPLIST_FILE = "Netflix Clone/Info.plist"; 430 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 431 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 432 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 433 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 434 | LD_RUNPATH_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "@executable_path/Frameworks", 437 | ); 438 | MARKETING_VERSION = 1.0; 439 | PRODUCT_BUNDLE_IDENTIFIER = "com.AmrHossam.Netflix-Clone"; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | SWIFT_EMIT_LOC_STRINGS = YES; 442 | SWIFT_VERSION = 5.0; 443 | TARGETED_DEVICE_FAMILY = "1,2"; 444 | }; 445 | name = Debug; 446 | }; 447 | 0A6209B92734603E00A88D71 /* Release */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 451 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 452 | CODE_SIGN_STYLE = Automatic; 453 | CURRENT_PROJECT_VERSION = 1; 454 | DEVELOPMENT_TEAM = DJCKM4F4RZ; 455 | GENERATE_INFOPLIST_FILE = YES; 456 | INFOPLIST_FILE = "Netflix Clone/Info.plist"; 457 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 458 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/Frameworks", 464 | ); 465 | MARKETING_VERSION = 1.0; 466 | PRODUCT_BUNDLE_IDENTIFIER = "com.AmrHossam.Netflix-Clone"; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Release; 473 | }; 474 | /* End XCBuildConfiguration section */ 475 | 476 | /* Begin XCConfigurationList section */ 477 | 0A62099E2734603D00A88D71 /* Build configuration list for PBXProject "Netflix Clone" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 0A6209B52734603E00A88D71 /* Debug */, 481 | 0A6209B62734603E00A88D71 /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | 0A6209B72734603E00A88D71 /* Build configuration list for PBXNativeTarget "Netflix Clone" */ = { 487 | isa = XCConfigurationList; 488 | buildConfigurations = ( 489 | 0A6209B82734603E00A88D71 /* Debug */, 490 | 0A6209B92734603E00A88D71 /* Release */, 491 | ); 492 | defaultConfigurationIsVisible = 0; 493 | defaultConfigurationName = Release; 494 | }; 495 | /* End XCConfigurationList section */ 496 | 497 | /* Begin XCRemoteSwiftPackageReference section */ 498 | 0A5AAAA8276AE2E600149719 /* XCRemoteSwiftPackageReference "SDWebImage" */ = { 499 | isa = XCRemoteSwiftPackageReference; 500 | repositoryURL = "https://github.com/SDWebImage/SDWebImage.git"; 501 | requirement = { 502 | branch = master; 503 | kind = branch; 504 | }; 505 | }; 506 | /* End XCRemoteSwiftPackageReference section */ 507 | 508 | /* Begin XCSwiftPackageProductDependency section */ 509 | 0A5AAAA9276AE2E600149719 /* SDWebImage */ = { 510 | isa = XCSwiftPackageProductDependency; 511 | package = 0A5AAAA8276AE2E600149719 /* XCRemoteSwiftPackageReference "SDWebImage" */; 512 | productName = SDWebImage; 513 | }; 514 | 0A5AAAAB276AE2E600149719 /* SDWebImageMapKit */ = { 515 | isa = XCSwiftPackageProductDependency; 516 | package = 0A5AAAA8276AE2E600149719 /* XCRemoteSwiftPackageReference "SDWebImage" */; 517 | productName = SDWebImageMapKit; 518 | }; 519 | /* End XCSwiftPackageProductDependency section */ 520 | 521 | /* Begin XCVersionGroup section */ 522 | 0A28420A2799EBED0023560C /* NetflixCloneModel.xcdatamodeld */ = { 523 | isa = XCVersionGroup; 524 | children = ( 525 | 0A28420B2799EBED0023560C /* NetflixCloneModel.xcdatamodel */, 526 | ); 527 | currentVersion = 0A28420B2799EBED0023560C /* NetflixCloneModel.xcdatamodel */; 528 | path = NetflixCloneModel.xcdatamodeld; 529 | sourceTree = ""; 530 | versionGroupType = wrapper.xcdatamodel; 531 | }; 532 | /* End XCVersionGroup section */ 533 | }; 534 | rootObject = 0A62099B2734603D00A88D71 /* Project object */; 535 | } 536 | --------------------------------------------------------------------------------