├── .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 |
--------------------------------------------------------------------------------