├── Movie-App
├── Movie-App
│ ├── Supporting Files
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ ├── 20.png
│ │ │ │ ├── 29.png
│ │ │ │ ├── 40.png
│ │ │ │ ├── 50.png
│ │ │ │ ├── 57.png
│ │ │ │ ├── 58.png
│ │ │ │ ├── 60.png
│ │ │ │ ├── 72.png
│ │ │ │ ├── 76.png
│ │ │ │ ├── 80.png
│ │ │ │ ├── 87.png
│ │ │ │ ├── 100.png
│ │ │ │ ├── 1024.png
│ │ │ │ ├── 114.png
│ │ │ │ ├── 120.png
│ │ │ │ ├── 144.png
│ │ │ │ ├── 152.png
│ │ │ │ ├── 167.png
│ │ │ │ ├── 180.png
│ │ │ │ └── Contents.json
│ │ │ ├── userAvatar.imageset
│ │ │ │ ├── userAvar.png
│ │ │ │ └── Contents.json
│ │ │ ├── MovieLogoPNG.imageset
│ │ │ │ ├── MovieLogoPNG.png
│ │ │ │ └── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── getInTouch.imageset
│ │ │ │ ├── undraw_Online_chat_re_c4lx-removebg-preview.png
│ │ │ │ └── Contents.json
│ │ │ └── heroImage.imageset
│ │ │ │ ├── https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg
│ │ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── GoogleService-Info.plist
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ └── SceneDelegate.swift
│ ├── Extensions
│ │ ├── String+Ext.swift
│ │ ├── Date+Ext.swift
│ │ ├── UIStackView+Ext.swift
│ │ ├── Constants.swift
│ │ ├── MovieError.swift
│ │ ├── Validation + Ext.swift
│ │ ├── UIViewController+Ext.swift
│ │ └── UIView+Ext.swift
│ ├── Models
│ │ ├── OnboardingModel.swift
│ │ ├── MovieViewModel.swift
│ │ ├── MoviePreviewModel.swift
│ │ ├── YoutubeSearchResponse.swift
│ │ └── Movie.swift
│ ├── Views
│ │ ├── Alert
│ │ │ └── AlertContainerView.swift
│ │ ├── Label
│ │ │ ├── TitleLabel.swift
│ │ │ ├── SecondaryTitleLabel.swift
│ │ │ └── BodyLabel.swift
│ │ ├── Buttons
│ │ │ └── MovieButton.swift
│ │ ├── Home
│ │ │ ├── MovieCollectionViewCell.swift
│ │ │ ├── CollectionViewTableViewCell.swift
│ │ │ └── HeroHeaderUIView.swift
│ │ ├── Profile
│ │ │ ├── SettingTableViewCell.swift
│ │ │ ├── HelpAndSupportUIView.swift
│ │ │ ├── ProfileUIView.swift
│ │ │ └── SwitchTableViewCell.swift
│ │ ├── Onboarding
│ │ │ └── SliderCell.swift
│ │ ├── TextField
│ │ │ └── CustomTextField.swift
│ │ ├── Search
│ │ │ └── MovieTableViewCell.swift
│ │ └── Dowload
│ │ │ └── DownloadTableViewCell.swift
│ ├── ViewModels
│ │ ├── OnboardingVM.swift
│ │ ├── SearchVM.swift
│ │ ├── DetailVM.swift
│ │ ├── ProfileVM.swift
│ │ ├── DownloadsVM.swift
│ │ ├── AuthVM.swift
│ │ └── HomeVM.swift
│ ├── Controllers
│ │ ├── DataLoading
│ │ │ └── MovieDataLoadingVC.swift
│ │ ├── MainTabBarViewController.swift
│ │ ├── Alert
│ │ │ └── AlertVC.swift
│ │ ├── DetailVC.swift
│ │ ├── Search
│ │ │ ├── SearchResultsViewController.swift
│ │ │ └── SearchViewController.swift
│ │ ├── DownloadsViewController.swift
│ │ ├── Auth
│ │ │ ├── ForgotPasswordVC.swift
│ │ │ ├── LoginVC.swift
│ │ │ └── RegisterVC.swift
│ │ ├── Profile
│ │ │ ├── HelpAndSupportVC.swift
│ │ │ ├── ChangePasswordVC.swift
│ │ │ └── ProfileViewController.swift
│ │ ├── Detail
│ │ │ └── MoviePreviewViewController.swift
│ │ └── HomeViewController.swift
│ └── Managers
│ │ └── APICaller.swift
└── Movie-App.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── xcshareddata
│ └── xcschemes
│ └── Movie-App.xcscheme
├── .gitignore
└── README.md
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/userAvatar.imageset/userAvar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/userAvatar.imageset/userAvar.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/MovieLogoPNG.imageset/MovieLogoPNG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/MovieLogoPNG.imageset/MovieLogoPNG.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/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 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/getInTouch.imageset/undraw_Online_chat_re_c4lx-removebg-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/getInTouch.imageset/undraw_Online_chat_re_c4lx-removebg-preview.png
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/String+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | extension String {
10 | func capitalizeFirstLetter() -> String {
11 | return self.prefix(1).uppercased() + self.lowercased().dropFirst()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/Date+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | func convertToMonthYearFormat() -> String {
12 | return formatted(.dateTime.day().month().year())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Models/OnboardingModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingModel.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 31.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | struct OnboardingItemModel{
12 | let color: UIColor
13 | let title: String
14 | let text: String
15 | let animationName: String
16 | }
17 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Models/MovieViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MovieCellModel.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import Foundation
10 |
11 | struct MovieCellModel{
12 | let titleName: String
13 | let posterURL: String
14 | let vote_average: Double?
15 | let release_date: String?
16 | }
17 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Models/MoviePreviewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MoviePreviewModel.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import Foundation
10 |
11 | struct MoviePreviewModel {
12 | let title: String
13 | let youtubeView: VideoElement
14 | let movieOverview: String
15 | let release_date: String?
16 | }
17 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/heroImage.imageset/https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasarduman/CineVerse-MVVM/HEAD/Movie-App/Movie-App/Supporting Files/Assets.xcassets/heroImage.imageset/https---specials-images.forbesimg.com-imageserve-61116cea2313e8bae55a536a--Dune--0x0.jpg?fit=scale.jpeg
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/UIStackView+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIStackView+Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | extension UIStackView {
12 | // MARK: - Adding Arranged Subviews
13 | func addArrangedSubviewsExt(_ views: UIView...) {
14 | for view in views { addArrangedSubview(view) }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/userAvatar.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "userAvar.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Models/YoutubeSearchResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YoutubeSearchResponse.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import Foundation
10 |
11 | struct YoutubeSearchResponse: Codable {
12 | let items: [VideoElement]
13 | }
14 |
15 |
16 | struct VideoElement: Codable {
17 | let id: IdVideoElement
18 | }
19 |
20 |
21 | struct IdVideoElement: Codable {
22 | let kind: String
23 | let videoId: String
24 | }
25 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/MovieLogoPNG.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "MovieLogoPNG.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/getInTouch.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "undraw_Online_chat_re_c4lx-removebg-preview.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/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 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Models/Movie.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Movie.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | struct MovieResponse: Codable {
10 | let results: [Movie]
11 | }
12 |
13 | struct Movie: Codable {
14 | let id: Int
15 | let original_title: String?
16 | let original_name: String?
17 | let overview: String?
18 | let poster_path: String?
19 | let media_type: String?
20 | let release_date: String?
21 | let first_air_date: String?
22 | let vote_average: Double?
23 | let vote_count: Int?
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 2.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | // MARK: - Color
11 | enum MovieColor {
12 | static let playButonBG = UIColor(red: 0.91, green: 0.65, blue: 0.01, alpha: 1.00)
13 | static let goldColor = UIColor(red: 1.00, green: 0.82, blue: 0.24, alpha: 1.00)
14 | static let TabarbgDark = UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.00)
15 | static let TabarbgWhite = UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.00)
16 | static let playButonBGText = UIColor(red: 0.78, green: 0.40, blue: 0.33, alpha: 1.00)
17 | }
18 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/MovieError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MovieError.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | // MARK: - Custom Error Enum
10 | enum MovieError: String, Error {
11 | case invalidUrl = "Url Dönüştürülemedi. Please try again."
12 | case invalidResponse = "Invalid response from the server. Please try again."
13 | case invalidData = "The data received from the server was invalid Please try again."
14 | case unableToComplete = "Unable to complete your request. Please check your internet connection."
15 | case unableToFavorite = "There was an error favoriting this user. Please try again."
16 | case alreadyInFavorites = "You've already favorited this user."
17 | }
18 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Alert/AlertContainerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFAlertContainerView.swift
3 | // GithubFollowers
4 | //
5 | // Created by Yaşar Duman on 6.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class AlertContainerView: UIView {
11 |
12 | override init(frame: CGRect) {
13 | super.init(frame: frame)
14 | configure()
15 | }
16 |
17 | required init?(coder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 |
21 | private func configure() {
22 |
23 | backgroundColor = .systemBackground
24 | layer.cornerRadius = 16
25 | layer.borderWidth = 2
26 | layer.borderColor = UIColor.white.cgColor
27 | translatesAutoresizingMaskIntoConstraints = false
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/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 | NSPhotoLibraryUsageDescription
23 | Choose Photo for profile picture
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 |
3 | OS X Finder
4 | .DS_Store
5 |
6 | Build generated
7 | build/
8 | DerivedData
9 |
10 | Various settings
11 | *.pbxuser
12 | !default.pbxuser
13 | *.mode1v3
14 | !default.mode1v3
15 | *.mode2v3
16 | !default.mode2v3
17 | *.perspectivev3
18 | !default.perspectivev3
19 | xcuserdata
20 |
21 | *.xcuserstate
22 | *.xccheckout
23 | /.idea
24 | *.dSYM.zip
25 |
26 | Other
27 | *.xccheckout
28 | *.moved-aside
29 | *.xcuserstate
30 | *.xcscmblueprint
31 |
32 | Obj-C/Swift specific
33 | *.hmap
34 | .ipa
35 |
36 | Dependencies
37 | Carthage
38 | Pods
39 |
40 | Fastlane
41 | /fastlane/README.md
42 | /fastlane/report.xml
43 | /fastlane/Error.png
44 | /fastlane/Preview.html
45 | /fastlane/screenshots//-portrait.png
46 | /fastlane/screenshots//-landscape.png
47 | /fastlane/screenshots/screenshots.html
48 | /fastlane/screenshots//-portrait_framed.png
49 | /fastlane/screenshots//-landscape_framed.png
50 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API_KEY
6 | AIzaSyDwoGN4Z0cOItzME0KLqJkqUH4eG68hB58
7 | GCM_SENDER_ID
8 | 341235766054
9 | PLIST_VERSION
10 | 1
11 | BUNDLE_ID
12 | com.info.Movie-App
13 | PROJECT_ID
14 | movie-57c02
15 | STORAGE_BUCKET
16 | movie-57c02.appspot.com
17 | IS_ADS_ENABLED
18 |
19 | IS_ANALYTICS_ENABLED
20 |
21 | IS_APPINVITE_ENABLED
22 |
23 | IS_GCM_ENABLED
24 |
25 | IS_SIGNIN_ENABLED
26 |
27 | GOOGLE_APP_ID
28 | 1:341235766054:ios:9f6b81d0b482eb2faca7e7
29 |
30 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/OnboardingVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 31.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class OnboardingVM {
11 | let sliderData: [OnboardingItemModel] = [
12 | OnboardingItemModel(color: MovieColor.TabarbgDark, title: "Step into the Magic of Cinema", text: "Cineverse welcomes you to the enchanting world of cinema. Begin your journey by discovering the latest film news and captivating stories.", animationName: "a2"),
13 |
14 | OnboardingItemModel(color: MovieColor.TabarbgDark, title: "Experience Film Delight with Cineverse", text: "For movie enthusiasts, Cineverse is here! Explore the newest films and immerse yourself in the enchanting world of cinema.", animationName: "a1"),
15 |
16 | OnboardingItemModel(color: MovieColor.TabarbgDark, title: "Ready for a Cinematic Adventure with Cineverse?", text: "Embark on a magical film journey with Cineverse. Discover the latest trailers, star updates, and more in the world of cinema!", animationName: "a2"),
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Label/TitleLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFTitleLabel.swift
3 | // GithubFollowers
4 | //
5 | // Created by Yaşar Duman on 4.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class TitleLabel: UILabel {
11 |
12 | // MARK: - Initialization
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 |
16 | configure()
17 | }
18 |
19 | required init?(coder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | convenience init(textAlignment: NSTextAlignment, fontSize: CGFloat) {
24 | self.init(frame: .zero)
25 |
26 | self.textAlignment = textAlignment
27 | self.font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
28 | }
29 |
30 | // MARK: - Configuration
31 | private func configure() {
32 | textColor = .label
33 | adjustsFontSizeToFitWidth = true
34 | minimumScaleFactor = 0.9
35 | lineBreakMode = .byTruncatingTail
36 | translatesAutoresizingMaskIntoConstraints = false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/DataLoading/MovieDataLoadingVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MovieDataLoadingVCswift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class MovieDataLoadingVC: UIViewController {
11 | // MARK: - Properties
12 | private var containerView: UIView!
13 |
14 | // MARK: - Loading View Methods
15 | func showLoadingView() {
16 | containerView = UIView(frame: view.bounds)
17 | view.addSubview(containerView)
18 |
19 | containerView.backgroundColor = .systemBackground
20 | containerView.alpha = 0
21 |
22 | UIView.animate(withDuration: 0.05) { self.containerView.alpha = 0.8 }
23 |
24 | let activityIndicator = UIActivityIndicatorView(style: .large)
25 | containerView.addSubview(activityIndicator)
26 |
27 | activityIndicator.centerInSuperview()
28 |
29 | activityIndicator.startAnimating()
30 | }
31 |
32 | func dismissLoadingView() {
33 | DispatchQueue.main.async {
34 | self.containerView.removeFromSuperview()
35 | self.containerView = nil
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Label/SecondaryTitleLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFSecondaryTitleLabel.swift
3 | // GithubFollowers
4 | //
5 | // Created by Yaşar Duman on 5.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class SecondaryTitleLabel: UILabel {
11 |
12 | // MARK: - Initialization
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 |
16 | configure()
17 | }
18 |
19 | required init?(coder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | convenience init(fontSize: CGFloat) {
24 | self.init(frame: .zero)
25 |
26 | font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
27 | }
28 |
29 | // MARK: - Configuration
30 | private func configure() {
31 | textColor = .secondaryLabel
32 | adjustsFontForContentSizeCategory = true
33 | adjustsFontSizeToFitWidth = true
34 | minimumScaleFactor = 0.90
35 | lineBreakMode = .byTruncatingTail
36 | translatesAutoresizingMaskIntoConstraints = false
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Label/BodyLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFBodyLabel.swift
3 | // GithubFollowers
4 | //
5 | // Created by Yaşar Duman on 4.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class BodyLabel: UILabel {
11 |
12 | // MARK: - Initialization
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 |
16 | configure()
17 | }
18 |
19 | required init?(coder: NSCoder) {
20 | fatalError("init(coder:) has not been implemented")
21 | }
22 |
23 | convenience init(textAlignment: NSTextAlignment) {
24 | self.init(frame: .zero)
25 |
26 | self.textAlignment = textAlignment
27 | }
28 |
29 | // MARK: - Configuration
30 | private func configure() {
31 | textColor = .secondaryLabel
32 | font = UIFont.preferredFont(forTextStyle: .body)
33 | adjustsFontForContentSizeCategory = true
34 | adjustsFontSizeToFitWidth = true
35 | minimumScaleFactor = 0.75
36 | lineBreakMode = .byWordWrapping
37 | translatesAutoresizingMaskIntoConstraints = false
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/Validation + Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Validation + Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 |
12 | func isValidEmail(email: String) -> Bool{
13 | let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-z0-9.-]+\\.[A-Za-z]{2,4}"
14 | let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
15 | let result = emailTest.evaluate(with: email)
16 | return result
17 | }
18 |
19 | func isValidPassword(password: String) -> Bool{
20 | let passwordRegEx = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{6,18}$"
21 | let passwordTest = NSPredicate(format: "SELF MATCHES %@", passwordRegEx)
22 | let result = passwordTest.evaluate(with: password)
23 | return result
24 | }
25 |
26 | func containsDigits(_ value: String) -> Bool{
27 | let DigitsRegEx = ".*[0-9]+.*"
28 | let DigitsTest = NSPredicate(format: "SELF MATCHES %@", DigitsRegEx)
29 | let result = DigitsTest.evaluate(with: value)
30 | return result
31 | }
32 |
33 | func containsUpperCase(_ value: String) -> Bool{
34 | let UpperCaseRegEx = ".*[A-Z]+.*"
35 | let UpperCaseTest = NSPredicate(format: "SELF MATCHES %@", UpperCaseRegEx)
36 | let result = UpperCaseTest.evaluate(with: value)
37 | return result
38 | }
39 |
40 | func containsLowerCase(_ value: String) -> Bool{
41 | let LowerCaseRegEx = ".*[a-z]+.*"
42 | let LowerCaseTest = NSPredicate(format: "SELF MATCHES %@", LowerCaseRegEx)
43 | let result = LowerCaseTest.evaluate(with: value)
44 | return result
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 | import FirebaseCore
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 |
18 | if UserDefaults.standard.value(forKey: "DarkMode") == nil {
19 | // Dark Mode anahtarını başlangıçta ayarlayın (örneğin, varsayılan olarak false)
20 | UserDefaults.standard.set(true, forKey: "DarkMode")
21 | }
22 |
23 | FirebaseApp.configure()
24 | return true
25 | }
26 |
27 | // MARK: UISceneSession Lifecycle
28 |
29 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
30 | // Called when a new scene session is being created.
31 | // Use this method to select a configuration to create the new scene with.
32 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
33 | }
34 |
35 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
36 | // Called when the user discards a scene session.
37 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
38 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
39 | }
40 |
41 |
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/UIViewController+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | extension UIViewController {
12 | // MARK: - Custom Alerts
13 | func presentAlert(title: String, message: String, buttonTitle: String) {
14 | let alertVC = AlertVC(title: title, message: message, buttonTitle: buttonTitle)
15 | alertVC.modalPresentationStyle = .overFullScreen
16 | alertVC.modalTransitionStyle = .crossDissolve
17 | self.present(alertVC, animated: true)
18 | }
19 |
20 | // Presents a default error alert with a standard message.
21 | func presentDefualtError() {
22 | let alertVC = AlertVC(title: "Something Wnt Wrong !",
23 | message: "We were unable to complete your task at this time . Please try again.",
24 | buttonTitle: "Ok")
25 | alertVC.modalPresentationStyle = .overFullScreen
26 | alertVC.modalTransitionStyle = .crossDissolve
27 | self.present(alertVC, animated: true)
28 |
29 | }
30 |
31 | // MARK: - Loading Indicator
32 | // Shows a loading indicator view on top of the current view controller.
33 | func showLoading() {
34 | let loadingViewController = MovieDataLoadingVC()
35 | loadingViewController.showLoadingView()
36 | loadingViewController.modalPresentationStyle = .overFullScreen
37 | loadingViewController.modalTransitionStyle = .crossDissolve
38 | self.present(loadingViewController, animated: true)
39 | }
40 | // Dismisses the currently presented loading indicator view.
41 | func dismissLoading() {
42 | DispatchQueue.main.async {
43 | self.dismiss(animated: true, completion: nil)
44 | }
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Buttons/MovieButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFButton.swift
3 | // GithubFollowers
4 | //
5 | // Created by Yaşar Duman on 4.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class MovieButton: UIButton {
11 |
12 | enum FontSize {
13 | case big
14 | case medium
15 | case small
16 | }
17 |
18 |
19 | // MARK: - Initialization
20 | override init(frame: CGRect) {
21 | super.init(frame: frame)
22 | configure()
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | convenience init(bgColor: UIColor ,color: UIColor, title: String, fontSize: FontSize = .medium, systemImageName: String? = nil,cornerStyle: UIButton.Configuration.CornerStyle? = .medium){
30 | self.init(frame: .zero)
31 | set(bgColor: bgColor ,color: color, title: title, fontSize: fontSize, systemImageName: systemImageName, cornerStyle: cornerStyle)
32 | }
33 |
34 | // MARK: - Configuration
35 | private func configure() {
36 | configuration = .tinted()
37 | configuration?.cornerStyle = .medium
38 | translatesAutoresizingMaskIntoConstraints = false
39 | }
40 |
41 |
42 | func set(bgColor: UIColor ,color: UIColor, title: String, fontSize: FontSize, systemImageName: String?,cornerStyle: UIButton.Configuration.CornerStyle?) {
43 | configuration?.baseBackgroundColor = bgColor
44 | configuration?.baseForegroundColor = color
45 | configuration?.cornerStyle = cornerStyle ?? .medium
46 | configuration?.title = title
47 |
48 | if let imageName = systemImageName {
49 | configuration?.image = UIImage(systemName: imageName)
50 | configuration?.imagePadding = 6
51 | }
52 |
53 | switch fontSize {
54 | case .big:
55 | self.titleLabel?.font = .systemFont(ofSize: 22, weight: .bold)
56 | case .medium:
57 | self.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
58 | case .small:
59 | self.titleLabel?.font = .systemFont(ofSize: 16, weight: .regular)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Home/MovieCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TitleCollectionViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import SDWebImage
10 |
11 | final class MovieCollectionViewCell: UICollectionViewCell {
12 | // MARK: - Properties
13 | static let identifier = "MovieCollectionViewCell"
14 |
15 | // MARK: - UI Elements
16 | private lazy var posterImageView: UIImageView = {
17 | let imageView = UIImageView()
18 | imageView.contentMode = .scaleAspectFill
19 | return imageView
20 | }()
21 |
22 | private lazy var imdbButton = MovieButton(bgColor: .red,
23 | color: MovieColor.playButonBG,
24 | title: "",
25 | cornerStyle: .capsule)
26 |
27 | // MARK: - Initializers
28 | override init(frame: CGRect) {
29 | super.init(frame: frame)
30 | contentView.addSubview(posterImageView)
31 | posterImageView.addSubview(imdbButton)
32 | }
33 |
34 | required init?(coder: NSCoder) {
35 | fatalError()
36 | }
37 |
38 | // MARK: - Layout Subviews
39 | override func layoutSubviews() {
40 | super.layoutSubviews()
41 | posterImageView.frame = contentView.bounds
42 | imdbButton.anchor(top: posterImageView.topAnchor,
43 | trailing: posterImageView.trailingAnchor,
44 | padding: .init(top: 6, trailing: 6),
45 | size: .init(width: 50, height: 25))
46 | }
47 |
48 | // MARK: - Public Methods
49 | func configure(with model: Movie) {
50 | let posterPath = model.poster_path
51 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(posterPath!)") else {
52 | return
53 | }
54 |
55 | posterImageView.sd_setImage(with: url, completed: nil)
56 | if let voteAverage = model.vote_average {
57 | let formattedValue = String(format: "%.1f", voteAverage)
58 | self.imdbButton.setTitle(formattedValue, for: UIControl.State.normal)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/SearchVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 9.12.2023.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | protocol searchVMInterface {
12 | func viewDidLoad()
13 | func didSelectRow(at indexPath: IndexPath)
14 | func fetchDiscoverMovies()
15 | func showDetail(movie: Movie , movieName: String)
16 |
17 | }
18 |
19 | final class SearchVM {
20 | private weak var view: searchVCInterface?
21 | var movies: [Movie] = []
22 |
23 | init(view: searchVCInterface? = nil) {
24 | self.view = view
25 | }
26 | }
27 |
28 | extension SearchVM: searchVMInterface {
29 |
30 | func didSelectRow(at indexPath: IndexPath) {
31 |
32 | let movie = movies[indexPath.row]
33 |
34 | guard let movieName = movie.original_title ?? movie.original_name else {
35 | return
36 | }
37 |
38 | showDetail(movie: movie, movieName: movieName)
39 | }
40 |
41 | func viewDidLoad() {
42 | view?.configureViewDidLoad()
43 | fetchDiscoverMovies()
44 | }
45 |
46 | // MARK: - Data Fetching
47 | func fetchDiscoverMovies() {
48 | Task{
49 | do {
50 | let getUpcomingMovies = try await APICaller.shared.getDiscoverMovies().results
51 | self.movies = getUpcomingMovies
52 | view?.discoverTableReloadData()
53 | }catch {
54 | if let movieError = error as? MovieError {
55 | print(movieError.rawValue)
56 | } else {
57 | view?.alert(title: "Error!", message: error.localizedDescription, buttonTitle: "OK")
58 | }
59 | }
60 | }
61 | }
62 |
63 | func showDetail(movie: Movie , movieName: String) {
64 | Task{
65 | do {
66 | let moviePreviewModel = try await APICaller.shared.getMovie(with: movieName)
67 | DispatchQueue.main.async {
68 | let vc = MoviePreviewViewController()
69 | vc.configure(with: MoviePreviewModel(title: movieName, youtubeView: moviePreviewModel, movieOverview: movie.overview ?? "", release_date: movie.release_date ?? movie.first_air_date),moviModelIsFavori: movie )
70 | self.view?.pushVC(vc: vc)
71 | }
72 | }catch {
73 | if let movieError = error as? MovieError {
74 | print(movieError.rawValue)
75 | } else {
76 | view?.alert(title: "Error", message: error.localizedDescription, buttonTitle: "OK")
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/DetailVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import FirebaseFirestore
9 | import FirebaseAuth
10 |
11 | final class DetailVM {
12 | let currentUserID = Auth.auth().currentUser!.uid
13 |
14 | func addToFavorites(movies: Movie, completion: @escaping (Bool) -> Void) {
15 |
16 | let data: [String: Any] = [
17 | "id": movies.id,
18 | "original_title": movies.original_title ?? "",
19 | "original_name": movies.original_name ?? "",
20 | "overview": movies.overview ?? "",
21 | "poster_path": movies.poster_path ?? "",
22 | "media_type": movies.media_type ?? "",
23 | "release_date": movies.release_date ?? "",
24 | "first_air_date": movies.first_air_date ?? "",
25 | "vote_average": movies.vote_average ?? 0.0,
26 | "vote_count": movies.vote_count ?? 0
27 | ]as [String : Any]
28 |
29 | Firestore.firestore()
30 | .collection("UsersInfo")
31 | .document(currentUserID)
32 | .collection("favorites")
33 | .document(String(movies.id))
34 | .setData(data) { error in
35 | if let error = error {
36 | print(error.localizedDescription)
37 | }
38 | self.isFavorited(movies: movies) { bool in
39 | completion(bool)
40 | }
41 | }
42 | }
43 |
44 | func removeFromFavorites(movies: Movie, completion: @escaping (Bool) -> Void) {
45 | Firestore.firestore()
46 | .collection("UsersInfo")
47 | .document(currentUserID)
48 | .collection("favorites")
49 | .document(String(movies.id))
50 | .delete { error in
51 | if let error = error {
52 | print(error.localizedDescription)
53 | }
54 | self.isFavorited(movies: movies) { bool in
55 | completion(bool)
56 | }
57 | }
58 | }
59 |
60 | func isFavorited(movies: Movie, completion: @escaping (Bool) -> Void) {
61 | Firestore.firestore()
62 | .collection("UsersInfo")
63 | .document(currentUserID)
64 | .collection("favorites")
65 | .document(String(movies.id))
66 | .getDocument { snapshot, error in
67 | if let error = error {
68 | print(error.localizedDescription)
69 | return
70 | }
71 |
72 | if let snapshot = snapshot {
73 | completion(snapshot.exists)
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Profile/SettingTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingTableViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class SettingTableViewCell: UITableViewCell {
11 | // MARK: - Properties
12 | static let identifier = "SettingTableViewCell"
13 |
14 | // MARK: - UI Elements
15 | private lazy var iconContainer: UIView = {
16 | let view = UIView()
17 | view.clipsToBounds = true
18 | view.layer.cornerRadius = 10
19 | view.layer.masksToBounds = true
20 | return view
21 | }()
22 |
23 | private lazy var iconImageView: UIImageView = {
24 | let imageView = UIImageView()
25 | imageView.tintColor = MovieColor.playButonBG
26 | imageView.contentMode = .scaleAspectFit
27 | return imageView
28 | }()
29 |
30 |
31 | private let label: UILabel = {
32 | let label = UILabel()
33 | label.numberOfLines = 1
34 | return label
35 | }()
36 |
37 | // MARK: - Initializers
38 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
39 | super.init(style: style, reuseIdentifier: reuseIdentifier)
40 | contentView.addSubview(label)
41 | contentView.addSubview(iconContainer)
42 | iconContainer.addSubview(iconImageView)
43 |
44 | contentView.clipsToBounds = true
45 | accessoryType = .disclosureIndicator
46 | }
47 |
48 | required init?(coder: NSCoder) {
49 | fatalError("init(coder:) has not been implemented")
50 | }
51 |
52 | // MARK: - Layout Subviews
53 | override func layoutSubviews() {
54 | super.layoutSubviews()
55 |
56 | let size: CGFloat = contentView.frame.size.height - 12
57 | let imageSize: CGFloat = size/1.5
58 |
59 | iconContainer.anchor(leading: leadingAnchor,
60 | padding: .init(leading: 15),
61 | size: .init(width: size, height: size))
62 | iconContainer.centerYInSuperview()
63 |
64 | iconImageView.anchor(size: .init(width: imageSize, height: imageSize))
65 | iconImageView.centerXInSuperview()
66 | iconImageView.centerYInSuperview()
67 |
68 | label.anchor(leading: iconContainer.trailingAnchor,
69 | padding: .init(leading: 20))
70 | label.centerYInSuperview()
71 | }
72 |
73 | // MARK: - Prepare For Reuse
74 | override func prepareForReuse() {
75 | super.prepareForReuse()
76 | iconImageView.image = nil
77 | label.text = nil
78 | iconContainer.backgroundColor = nil
79 | }
80 |
81 | // MARK: - Configure Cell
82 | func configure(with model: SettingsOption){
83 | label.text = model.title
84 | iconImageView.image = model.icon
85 | iconContainer.backgroundColor = model.iconBackgrondColor
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Onboarding/SliderCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SliderCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 | import Lottie
11 |
12 | final class SliderCell: UICollectionViewCell {
13 |
14 | //MARK: - UI Elements
15 | private lazy var lottieView: LottieAnimationView = {
16 | let lottieView = LottieAnimationView()
17 | lottieView.loopMode = .loop
18 | lottieView.contentMode = .scaleAspectFit
19 | return lottieView
20 | }()
21 |
22 | lazy var titleLabel: UILabel = {
23 | let label = UILabel()
24 | label.textAlignment = .center
25 | label.textColor = MovieColor.playButonBG
26 | label.numberOfLines = 0
27 | label.font = UIFont.systemFont(ofSize: 20, weight: .black)
28 | return label
29 | }()
30 |
31 | lazy var textLabel: UILabel = {
32 | let label = UILabel()
33 | label.textAlignment = .center
34 | label.textColor = .white
35 | label.numberOfLines = 0
36 | return label
37 | }()
38 |
39 |
40 | // MARK: - Initialization
41 | override init(frame: CGRect) {
42 | super.init(frame: frame)
43 | configureUI()
44 | }
45 |
46 | required init?(coder: NSCoder) {
47 | fatalError("init(coder:) has not been implemented")
48 | }
49 |
50 | //MARK: - Helper Functions
51 | private func configureUI() {
52 | contentView.addSubviewsExt(lottieView, titleLabel, textLabel)
53 | configureLottieView()
54 | configureTitleLabel()
55 | configureTextLabel()
56 | }
57 |
58 | private func configureLottieView() {
59 | lottieView.anchor(top: contentView.topAnchor,
60 | leading: contentView.leadingAnchor,
61 | trailing: contentView.trailingAnchor,
62 | padding: .init(top: 140),
63 | size: .init(heightSize: 300))
64 | }
65 |
66 | private func configureTitleLabel() {
67 | titleLabel.anchor(top: lottieView.bottomAnchor,
68 | leading: contentView.leadingAnchor,
69 | trailing: contentView.trailingAnchor,
70 | padding: .init(top: 40,
71 | leading: 20,
72 | trailing: 20))
73 | }
74 |
75 | private func configureTextLabel() {
76 | textLabel.anchor(top: titleLabel.bottomAnchor,
77 | leading: contentView.leadingAnchor,
78 | trailing: contentView.trailingAnchor,
79 | padding: .init(top: 30,
80 | leading: 20,
81 | trailing: 20))
82 | }
83 |
84 |
85 | // MARK: - Animation Setup
86 | func animationSetup(animationName: String){
87 | lottieView.animation = LottieAnimation.named(animationName)
88 | lottieView.play()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App.xcodeproj/xcshareddata/xcschemes/Movie-App.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/MainTabBarViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTabBarViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 | // Tab bar arka plan rengini beyaz yapın
16 | UITabBar.appearance().backgroundColor = .clear
17 |
18 | // Seçili olan öğelerin rengini turuncu yapın
19 | UITabBar.appearance().tintColor = MovieColor.playButonBG
20 |
21 | // Seçilmeyen öğelerin rengini gri yapın
22 | UITabBar.appearance().unselectedItemTintColor = .systemGray
23 |
24 | viewControllers = [
25 | createHomeNC(),
26 | createSearchNC(),
27 | createFavoritesNC(),
28 | createProfileNC()
29 | ]
30 | }
31 |
32 |
33 | // MARK: - Home Navigation Controller 🏠
34 | private func createHomeNC() -> UINavigationController {
35 | let homeVC = HomeViewController()
36 |
37 | homeVC.tabBarItem = UITabBarItem(title: "Home",
38 | image: UIImage(systemName: "house"),
39 | tag: 0)
40 |
41 | homeVC.tabBarItem.selectedImage = UIImage(systemName: "house.fill")
42 |
43 | return UINavigationController(rootViewController: homeVC)
44 | }
45 |
46 | // MARK: - Search Navigation Controller 🔍
47 | private func createSearchNC() -> UINavigationController {
48 | let searchVC = SearchViewController()
49 | searchVC.title = "Search"
50 |
51 | searchVC.tabBarItem = UITabBarItem(title: "Search",
52 | image: UIImage(systemName: "magnifyingglass"),
53 | tag: 1)
54 |
55 |
56 | return UINavigationController(rootViewController: searchVC)
57 | }
58 |
59 | // MARK: - Favorites Navigation Controller ⭐️
60 | private func createFavoritesNC() -> UINavigationController {
61 | let downloadVC = DownloadsViewController()
62 | downloadVC.title = "Download"
63 |
64 | downloadVC.tabBarItem = UITabBarItem(title: "Download",
65 | image: UIImage(systemName: "arrow.down.to.line"),
66 | tag: 2)
67 |
68 | return UINavigationController(rootViewController: downloadVC)
69 | }
70 |
71 |
72 | // MARK: - Profile Navigation Controller ⚙️
73 | private func createProfileNC() -> UINavigationController {
74 | let profileVC = ProfileViewController()
75 | profileVC.title = "Profile"
76 |
77 | profileVC.tabBarItem = UITabBarItem(title: "Profile",
78 | image: UIImage(systemName: "person"),
79 | tag: 3)
80 |
81 | return UINavigationController(rootViewController: profileVC)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/ProfileVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 | import UIKit
9 | import FirebaseFirestore
10 | import FirebaseAuth
11 | import FirebaseStorage
12 |
13 | // MARK: - Section Data Structure
14 | struct Section {
15 | let title: String
16 | let options: [SettingsOptionType]
17 | }
18 |
19 | // MARK: - Settings Option Type Enumeration
20 | enum SettingsOptionType {
21 | case staticCell(model: SettingsOption)
22 | case switchCell(model: SettingsSwitchOption)
23 | }
24 |
25 | // MARK: - Switch Option Data Structure
26 | struct SettingsSwitchOption {
27 | let title: String
28 | let icon: UIImage?
29 | let iconBackgrondColor: UIColor
30 | let handler: (() -> Void)
31 | var isOn: Bool
32 | }
33 |
34 | // MARK: - Settings Option Data Structure
35 | struct SettingsOption {
36 | let title: String
37 | let icon: UIImage?
38 | let iconBackgrondColor: UIColor
39 | let handler: (() -> Void)
40 | }
41 |
42 |
43 | protocol ProfileVMInterface: AnyObject {
44 | func viewDidLoad()
45 | }
46 |
47 | // MARK: - ViewModel
48 | final class ProfileVM {
49 | private var view: ProfileVCInterface?
50 | private let currentUserID = Auth.auth().currentUser!.uid
51 | lazy var models = [Section]()
52 |
53 | init(view: ProfileVCInterface? = nil) {
54 | self.view = view
55 | }
56 |
57 | func fetchUserName(completion: @escaping (String) -> Void) {
58 | Firestore.firestore()
59 | .collection("UsersInfo")
60 | .document(currentUserID)
61 | .getDocument { snapshot, error in
62 | if let error = error {
63 | print(error.localizedDescription)
64 | }
65 | if let snapshot = snapshot {
66 | if let data = snapshot.data() {
67 | let userName = data["userName"] as! String
68 | completion(userName)
69 | }
70 | }
71 | }
72 | }
73 |
74 | func uploadUserPhoto(imageData: UIImage) {
75 | let storageRefernce = Storage.storage().reference()
76 |
77 | //turn of image into data
78 | let imageData = imageData.jpegData(compressionQuality: 0.8)
79 |
80 | guard imageData != nil else{
81 | return
82 | }
83 |
84 | let fileRef = storageRefernce.child("Media/\(currentUserID).jpg")
85 |
86 | fileRef.putData(imageData!, metadata: nil)
87 | }
88 |
89 | func fetchUserPhoto(completion: @escaping (String) -> Void) {
90 | let storageRef = Storage.storage().reference()
91 |
92 | let fileRef = storageRef.child("Media/\(currentUserID).jpg")
93 | fileRef.downloadURL { url, error in
94 | if error == nil {
95 | let imageUrl = url?.absoluteString
96 | completion(imageUrl!)
97 | }
98 | }
99 | }
100 | }
101 |
102 | extension ProfileVM: ProfileVMInterface {
103 | func viewDidLoad() {
104 | view?.configureViewDidLoad()
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/DownloadsVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadsVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import FirebaseFirestore
9 | import FirebaseAuth
10 |
11 |
12 | protocol DownloadVMInterface {
13 | func viewDidLoad()
14 | func refreshUI()
15 | func didSelectRowAt(at indexPath: IndexPath)
16 | }
17 |
18 | final class DownloadsVM {
19 | var view: DownloadVCInterface?
20 | let currentUserID = Auth.auth().currentUser!.uid
21 | var movies: [Movie] = []
22 |
23 |
24 | init(view: DownloadVCInterface? = nil) {
25 | self.view = view
26 | }
27 |
28 | func fetchFavorites(completion: @escaping([Movie]) -> Void) {
29 | Firestore.firestore()
30 | .collection("UsersInfo")
31 | .document(currentUserID)
32 | .collection("favorites")
33 | .getDocuments { snapshot, error in
34 | if let error = error {
35 | print(error.localizedDescription)
36 | return
37 | }
38 |
39 | guard let documents = snapshot?.documents else { return }
40 | let movies = documents.compactMap({try? $0.data(as: Movie.self)})
41 | completion(movies)
42 | }
43 | }
44 |
45 |
46 |
47 | func removeFromFavorites(movies: Movie) {
48 | Firestore.firestore()
49 | .collection("UsersInfo")
50 | .document(currentUserID)
51 | .collection("favorites")
52 | .document(String(movies.id))
53 | .delete { error in
54 | if let error = error {
55 | print(error.localizedDescription)
56 | }
57 | }
58 | }
59 |
60 |
61 | }
62 |
63 | extension DownloadsVM: DownloadVMInterface{
64 | func didSelectRowAt(at indexPath: IndexPath) {
65 |
66 | let movie = movies[indexPath.row]
67 |
68 | guard let movieName = movie.original_title ?? movie.original_name else {
69 | return
70 | }
71 |
72 | Task{
73 | do {
74 | let moviePreveiwModel = try await APICaller.shared.getMovie(with: movieName)
75 |
76 | let vc = await MoviePreviewViewController()
77 | await vc.configure(with: MoviePreviewModel(title: movieName, youtubeView: moviePreveiwModel, movieOverview: movie.overview ?? "", release_date: movie.release_date ?? movie.first_air_date),moviModelIsFavori: movie)
78 |
79 | view?.pushVC(vc: vc)
80 |
81 | }catch {
82 | if let movieError = error as? MovieError {
83 | print(movieError.rawValue)
84 | } else {
85 | view?.alert(title: "Error!", message: error.localizedDescription, buttonTitle: "OK")
86 | }
87 | }
88 | }
89 | }
90 |
91 | func viewDidLoad() {
92 | view?.configureViewDidLoad()
93 | refreshUI()
94 | }
95 |
96 | func refreshUI() {
97 | fetchFavorites { movies in
98 | self.movies = movies
99 | self.view?.tableViewReloadData()
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/AuthVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 31.10.2023.
6 | //
7 |
8 |
9 | import FirebaseAuth
10 | import FirebaseFirestore
11 |
12 | final class AuthVM{
13 | // MARK: - Login
14 | func login(email: String, password: String, completion: @escaping (Bool, String) -> Void) {
15 | Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
16 |
17 | if let error = error {
18 | completion(false, error.localizedDescription)
19 | } else {
20 | completion(true, "Giriş başarılı.")
21 | }
22 | }
23 | }
24 |
25 | // MARK: - Register
26 | func register(userName: String, email: String, password: String, completion: @escaping (Bool, String) -> Void) {
27 | Auth.auth().createUser(withEmail: email, password: password) { result, error in
28 | if let error = error {
29 | print(error.localizedDescription)
30 | } else {
31 | guard let user = result?.user else {
32 | print("registerdan donen kullanıcı yok")
33 | return
34 | }
35 | let fireStore = Firestore.firestore()
36 |
37 | let userDictionaray = [
38 | "userName" : userName
39 | ] as! [String : Any]
40 |
41 | fireStore.collection("UsersInfo")
42 | .document(user.uid)
43 | .setData(userDictionaray) { error in
44 | if let error = error {
45 | print(error.localizedDescription)
46 | }
47 |
48 | completion(true,"Kayıt başarılı")
49 | }
50 | }
51 | }
52 | }
53 |
54 | // MARK: - ForgotPassword
55 | func resetPassword(email: String, completion: @escaping (Bool, String) -> Void) {
56 | guard !email.isEmpty else {
57 | completion(false, "E-posta alanı boş bırakılamaz.")
58 | return
59 | }
60 |
61 | Auth.auth().sendPasswordReset(withEmail: email) { error in
62 | if let error = error {
63 | // Şifre sıfırlama işlemi başarısız
64 | completion(false, "Şifre sıfırlama hatası: \(error.localizedDescription)")
65 | } else {
66 | // Şifre sıfırlama işlemi başarılı
67 | completion(true, "Şifrenizi sıfırlamak için e-posta gönderildi.")
68 | }
69 | }
70 | }
71 |
72 | // MARK: - Change Password
73 | func changePassword(password: String, completion: @escaping (Bool, String) -> Void) {
74 | guard !password.isEmpty else {
75 | completion(false, "Parola alanı boş bırakılamaz.")
76 | return
77 | }
78 | Auth.auth().currentUser?.updatePassword(to: password) { (error) in
79 | if let error = error {
80 | // Şifreyenileme işlemi başarısız
81 | completion(false, "Şifre Güncellendi: \(error.localizedDescription)")
82 | } else {
83 | // Şifre yenileme başarılı
84 | completion(true, "Şifreniz Güncellendi")
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Alert/AlertVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertVC.swift
3 | // // Movie-App
4 | //
5 | // Created by Yaşar Duman on 4.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class AlertVC: UIViewController {
11 |
12 | // MARK: - Properties
13 | private lazy var containerView = AlertContainerView()
14 | private lazy var titleLabel = TitleLabel(textAlignment: .center, fontSize: 20)
15 | private lazy var messageLabel = BodyLabel(textAlignment: .center)
16 | private lazy var actionButton = MovieButton(bgColor: .systemPink, color: .systemPink, title: "Ok", systemImageName: "checkmark.circle")
17 |
18 | var alertTitle: String?
19 | var message: String?
20 | var buttonTitle: String?
21 |
22 | private let padding: CGFloat = 20
23 |
24 | // MARK: - Initialization
25 | init(title: String, message: String, buttonTitle: String) {
26 | super.init(nibName: nil, bundle: nil)
27 | self.alertTitle = title
28 | self.message = message
29 | self.buttonTitle = buttonTitle
30 | }
31 |
32 | required init?(coder: NSCoder) {
33 | fatalError("init(coder:) has not been implemented")
34 | }
35 |
36 | // MARK: - View Lifecycle
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 | view.backgroundColor = UIColor.black.withAlphaComponent(0.75)
40 | view.addSubviewsExt(containerView, titleLabel, actionButton, messageLabel)
41 | configureContainerView()
42 | configureTitleLabel()
43 | configureActionButton()
44 | configureMessageLabel()
45 | }
46 |
47 | // MARK: - UI Configuration
48 | func configureContainerView() {
49 | containerView.centerInSuperview()
50 | containerView.anchor(size: .init(width: 280, height: 220))
51 | }
52 |
53 | func configureTitleLabel() {
54 | titleLabel.text = alertTitle ?? "Something went wrong"
55 |
56 | titleLabel.anchor(top: containerView.topAnchor,
57 | leading: containerView.leadingAnchor,
58 | trailing: containerView.trailingAnchor,
59 | padding: .init(top: 20, leading: 20, trailing: 20),
60 | size: .init(heightSize: 28))
61 | }
62 |
63 | func configureMessageLabel(){
64 | messageLabel.text = message ?? "Unable to complete request"
65 | messageLabel.numberOfLines = 4
66 |
67 | messageLabel.anchor(top: titleLabel.bottomAnchor,
68 | leading: containerView.leadingAnchor,
69 | bottom: actionButton.topAnchor,
70 | trailing: containerView.trailingAnchor,
71 | padding: .init(top: 8, left: 20, bottom: 12, right: 20))
72 |
73 | }
74 |
75 | func configureActionButton() {
76 | actionButton.setTitle(buttonTitle ?? "Ok", for: .normal)
77 | actionButton.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
78 |
79 | actionButton.anchor(leading: containerView.leadingAnchor,
80 | bottom: containerView.bottomAnchor,
81 | trailing: containerView.trailingAnchor,
82 | padding: .init(leading: 20, bottom: 20, trailing: 20),
83 | size: .init(heightSize: 44))
84 |
85 | }
86 |
87 | // MARK: - Actions
88 | @objc private func dismissVC() {
89 | dismiss(animated: true)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Profile/HelpAndSupportUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpAndSupportUIView.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 4.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 |
11 | final class HelpAndSupportUIView: UIView {
12 | // MARK: - UI Elements
13 | private lazy var containerView : UIView = {
14 | let container = UIView()
15 | container.backgroundColor = .secondarySystemBackground
16 | container.layer.cornerRadius = 10
17 | return container
18 | }()
19 |
20 | private lazy var userImage: UIImageView = {
21 | let image = UIImageView()
22 | image.image = UIImage(named: "heroImage")
23 | image.clipsToBounds = true
24 | image.layer.cornerRadius = 35
25 | return image
26 | }()
27 |
28 | private lazy var userName: UILabel = {
29 | let label = UILabel()
30 | label.text = "Yaşar Duman"
31 | label.font = UIFont.systemFont(ofSize: 20)
32 | return label
33 | }()
34 |
35 | private lazy var userMessage: UILabel = {
36 | let label = UILabel()
37 | label.text = "Send you a message"
38 | label.font = UIFont.systemFont(ofSize: 15)
39 | return label
40 | }()
41 |
42 | lazy var sendImage: UIImageView = {
43 | let image = UIImageView()
44 | image.image = UIImage(systemName: "message.badge.filled.fill")
45 | image.tintColor = MovieColor.playButonBG
46 | return image
47 | }()
48 |
49 | // MARK: - Properties
50 | var userEmail: String?
51 |
52 | // MARK: - Initializers
53 | init(userName:String, userImageName: String ,userEmail: String) {
54 | super.init(frame: .zero)
55 | self.userName.text = userName
56 | self.userImage.image = UIImage(named: userImageName)
57 | self.userEmail = userEmail
58 |
59 | configureUI()
60 | }
61 |
62 | required init?(coder: NSCoder) {
63 | fatalError("init(coder:) has not been implemented")
64 | }
65 |
66 | // MARK: - UI Configuration
67 | private func configureUI(){
68 | addSubview(containerView)
69 | containerView.addSubviewsExt(userImage, userName, userMessage, sendImage)
70 | configureContainer()
71 | configureuserImage()
72 | configureUserName()
73 | configureUserMessage()
74 | configureSendIamge()
75 | }
76 |
77 | private func configureContainer(){
78 | containerView.backgroundColor = .tertiarySystemGroupedBackground
79 | containerView.fillSuperview()
80 | }
81 |
82 | private func configureuserImage(){
83 | userImage.anchor(leading: containerView.leadingAnchor,
84 | padding: .init(leading: 15),
85 | size: .init(width: 70, height: 70))
86 |
87 | userImage.centerYInSuperview()
88 | }
89 |
90 | private func configureUserName(){
91 | userName.anchor(top: containerView.topAnchor,
92 | padding: .init(top: 20))
93 |
94 | userName.centerXInSuperview()
95 | }
96 |
97 | private func configureUserMessage(){
98 | userMessage.anchor(top: userName.bottomAnchor,
99 | padding: .init(top: 10))
100 |
101 | userMessage.centerXInSuperview()
102 | }
103 |
104 | private func configureSendIamge(){
105 | sendImage.anchor(trailing: containerView.trailingAnchor,
106 | padding: .init(trailing: 10),
107 | size: .init(width: 35, height: 35))
108 |
109 | sendImage.centerYInSuperview()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "29.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "58.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "87.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "80.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "57.png",
47 | "idiom" : "iphone",
48 | "scale" : "1x",
49 | "size" : "57x57"
50 | },
51 | {
52 | "filename" : "114.png",
53 | "idiom" : "iphone",
54 | "scale" : "2x",
55 | "size" : "57x57"
56 | },
57 | {
58 | "filename" : "120.png",
59 | "idiom" : "iphone",
60 | "scale" : "2x",
61 | "size" : "60x60"
62 | },
63 | {
64 | "filename" : "180.png",
65 | "idiom" : "iphone",
66 | "scale" : "3x",
67 | "size" : "60x60"
68 | },
69 | {
70 | "filename" : "20.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "20x20"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "20x20"
80 | },
81 | {
82 | "filename" : "29.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "29x29"
86 | },
87 | {
88 | "filename" : "58.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "29x29"
92 | },
93 | {
94 | "filename" : "40.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "40x40"
98 | },
99 | {
100 | "filename" : "80.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "40x40"
104 | },
105 | {
106 | "filename" : "50.png",
107 | "idiom" : "ipad",
108 | "scale" : "1x",
109 | "size" : "50x50"
110 | },
111 | {
112 | "filename" : "100.png",
113 | "idiom" : "ipad",
114 | "scale" : "2x",
115 | "size" : "50x50"
116 | },
117 | {
118 | "filename" : "72.png",
119 | "idiom" : "ipad",
120 | "scale" : "1x",
121 | "size" : "72x72"
122 | },
123 | {
124 | "filename" : "144.png",
125 | "idiom" : "ipad",
126 | "scale" : "2x",
127 | "size" : "72x72"
128 | },
129 | {
130 | "filename" : "76.png",
131 | "idiom" : "ipad",
132 | "scale" : "1x",
133 | "size" : "76x76"
134 | },
135 | {
136 | "filename" : "152.png",
137 | "idiom" : "ipad",
138 | "scale" : "2x",
139 | "size" : "76x76"
140 | },
141 | {
142 | "filename" : "167.png",
143 | "idiom" : "ipad",
144 | "scale" : "2x",
145 | "size" : "83.5x83.5"
146 | },
147 | {
148 | "filename" : "1024.png",
149 | "idiom" : "ios-marketing",
150 | "scale" : "1x",
151 | "size" : "1024x1024"
152 | }
153 | ],
154 | "info" : {
155 | "author" : "xcode",
156 | "version" : 1
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/DetailVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 4.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class DetailVC: UIViewController {
11 | // MARK: - Header View
12 | private var headerView = DetailUIView()
13 |
14 | // MARK: - TableView
15 | private let homeFeedTable: UITableView = {
16 | let table = UITableView(frame: .zero, style: .grouped)
17 | table.separatorStyle = .none
18 | table.register(CollectionViewTableViewCell.self, forCellReuseIdentifier: CollectionViewTableViewCell.identifier)
19 | return table
20 | }()
21 |
22 | lazy var movie: Movie? = nil
23 |
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | view.addSubview(homeFeedTable)
28 | configureTableView()
29 | configureHeaderView()
30 | homeFeedTable.tableHeaderView = headerView
31 | homeFeedTable.frame = view.frame
32 | }
33 |
34 | override func viewDidLayoutSubviews() {
35 | super.viewDidLayoutSubviews()
36 | // Güvenli alanın altındaki boşluğu hesaplayın
37 | let safeAreaBottom = view.safeAreaInsets.bottom
38 | //SafeAreaKadar Boşluk bıraktık
39 | homeFeedTable.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: safeAreaBottom, right: 0)
40 |
41 | }
42 |
43 | // MARK: - Configure HeaderView
44 | private func configureHeaderView() {
45 | headerView = DetailUIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 500))
46 | }
47 |
48 |
49 | private func configureTableView() {
50 |
51 |
52 | homeFeedTable.delegate = self
53 | homeFeedTable.dataSource = self
54 |
55 | homeFeedTable.backgroundColor = .tertiarySystemGroupedBackground
56 | homeFeedTable.contentInsetAdjustmentBehavior = .never
57 |
58 |
59 | }
60 |
61 | }
62 |
63 | extension DetailVC : UITableViewDelegate, UITableViewDataSource{
64 | func numberOfSections(in tableView: UITableView) -> Int {
65 | return 1
66 | }
67 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
68 | return 2
69 | }
70 |
71 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
72 | guard let cell = tableView.dequeueReusableCell(withIdentifier: CollectionViewTableViewCell.identifier, for: indexPath) as? CollectionViewTableViewCell else {
73 | return UITableViewCell()
74 | }
75 | cell.backgroundColor = .green
76 | return cell
77 | }
78 |
79 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
80 | return 50
81 | }
82 |
83 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
84 | return 40
85 | }
86 |
87 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
88 | guard let header = view as? UITableViewHeaderFooterView else {return}
89 | header.textLabel?.font = .systemFont(ofSize: 20, weight: .semibold)
90 | header.textLabel?.frame = CGRect(x: header.bounds.origin.x + 20, y: header.bounds.origin.y, width: 100, height: header.bounds.height)
91 | header.textLabel?.textColor = .label
92 | header.textLabel?.text = header.textLabel?.text?.capitalizeFirstLetter()
93 | }
94 |
95 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
96 | return "sectionTitles[section]"
97 | }
98 |
99 |
100 |
101 | }
102 |
103 | #Preview{
104 | DetailVC()
105 | }
106 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Profile/ProfileUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileUIView.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 4.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class ProfileUIView: UIView, UIImagePickerControllerDelegate & UINavigationControllerDelegate{
11 | //MARK: - UI Elements
12 | lazy var containerImage: UIView = {
13 | let container = UIView()
14 | container.layer.cornerRadius = 60
15 | container.layer.shadowColor = UIColor.label.cgColor
16 | container.layer.shadowOffset = CGSize(width: 0, height: 0)
17 | container.layer.shadowOpacity = 0.9
18 | container.layer.shadowRadius = 8
19 | return container
20 | }()
21 |
22 | lazy var userImage: UIImageView = {
23 | let image = UIImageView()
24 | let config = UIImage.SymbolConfiguration(weight: .ultraLight)
25 | image.image = UIImage(systemName: "person.circle",withConfiguration: config)
26 | image.backgroundColor = .systemBackground
27 | image.tintColor = .lightGray
28 | image.clipsToBounds = true
29 | image.layer.cornerRadius = 60
30 | return image
31 | }()
32 |
33 | lazy var userAddImageIcon: UIImageView = {
34 | let image = UIImageView()
35 | let config = UIImage.SymbolConfiguration(weight: .bold)
36 | image.image = UIImage(systemName: "pencil.circle.fill", withConfiguration: config)
37 | image.tintColor = MovieColor.playButonBG
38 | return image
39 | }()
40 |
41 | lazy var userName: UILabel = {
42 | let label = UILabel()
43 | label.text = "Yaşar DUMAN"
44 | label.font = UIFont.systemFont(ofSize: 20)
45 | label.textColor = .label
46 |
47 | return label
48 | }()
49 |
50 |
51 | lazy var userMesage: UILabel = {
52 | let label = UILabel()
53 | label.text = "Tekrardan Hoşgeldin Yaşar 🎉"
54 | label.font = UIFont.systemFont(ofSize: 15)
55 | label.textColor = .secondaryLabel
56 | label.numberOfLines = 0
57 | return label
58 | }()
59 |
60 | // MARK: - Initializers
61 | override init(frame: CGRect) {
62 | super.init(frame: frame)
63 | addSubviewsExt(containerImage, userAddImageIcon, userName, userMesage)
64 | containerImage.addSubview(userImage)
65 | configureContainerImage()
66 | configureImage()
67 | configureLabel()
68 | }
69 |
70 | required init?(coder: NSCoder) {
71 | fatalError("init(coder:) has not been implemented")
72 | }
73 |
74 | // MARK: - UI Configuration
75 | private func configureContainerImage(){
76 |
77 | containerImage.anchor(size: .init(width: 120, height: 120))
78 | containerImage.anchor(leading: leadingAnchor,
79 | padding: .init(top: 0, leading: 20))
80 | containerImage.centerYInSuperview()
81 | }
82 |
83 | private func configureImage(){
84 | userImage.fillSuperview()
85 |
86 | userAddImageIcon.anchor(bottom: userImage.bottomAnchor,
87 | trailing: userImage.trailingAnchor,
88 | padding: .init(bottom: 5, trailing: 5),
89 | size: .init(width: 30, height: 30))
90 | }
91 |
92 | private func configureLabel() {
93 | userName.anchor(top: userImage.topAnchor,
94 | leading: userImage.trailingAnchor,
95 | padding: .init(top: 20, leading: 20))
96 | userMesage.anchor(top: userName.topAnchor,
97 | leading: userImage.trailingAnchor,
98 | trailing: trailingAnchor,
99 | padding: .init(top: 40, leading: 20))
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Search/SearchResultsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchResultsViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol SearchResultsViewControllerDelegate: AnyObject {
11 | func searchResultsViewControllerDidTapItem(_ viewModel: MoviePreviewModel, movieModel: Movie)
12 |
13 | }
14 |
15 | final class SearchResultsViewController: UIViewController {
16 | // MARK: - Properties
17 | public var movies: [Movie] = [Movie]()
18 |
19 | public weak var delegate: SearchResultsViewControllerDelegate?
20 |
21 | // MARK: - UI Elements
22 | public let searchResultsCollectionView: UICollectionView = {
23 |
24 | let layout = UICollectionViewFlowLayout()
25 | layout.itemSize = CGSize(width: UIScreen.main.bounds.width / 3 - 10, height: 200)
26 | layout.minimumInteritemSpacing = 0
27 |
28 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
29 | collectionView.register(MovieCollectionViewCell.self, forCellWithReuseIdentifier: MovieCollectionViewCell.identifier)
30 | return collectionView
31 | }()
32 |
33 | // MARK: - View Lifecycle
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 | view.backgroundColor = .systemBackground
37 | view.addSubview(searchResultsCollectionView)
38 |
39 | searchResultsCollectionView.delegate = self
40 | searchResultsCollectionView.dataSource = self
41 | }
42 |
43 | override func viewDidLayoutSubviews() {
44 | super.viewDidLayoutSubviews()
45 | searchResultsCollectionView.frame = view.bounds
46 | }
47 | }
48 |
49 | // MARK: - UICollectionView Data Source and Delegate
50 | extension SearchResultsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
51 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
52 | return movies.count
53 | }
54 |
55 |
56 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
57 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MovieCollectionViewCell.identifier, for: indexPath) as? MovieCollectionViewCell else {
58 | return UICollectionViewCell()
59 | }
60 |
61 |
62 | let movie = movies[indexPath.row]
63 | cell.configure(with: movie /*?? ""*/)
64 | return cell
65 | }
66 |
67 |
68 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
69 | collectionView.deselectItem(at: indexPath, animated: true)
70 |
71 | let movie = movies[indexPath.row]
72 | let movieName = movie.original_title ?? ""
73 |
74 | Task{
75 | do {
76 | let getUpcomingMovies = try await APICaller.shared.getMovie(with: movieName)
77 | self.delegate?.searchResultsViewControllerDidTapItem(MoviePreviewModel(title: movie.original_title ?? "",
78 | youtubeView: getUpcomingMovies,
79 | movieOverview: movie.overview ?? "",
80 | release_date: movie.release_date ?? movie.first_air_date),
81 | movieModel: movie)
82 | }catch {
83 | if let movieError = error as? MovieError {
84 | print(movieError.rawValue)
85 | } else {
86 | presentAlert(title: "Error", message: error.localizedDescription, buttonTitle: "OK")
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Supporting Files/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 | import FirebaseAuth
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | guard let windowScene = (scene as? UIWindowScene) else { return }
18 | window = UIWindow(windowScene: windowScene)
19 |
20 | // MARK: - DarkMode
21 | let isDarkModeOn = UserDefaults.standard.bool(forKey: "DarkMode")
22 | applyDarkMode(isDarkModeOn)
23 |
24 | // MARK: - onboardingVC
25 | let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
26 | // Uygulama ilk kez açılıyorsa, onboarding ekranını göster
27 |
28 | if !hasLaunchedBefore {
29 | let onboardingVC = OnboardingVC()
30 | onboardingVC.modalPresentationStyle = .fullScreen
31 | window?.rootViewController = onboardingVC
32 |
33 | UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
34 | } else {
35 | let loginVC = LoginVC()
36 | let nav = UINavigationController(rootViewController: loginVC)
37 | nav.modalPresentationStyle = .fullScreen
38 | window?.rootViewController = nav
39 | }
40 |
41 | // MARK: - kullanıcı sürekli giriş yapmamsı için yapılan işlem kullanıcıyı hatırlama işlemi
42 | if Auth.auth().currentUser != nil {
43 | let TabBar = MainTabBarViewController()
44 | TabBar.modalPresentationStyle = .fullScreen
45 | window?.rootViewController = TabBar
46 | }
47 | self.window?.makeKeyAndVisible()
48 | }
49 |
50 | // MARK: - DarkMode
51 | func applyDarkMode(_ isDarkModeOn: Bool) {
52 | if isDarkModeOn {
53 | if #available(iOS 13.0, *) {
54 | window?.overrideUserInterfaceStyle = .dark
55 | }
56 | } else {
57 | if #available(iOS 13.0, *) {
58 | window?.overrideUserInterfaceStyle = .light
59 | }
60 | }
61 | }
62 |
63 | func sceneDidDisconnect(_ scene: UIScene) {
64 | // Called as the scene is being released by the system.
65 | // This occurs shortly after the scene enters the background, or when its session is discarded.
66 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
67 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
68 | }
69 |
70 | func sceneDidBecomeActive(_ scene: UIScene) {
71 | // Called when the scene has moved from an inactive state to an active state.
72 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
73 | }
74 |
75 | func sceneWillResignActive(_ scene: UIScene) {
76 | // Called when the scene will move from an active state to an inactive state.
77 | // This may occur due to temporary interruptions (ex. an incoming phone call).
78 | }
79 |
80 | func sceneWillEnterForeground(_ scene: UIScene) {
81 | // Called as the scene transitions from the background to the foreground.
82 | // Use this method to undo the changes made on entering the background.
83 | }
84 |
85 | func sceneDidEnterBackground(_ scene: UIScene) {
86 | // Called as the scene transitions from the foreground to the background.
87 | // Use this method to save data, release shared resources, and store enough scene-specific state information
88 | // to restore the scene back to its current state.
89 | }
90 |
91 |
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | CineVerse-App 🎬
3 |
4 |
5 | Designed as a mobile application, this platform serves with 5 different categories in the movie universe: trending movies, trending TV, popular movies, upcoming movies, favorites. With API integration, it seamlessly pulls movies from TMDB and allows users to navigate the movie universe, add and remove movies to favorites, search movies, check IMDB score, and watch movie trailers with **YouTube API**. Designed with **MVVM** architecture and **Clean Architecture**, the app offers users a seamless approach to get information about movies whenever they want. 🏆
6 |
7 |
8 |
9 | ## 🎥 Features 🍿
10 | ### 🎥 Categories:
11 | - [x] Trending Movies 📈
12 | - [x] Trending TV 📺
13 | - [x] Popular Movies 👑
14 | - [x] Upcoming Movies 🚀
15 | - [x] Top Rated 🌟
16 |
17 | ### 📡 TMDB API Integration:
18 | - [x] Explore the movie universe 🌐
19 | - [x] Watch trailers (**YouTube API**) 🎬
20 | - [x] Add or ❤️ Remove movies 🗑️ from favorites
21 | - [x] Search for movies 🔍
22 | - [x] Check out IMDB scores 🌟
23 |
24 | ## Libraries 📚
25 |
26 | This app is built with the help of the following libraries and dependencies:
27 |
28 | - Firebase (version 10.17.0) 🔥
29 | - SDWebImage (version 5.18.4) 🖼️
30 | - Lottie (version 4.3.3) 🎮
31 |
32 | ## Screenshots📱
33 |
34 | | On Boarding | Login | Register | Forgot Password |
35 | | --- | --- | --- | --- |
36 | |
|
|
|
|
37 |
38 | | Home | Movie Detail | Search | Search Result |
39 | | --- | --- | --- | --- |
40 | |
|
|
|
|
41 |
42 | | Download | User Profile | Reset Password | Help and Support |
43 | | --- | --- | --- | --- |
44 | |
|
|
|
|
45 |
46 |
47 |
48 | ## Video Preview 🎥
49 |
50 |
51 |
52 |
53 | ## Contact Me 📬
54 |
55 | If you have any questions, encounter issues, or want to provide feedback, please feel free to reach out to me via email at [01.yasarduman@gmail.com](mailto:01.yasarduman@gmail.com)
56 |
57 |
58 |
59 | **Tags:** `Swift` `UIKit` `MVVM` `iOS` `MovieApp` `MovieAppAPI` `ProgrammaticUI` `Programmaticly`
60 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/TextField/CustomTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomTextField.swift
3 | // UIKitLoginWithFireBase
4 | //
5 | // Created by Yaroslav Sokolov on 02/10/2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class CustomTextField: UITextField {
11 |
12 | enum CustomTextFieldType {
13 | case username
14 | case email
15 | case password
16 | }
17 | private lazy var rightButton = UIButton(type: .custom)
18 | private let authField: CustomTextFieldType
19 |
20 | init(fieldType: CustomTextFieldType) {
21 | self.authField = fieldType
22 | super.init(frame: .zero)
23 |
24 |
25 | translatesAutoresizingMaskIntoConstraints = false
26 |
27 | self.backgroundColor = .secondarySystemBackground
28 | //self.layer.cornerRadius = 10
29 | layer.borderWidth = 2
30 | layer.borderColor = UIColor.systemGray4.cgColor
31 | textColor = .label
32 | tintColor = .label
33 | textAlignment = .left
34 |
35 | adjustsFontSizeToFitWidth = true
36 | minimumFontSize = 12
37 |
38 | self.leftViewMode = .always
39 | self.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: self.frame.size.height))
40 |
41 | self.returnKeyType = .done
42 | // Klavyenin "Return" (veya "Done") düğmesinin işlevini belirler. .done, klavyede "Done" düğmesini gösterir ve genellikle bir metin girişi işlemi tamamlandığında kullanılır.
43 |
44 | self.autocorrectionType = .no
45 | // Otomatik düzeltme işlemini denetler. .no, otomatik düzeltme özelliğini devre dışı bırakır. Bu, kullanıcının metin girişi yaparken yazım hatalarının düzeltilmesini önler.
46 |
47 | self.autocapitalizationType = .none
48 | // Otomatik büyük harf dönüşümünü yönetir. .none, otomatik büyük harf dönüşümünü devre dışı bırakır, yani kullanıcı tarafından girilen metin tamamen küçük harfle kalır.
49 |
50 |
51 |
52 |
53 |
54 | switch fieldType {
55 | case .username:
56 | self.placeholder = "Username"
57 |
58 | case .email:
59 | self.placeholder = "Email address"
60 | self.keyboardType = .emailAddress
61 | // Klavye türünü belirler. .emailAddress, kullanıcının bir e-posta adresi girmesi gerektiğini belirten bir klavye türüdür. Bu, klavyede @ sembolü ve diğer e-posta adresi karakterlerini kolayca erişilebilir hale getirir.
62 |
63 | self.textContentType = .emailAddress
64 | // Metin içeriği türünü belirler. .emailAddress, metin girişi sırasında otomatik tamamlama ve önerileri etkinleştirir ve kullanıcının e-posta adresi girmesini kolaylaştırır. Örneğin, daha önce kullanılan e-posta adreslerini önerme yeteneği gibi özellikleri içerebilir.
65 |
66 | case .password:
67 | self.placeholder = "Password"
68 | self.textContentType = .password
69 | self.isSecureTextEntry = true
70 |
71 | rightButton.setImage(UIImage(systemName: "eye.slash.fill") , for: .normal)
72 | rightButton.addTarget(self, action: #selector(toggleShowHide), for: .touchUpInside)
73 | rightButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: -16, bottom: 0, right: 0)
74 | rightButton.frame = CGRect(x:0, y:0, width:30, height:30)
75 |
76 |
77 | rightViewMode = .always
78 | rightView = rightButton
79 |
80 | }
81 | }
82 |
83 | @objc
84 | func toggleShowHide(button: UIButton) {
85 | toggle()
86 | }
87 |
88 | func toggle() {
89 | isSecureTextEntry = !isSecureTextEntry
90 | if isSecureTextEntry {
91 | rightButton.setImage(UIImage(systemName: "eye.slash.fill") , for: .normal)
92 | } else {
93 | rightButton.setImage(UIImage(systemName: "eye.fill") , for: .normal)
94 | }
95 | }
96 |
97 | required init?(coder: NSCoder) {
98 | fatalError("init(coder:) has not been implemented")
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Home/CollectionViewTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewTableViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | protocol CollectionViewTableViewCellDelegate: AnyObject {
12 | func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel: MoviePreviewModel, movieModel: Movie)
13 | }
14 |
15 | final class CollectionViewTableViewCell: UITableViewCell {
16 | // MARK: - Properties
17 | weak var delegate: CollectionViewTableViewCellDelegate?
18 | static let identifier = "CollectionViewTableViewCell"
19 | private var movies: [Movie] = []
20 |
21 |
22 | // MARK: - UI Elements
23 | private lazy var 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(MovieCollectionViewCell.self, forCellWithReuseIdentifier: MovieCollectionViewCell.identifier)
29 | return collectionView
30 | }()
31 |
32 | // MARK: - Initialization
33 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
34 | super.init(style: style, reuseIdentifier: reuseIdentifier)
35 | contentView.addSubview(collectionView)
36 | collectionView.backgroundColor = .tertiarySystemGroupedBackground
37 | collectionView.delegate = self
38 | collectionView.dataSource = self
39 | }
40 |
41 | required init?(coder: NSCoder) {
42 | fatalError()
43 | }
44 |
45 | // MARK: - Layout
46 | override func layoutSubviews() {
47 | super.layoutSubviews()
48 | collectionView.frame = contentView.bounds
49 | }
50 |
51 | // MARK: - Configuration
52 | public func configure(with movie: [Movie]) {
53 | self.movies = movie
54 |
55 | DispatchQueue.main.async { [weak self] in
56 | self?.collectionView.reloadData()
57 | }
58 | }
59 | }
60 |
61 | // MARK: - UICollectionViewDelegate, UICollectionViewDataSource
62 | extension CollectionViewTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
63 |
64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
65 |
66 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MovieCollectionViewCell.identifier, for: indexPath) as? MovieCollectionViewCell else {
67 | return UICollectionViewCell()
68 | }
69 |
70 | if let model: Movie = movies[indexPath.row] as? Movie {
71 | cell.configure(with: model)
72 | } else {
73 | return UICollectionViewCell()
74 | }
75 |
76 | return cell
77 | }
78 |
79 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
80 | return movies.count
81 | }
82 |
83 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
84 | collectionView.deselectItem(at: indexPath, animated: true)
85 |
86 | let movie = movies[indexPath.row]
87 | guard let movieName = movie.original_title ?? movie.original_name else {
88 | return
89 | }
90 |
91 | Task{
92 | do {
93 | let moviePreviewModel = try await APICaller.shared.getMovie(with: movieName + " trailer")
94 | guard let movieOverview = movie.overview else {
95 | return
96 | }
97 |
98 | let viewModel = MoviePreviewModel(title: movieName, youtubeView: moviePreviewModel, movieOverview: movieOverview, release_date: movie.release_date ?? movie.first_air_date)
99 | self.delegate?.collectionViewTableViewCellDidTapCell(self, viewModel: viewModel, movieModel: movie)
100 |
101 | }catch {
102 | if let movieError = error as? MovieError {
103 | print(movieError.rawValue)
104 | } else {
105 |
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "abseil-cpp-binary",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/google/abseil-cpp-binary.git",
7 | "state" : {
8 | "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c",
9 | "version" : "1.2022062300.0"
10 | }
11 | },
12 | {
13 | "identity" : "firebase-ios-sdk",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/firebase/firebase-ios-sdk",
16 | "state" : {
17 | "revision" : "8872dbd7d947acf757abab933da10e83c1842280",
18 | "version" : "10.17.0"
19 | }
20 | },
21 | {
22 | "identity" : "googleappmeasurement",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/google/GoogleAppMeasurement.git",
25 | "state" : {
26 | "revision" : "6b332152355c372ace9966d8ee76ed191f97025e",
27 | "version" : "10.17.0"
28 | }
29 | },
30 | {
31 | "identity" : "googledatatransport",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/google/GoogleDataTransport.git",
34 | "state" : {
35 | "revision" : "aae45a320fd0d11811820335b1eabc8753902a40",
36 | "version" : "9.2.5"
37 | }
38 | },
39 | {
40 | "identity" : "googleutilities",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/google/GoogleUtilities.git",
43 | "state" : {
44 | "revision" : "1cd556b33550982ec17f80e358253d905e756f0f",
45 | "version" : "7.11.6"
46 | }
47 | },
48 | {
49 | "identity" : "grpc-binary",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/google/grpc-binary.git",
52 | "state" : {
53 | "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98",
54 | "version" : "1.49.1"
55 | }
56 | },
57 | {
58 | "identity" : "gtm-session-fetcher",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/google/gtm-session-fetcher.git",
61 | "state" : {
62 | "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd",
63 | "version" : "3.1.1"
64 | }
65 | },
66 | {
67 | "identity" : "interop-ios-for-google-sdks",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
70 | "state" : {
71 | "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
72 | "version" : "100.0.0"
73 | }
74 | },
75 | {
76 | "identity" : "leveldb",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/firebase/leveldb.git",
79 | "state" : {
80 | "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
81 | "version" : "1.22.2"
82 | }
83 | },
84 | {
85 | "identity" : "lottie-ios",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/airbnb/lottie-ios",
88 | "state" : {
89 | "revision" : "45517c3cfec9469bbdd4f86e32393c28ae9df0bc",
90 | "version" : "4.3.3"
91 | }
92 | },
93 | {
94 | "identity" : "nanopb",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/firebase/nanopb.git",
97 | "state" : {
98 | "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
99 | "version" : "2.30909.0"
100 | }
101 | },
102 | {
103 | "identity" : "promises",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/google/promises.git",
106 | "state" : {
107 | "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e",
108 | "version" : "2.3.1"
109 | }
110 | },
111 | {
112 | "identity" : "sdwebimage",
113 | "kind" : "remoteSourceControl",
114 | "location" : "https://github.com/SDWebImage/SDWebImage.git",
115 | "state" : {
116 | "revision" : "fd1950de05a5ad77cb252fd88576c1e1809ee50d",
117 | "version" : "5.18.4"
118 | }
119 | },
120 | {
121 | "identity" : "swift-protobuf",
122 | "kind" : "remoteSourceControl",
123 | "location" : "https://github.com/apple/swift-protobuf.git",
124 | "state" : {
125 | "revision" : "07f7f26ded8df9645c072f220378879c4642e063",
126 | "version" : "1.25.1"
127 | }
128 | }
129 | ],
130 | "version" : 2
131 | }
132 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/ViewModels/HomeVM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeVM.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 31.10.2023.
6 | //
7 |
8 |
9 | // MARK: - Sections Enum
10 | enum Sections: Int {
11 | case TrendingMovies = 0
12 | case TrendingTv = 1
13 | case Popular = 2
14 | case Upcoming = 3
15 | case TopRated = 4
16 | }
17 |
18 | protocol HomeVMInterface {
19 |
20 | func viewDidLoad()
21 | func showDetail(movie: Movie)
22 | func getMovies()
23 | }
24 |
25 | final class HomeVM {
26 | private weak var view: HomeViewInterface?
27 |
28 | // MARK: - Data Arrays
29 | lazy var trendingMovies: [Movie] = []
30 | lazy var TrendingTVs: [Movie] = []
31 | lazy var UpcomingMovies: [Movie] = []
32 | lazy var Popular: [Movie] = []
33 | lazy var TopRated: [Movie] = []
34 |
35 |
36 | init(view: HomeViewInterface? = nil) {
37 | self.view = view
38 | }
39 |
40 |
41 |
42 | func showLoadingView() {
43 | view?.showLoadingIndicator()
44 | }
45 |
46 | func hideLoadingView() {
47 | view?.dismissLoadingIndicator()
48 | }
49 | }
50 |
51 |
52 | extension HomeVM: HomeVMInterface {
53 | func showDetail(movie: Movie) {
54 | let vc = MoviePreviewViewController()
55 | Task{
56 | do {
57 | let moviePreviewModel = try await APICaller.shared.getMovie(with: movie.original_title! + " trailer")
58 | guard let movieOverview = movie.overview else {
59 | return
60 | }
61 |
62 | let viewModel = MoviePreviewModel(title: movie.original_title!,
63 | youtubeView: moviePreviewModel,
64 | movieOverview: movieOverview,
65 | release_date: movie.release_date ?? movie.first_air_date)
66 | await vc.configure(with: viewModel, moviModelIsFavori: movie)
67 | view?.pushVC(vc)
68 |
69 | }catch {
70 | if let movieError = error as? MovieError {
71 | print(movieError.rawValue)
72 | } else {
73 | view?.alert(title: "Error!", message: error.localizedDescription, buttonTitle: "Ok")
74 | }
75 | }
76 | }
77 | }
78 |
79 | func viewDidLoad() {
80 | view?.configureViewDidLoad()
81 | getMovies()
82 | }
83 |
84 | func getMovies(){
85 | showLoadingView()
86 | Task{
87 | do {
88 | let getTrendingMovies = try await APICaller.shared.getTrendingMovies().results
89 | let getTrendingTVs = try await APICaller.shared.getTrendingTVs().results
90 | let getUpcomingMovies = try await APICaller.shared.getUpcomingMovies().results
91 | let getPopularMovies = try await APICaller.shared.getPopular().results
92 | let getTopRated = try await APICaller.shared.getTopRated().results
93 |
94 |
95 | updateTable(with: getTrendingMovies, for: .TrendingMovies)
96 | updateTable(with: getTrendingTVs, for: .TrendingTv)
97 | updateTable(with: getUpcomingMovies, for: .Popular)
98 | updateTable(with: getPopularMovies, for: .Upcoming)
99 | updateTable(with: getTopRated, for: .TopRated)
100 |
101 | view?.configureHeaderView(with: getTrendingMovies)
102 | hideLoadingView()
103 | }catch {
104 | if let movieError = error as? MovieError {
105 | print(movieError.rawValue)
106 | } else {
107 |
108 | }
109 | hideLoadingView()
110 | }
111 | }
112 | }
113 |
114 | // MARK: - Data Update
115 | private func updateTable(with data: [Movie]? = nil, for section: Sections) {
116 | switch section {
117 | case .TrendingMovies:
118 | trendingMovies = data ?? []
119 | case .TrendingTv:
120 | TrendingTVs = data ?? []
121 | case .Popular:
122 | Popular = data ?? []
123 | case .Upcoming:
124 | UpcomingMovies = data ?? []
125 | case .TopRated:
126 | TopRated = data ?? []
127 | }
128 |
129 | view?.tableViewReloadData()
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/DownloadsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadsViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol DownloadVCInterface{
11 | func configureViewDidLoad()
12 | func tableViewReloadData()
13 | func pushVC(vc: UIViewController)
14 | func alert(title: String, message: String, buttonTitle: String)
15 | }
16 |
17 | final class DownloadsViewController: UIViewController {
18 | //MARK: - Variables
19 | private lazy var viewModel: DownloadsVM? = DownloadsVM(view: self)
20 |
21 |
22 | // MARK: - UI Elements
23 | private lazy var tableView: UITableView = {
24 | let tableView = UITableView()
25 | tableView.separatorStyle = .none
26 | tableView.register(MovieTableViewCell.self, forCellReuseIdentifier: MovieTableViewCell.identifier)
27 | return tableView
28 | }()
29 |
30 | // MARK: - View Lifecycle
31 | override func viewDidLoad() {
32 | super.viewDidLoad()
33 | viewModel?.viewDidLoad()
34 |
35 | }
36 | override func viewWillAppear(_ animated: Bool) {
37 | viewModel?.refreshUI()
38 | }
39 |
40 | override func viewDidLayoutSubviews() {
41 | super.viewDidLayoutSubviews()
42 | tableView.frame = view.frame
43 | }
44 | // MARK: - Helper Functions
45 | private func configureTableView(){
46 | view.addSubview(tableView)
47 | tableView.delegate = self
48 | tableView.dataSource = self
49 | tableView.backgroundColor = .secondarySystemBackground
50 | }
51 | }
52 |
53 | // MARK: - UITableViewDelegate & UITableViewDataSource
54 | extension DownloadsViewController: UITableViewDelegate, UITableViewDataSource{
55 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
56 | return viewModel?.movies.count ?? 0
57 | }
58 |
59 |
60 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
61 | guard let cell = tableView.dequeueReusableCell(withIdentifier: MovieTableViewCell.identifier, for: indexPath) as? MovieTableViewCell else {
62 | return UITableViewCell()
63 | }
64 |
65 | let movie = viewModel?.movies[indexPath.row]
66 |
67 | if let movieName = movie?.original_title ?? viewModel?.movies[indexPath.row].original_name,
68 | let posterURL = movie?.poster_path,
69 | let imdbScore = movie?.vote_average,
70 | let movieDate = movie?.release_date ?? viewModel?.movies[indexPath.row].first_air_date {
71 |
72 | cell.configure(with: MovieCellModel(titleName: movieName, posterURL: posterURL, vote_average: imdbScore, release_date: movieDate))
73 | }
74 |
75 | return cell
76 | }
77 |
78 |
79 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
80 | tableView.deselectRow(at: indexPath, animated: true)
81 | viewModel?.didSelectRowAt(at: indexPath)
82 | }
83 |
84 |
85 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
86 | return 150
87 | }
88 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
89 | let deleteAction = UIContextualAction(style: .destructive, title: "Sil") { contextualAction, view, boolValue in
90 | let movies = self.viewModel?.movies[indexPath.row]
91 | self.viewModel?.removeFromFavorites(movies: movies!)
92 | self.viewModel?.refreshUI()
93 | }
94 |
95 | return UISwipeActionsConfiguration(actions: [deleteAction])
96 | }
97 | }
98 |
99 | extension DownloadsViewController: DownloadVCInterface{
100 | func alert(title: String, message: String, buttonTitle: String) {
101 | presentAlert(title: title, message:message, buttonTitle: buttonTitle)
102 | }
103 |
104 | func pushVC(vc: UIViewController) {
105 | DispatchQueue.main.async {
106 | self.navigationController?.pushViewController(vc, animated: true)
107 | }
108 | }
109 |
110 | func tableViewReloadData() {
111 | self.tableView.reloadData()
112 | }
113 |
114 | func configureViewDidLoad() {
115 | navigationController?.navigationBar.tintColor = MovieColor.playButonBG
116 | view.backgroundColor = .secondarySystemBackground
117 | configureTableView()
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Auth/ForgotPasswordVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForgotPasswordVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 18.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class ForgotPasswordVC: UIViewController {
11 | // MARK: - Properties
12 | private let HeadLabel = TitleLabel(textAlignment: .left, fontSize: 20)
13 | private lazy var emailTextField = CustomTextField(fieldType: .email)
14 | private lazy var forgotPasswordButton = MovieButton( bgColor:MovieColor.playButonBG ,color:MovieColor.playButonBG, title: "Submit", fontSize: .big)
15 | private let infoLabel = SecondaryTitleLabel(fontSize: 16)
16 | private lazy var signInButton = MovieButton( bgColor:.clear ,color: .label, title: "Sign In.", fontSize: .small)
17 |
18 | private lazy var stackView = UIStackView()
19 | private let authVM : AuthVM? = AuthVM()
20 |
21 | // MARK: - View Controller Lifecycle
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | configureViewController()
25 | configureHeadLabel()
26 | configureTextField()
27 | configureForgotPassword()
28 | configureStackView()
29 | }
30 |
31 |
32 | // MARK: - UI Configuration
33 | func configureViewController() {
34 | view.backgroundColor = .systemBackground
35 | self.navigationItem.setHidesBackButton(true, animated: true)
36 | view.addSubviewsExt(HeadLabel, emailTextField, forgotPasswordButton, stackView)
37 | }
38 |
39 | private func configureHeadLabel() {
40 | HeadLabel.text = "Forgot Password"
41 |
42 | HeadLabel.anchor(top: view.topAnchor,
43 | leading: view.leadingAnchor,
44 | //trailing: view.trailingAnchor,
45 | padding: .init(top: 80, leading: 20))
46 | }
47 |
48 | private func configureTextField() {
49 | emailTextField.anchor(top: HeadLabel.bottomAnchor,
50 | leading: view.leadingAnchor,
51 | trailing: view.trailingAnchor,
52 | padding: .init(top: 40, leading: 20, trailing: 20),
53 | size: .init(heightSize: 50))
54 | }
55 |
56 | private func configureForgotPassword(){
57 | forgotPasswordButton.configuration?.cornerStyle = .capsule
58 |
59 | forgotPasswordButton.anchor(top: emailTextField.bottomAnchor,
60 | leading: view.leadingAnchor,
61 | trailing: view.trailingAnchor,
62 | padding: .init(top: 20, leading: 20, trailing: 20),
63 | size: .init(heightSize: 50))
64 |
65 | forgotPasswordButton.addTarget(self, action: #selector(didTapForgotPassword), for: .touchUpInside)
66 | }
67 |
68 | private func configureStackView() {
69 | stackView.axis = .horizontal
70 |
71 | stackView.addArrangedSubview(infoLabel)
72 | stackView.addArrangedSubview(signInButton)
73 |
74 | infoLabel.text = "Already have an account?"
75 |
76 | stackView.anchor(top: forgotPasswordButton.bottomAnchor,
77 | padding: .init(top: 5))
78 |
79 | stackView.centerXInSuperview()
80 |
81 | signInButton.addTarget(self, action: #selector(didTapSignIn), for: .touchUpInside)
82 | }
83 |
84 | // MARK: - Actions
85 | @objc private func didTapForgotPassword(){
86 | //Email & Password Validation
87 |
88 | guard let email = emailTextField.text else {
89 | return
90 | }
91 |
92 | guard email.isValidEmail(email: email) else {
93 | presentAlert(title: "Alert!", message: "Invalide Email Address", buttonTitle: "Ok")
94 | return
95 | }
96 |
97 | authVM?.resetPassword(email: email) { [weak self] success, error in
98 | guard let self = self else { return }
99 |
100 | if success {
101 | self.presentAlert(title: "Alert!", message: "Password renewal request sent to your e-mail address 🥳", buttonTitle: "Ok")
102 | self.navigationController?.popToRootViewController(animated: true)
103 | } else {
104 | self.presentAlert(title: "Alert!", message: error, buttonTitle: "Ok")
105 | }
106 | }
107 | }
108 |
109 | @objc private func didTapSignIn() {
110 |
111 | self.navigationController?.popToRootViewController(animated: true)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Profile/HelpAndSupportVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpAndSupportVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 4.11.2023.
6 | //
7 |
8 | import UIKit
9 | import MessageUI
10 |
11 | final class HelpAndSupportVC: UIViewController, MFMailComposeViewControllerDelegate {
12 |
13 | private let mailComposer = MFMailComposeViewController()
14 | private let headLabel = TitleLabel(textAlignment: .center, fontSize: 25)
15 | private let secoLabel = SecondaryTitleLabel(fontSize: 20)
16 | lazy var getInTouchImage: UIImageView = {
17 | let image = UIImageView()
18 | image.image = UIImage(named: "getInTouch")
19 | image.contentMode = .scaleAspectFit
20 | return image
21 | }()
22 |
23 | // MARK: - Header View
24 | private var user1 = HelpAndSupportUIView(
25 | userName: "Yaşar Duman",
26 | userImageName: "userAvatar",
27 | userEmail: "01.yasarduman@gmail.com")
28 | private var user2 = HelpAndSupportUIView(
29 | userName: "Erislam Nurluyol",
30 | userImageName: "userAvatar",
31 | userEmail: "01.yasarduman@gmail.com")
32 |
33 | override func viewDidLoad() {
34 | super.viewDidLoad()
35 | view.backgroundColor = .secondarySystemGroupedBackground
36 |
37 | configureGetInTouchImage()
38 | configureHeadText()
39 | configureUser1()
40 | configureUser2()
41 | }
42 |
43 | // MARK: - Cofigure UI
44 | private func configureGetInTouchImage(){
45 | view.addSubview(getInTouchImage)
46 | getInTouchImage.anchor(top: view.safeAreaLayoutGuide.topAnchor,
47 | leading: view.leadingAnchor,
48 | trailing: view.trailingAnchor,
49 |
50 | size: .init(width: 0, height: 300))
51 | }
52 |
53 | private func configureHeadText(){
54 | view.addSubview(headLabel)
55 | view.addSubview(secoLabel)
56 |
57 | headLabel.text = "Get In Touch"
58 | secoLabel.text = "If you have any inquiries get in touch with us. We'll be happy to help you"
59 |
60 | headLabel.anchor(top: getInTouchImage.bottomAnchor)
61 | headLabel.centerXInSuperview()
62 |
63 | secoLabel.numberOfLines = 2
64 | secoLabel.textAlignment = .center
65 | secoLabel.anchor(top: headLabel.bottomAnchor,
66 | leading: view.leadingAnchor,
67 | trailing: view.trailingAnchor,
68 | padding: .init(top: 20, left: 20, bottom: 0, right: 20)
69 | )
70 | }
71 |
72 | // MARK: - Configure Users
73 | private func configureUser1() {
74 | view.addSubview(user1)
75 | user1.anchor(top: secoLabel.bottomAnchor,
76 | leading: view.leadingAnchor,
77 | trailing: view.trailingAnchor,
78 | padding: .init(top: 20, left: 10, bottom: 0, right: 10),
79 | size: .init(width: 0, height: 100))
80 |
81 | // MARK: - SendEmail
82 | mailComposer.setToRecipients([user1.userEmail!]) // E-posta alıcısı
83 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendEmail))
84 | user1.sendImage.isUserInteractionEnabled = true // UIImageView'ı etkileşimli hale getirin
85 | user1.sendImage.addGestureRecognizer(tapGestureRecognizer)
86 | }
87 |
88 | private func configureUser2() {
89 | view.addSubview(user2)
90 | user2.anchor(top: user1.bottomAnchor,
91 | leading: view.leadingAnchor,
92 | trailing: view.trailingAnchor,
93 | padding: .init(top: 20, left: 10, bottom: 0, right: 10),
94 | size: .init(width: 0, height: 100)
95 | )
96 | // MARK: - SendEmail
97 | mailComposer.setToRecipients([user2.userEmail!]) // E-posta alıcısı
98 | let tapGestureRecognizer2 = UITapGestureRecognizer(target: self, action: #selector(sendEmail))
99 | user2.sendImage.isUserInteractionEnabled = true // UIImageView'ı etkileşimli hale getirin
100 | user2.sendImage.addGestureRecognizer(tapGestureRecognizer2)
101 | }
102 |
103 | // MARK: - Action
104 | @objc func sendEmail() {
105 | if MFMailComposeViewController.canSendMail() {
106 | mailComposer.mailComposeDelegate = self
107 | present(mailComposer, animated: true, completion: nil)
108 | } else {
109 | presentAlert(title: "Hata", message: "E-posta gönderme işlevi kullanılamıyor.", buttonTitle: "Ok")
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Profile/SwitchTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchTableViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class SwitchTableViewCell: UITableViewCell {
11 | // MARK: - Properties
12 | static let identifier = "SwitchTableViewCell"
13 |
14 | // MARK: - UI Elements
15 | private lazy var iconContainer: UIView = {
16 | let view = UIView()
17 | view.clipsToBounds = true
18 | view.layer.cornerRadius = 10
19 | view.layer.masksToBounds = true
20 | return view
21 | }()
22 |
23 | private lazy var iconImageView: UIImageView = {
24 | let imageView = UIImageView()
25 | imageView.tintColor = MovieColor.playButonBG
26 | imageView.contentMode = .scaleAspectFit
27 | return imageView
28 | }()
29 |
30 | private let label: UILabel = {
31 | let label = UILabel()
32 | label.numberOfLines = 1
33 | return label
34 | }()
35 |
36 | private lazy var mySwitch: UISwitch = {
37 | let mySwitch = UISwitch()
38 | mySwitch.onTintColor = MovieColor.goldColor
39 | return mySwitch
40 | }()
41 |
42 | // MARK: - Initializers
43 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
44 | super.init(style: style, reuseIdentifier: reuseIdentifier)
45 | contentView.addSubviewsExt(label, iconContainer, mySwitch)
46 | iconContainer.addSubview(iconImageView)
47 |
48 | contentView.clipsToBounds = true
49 | accessoryType = .disclosureIndicator
50 |
51 | // UserDefaults veya başka bir ayar mekanizması ile Dark Mode durumunu kontrol edin
52 | mySwitch.addTarget(self, action: #selector(darkModeSwitchValueChanged(_:)), for: .valueChanged)
53 | updateDarkModeUI()
54 | }
55 |
56 | @objc func darkModeSwitchValueChanged(_ sender: UISwitch) {
57 | let isDarkModeOn = sender.isOn
58 |
59 | if isDarkModeOn {
60 | UserDefaults.standard.set(true, forKey: "DarkMode")
61 | } else {
62 | UserDefaults.standard.set(false, forKey: "DarkMode")
63 | }
64 |
65 | updateDarkModeUI()
66 |
67 | if #available(iOS 15.0, *) {
68 | if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
69 | windowScene.windows.forEach { window in
70 | window.overrideUserInterfaceStyle = isDarkModeOn ? .dark : .light
71 | }
72 | }
73 | } else {
74 | UIApplication.shared.windows.forEach { window in
75 | window.overrideUserInterfaceStyle = isDarkModeOn ? .dark : .light
76 | }
77 | }
78 | }
79 |
80 | // MARK: - Dark Mode Handling
81 | private func updateDarkModeUI() {
82 | let isDarkModeOn = UserDefaults.standard.bool(forKey: "DarkMode")
83 | mySwitch.isOn = isDarkModeOn
84 | }
85 |
86 | required init?(coder: NSCoder) {
87 | fatalError("init(coder:) has not been implemented")
88 | }
89 |
90 | // MARK: - Layout Subviews
91 | override func layoutSubviews() {
92 | super.layoutSubviews()
93 |
94 | mySwitch.sizeToFit()
95 | let mySwitchwidth: CGFloat = mySwitch.frame.size.width
96 | let mySwitchheight: CGFloat = mySwitch.frame.size.height
97 |
98 | let size: CGFloat = contentView.frame.size.height - 12
99 | let imageSize: CGFloat = size/1.5
100 |
101 | iconContainer.anchor(leading: leadingAnchor,
102 | padding: .init( leading: 15),
103 | size: .init(width: size, height: size))
104 |
105 | iconContainer.centerYInSuperview()
106 |
107 | iconImageView.anchor(size: .init(width: imageSize, height: imageSize))
108 | iconImageView.centerXInSuperview()
109 | iconImageView.centerYInSuperview()
110 |
111 | label.anchor(leading: iconContainer.trailingAnchor,
112 | padding: .init(leading: 20))
113 |
114 | label.centerYInSuperview()
115 |
116 | mySwitch.anchor(trailing: contentView.trailingAnchor,
117 | padding: .init(trailing: 20),
118 | size: .init(width: mySwitchwidth, height: mySwitchheight))
119 |
120 | mySwitch.centerYInSuperview()
121 | }
122 |
123 | // MARK: - Prepare For Reuse
124 | override func prepareForReuse() {
125 | super.prepareForReuse()
126 | iconImageView.image = nil
127 | label.text = nil
128 | iconContainer.backgroundColor = nil
129 | mySwitch.isOn = false
130 | }
131 |
132 | // MARK: - Configure Cell
133 | func configure(with model: SettingsSwitchOption){
134 | label.text = model.title
135 | iconImageView.image = model.icon
136 | iconContainer.backgroundColor = model.iconBackgrondColor
137 | mySwitch.isOn = model.isOn
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Profile/ChangePasswordVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangePasswordVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class ChangePasswordVC: UIViewController {
11 |
12 | // MARK: - Properties
13 | private let HeadLabel = TitleLabel(textAlignment: .left, fontSize: 20)
14 | private lazy var passwordTextField = CustomTextField(fieldType: .password)
15 | private lazy var repasswordTextField = CustomTextField(fieldType: .password)
16 | private lazy var resetButton = MovieButton( bgColor: MovieColor.playButonBG ,color: MovieColor.playButonBG, title: "Reset", fontSize: .big)
17 | private let authVM : AuthVM? = AuthVM()
18 |
19 | // MARK: - View Controller Lifecycle
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 | configureViewController()
23 | configureHeadLabel()
24 | configureTextField()
25 | configureResetButton()
26 | }
27 |
28 | func configureViewController() {
29 | view.backgroundColor = .systemBackground
30 | view.addSubviewsExt(HeadLabel, passwordTextField, repasswordTextField, resetButton)
31 | }
32 |
33 | // MARK: - Configuration
34 | private func configureHeadLabel() {
35 | HeadLabel.text = "Reset Password"
36 |
37 | HeadLabel.anchor(top: view.safeAreaLayoutGuide.topAnchor,
38 | leading: view.leadingAnchor,
39 | padding: .init(top: 0, left: 20, bottom: 0, right: 0))
40 |
41 | }
42 |
43 | private func configureTextField() {
44 | passwordTextField.placeholder = "New Password"
45 | passwordTextField.anchor(top: HeadLabel.bottomAnchor,
46 | leading: view.leadingAnchor,
47 | trailing: view.trailingAnchor,
48 | padding: .init(top: 40, left: 20, bottom: 0, right: 20),
49 | size: .init(width: 0, height: 50))
50 |
51 | repasswordTextField.placeholder = "Confirm Password"
52 |
53 | repasswordTextField.anchor(top: passwordTextField.bottomAnchor,
54 | leading: view.leadingAnchor,
55 | trailing: view.trailingAnchor,
56 | padding: .init(top: 20, left: 20, bottom: 0, right: 20),
57 | size: .init(width: 0, height: 50))
58 | }
59 |
60 | private func configureResetButton(){
61 | resetButton.configuration?.cornerStyle = .capsule
62 |
63 | resetButton.anchor(top: repasswordTextField.bottomAnchor,
64 | leading: view.leadingAnchor,
65 | trailing: view.trailingAnchor,
66 | padding: .init(top: 20, left: 20, bottom: 0, right: 20),
67 | size: .init(width: 0, height: 50))
68 |
69 | resetButton.addTarget(self, action: #selector(didTapResetButton), for: .touchUpInside)
70 | }
71 |
72 | // MARK: - Action
73 | @objc private func didTapResetButton() {
74 | //Email & Password Validation
75 | guard let password = passwordTextField.text,
76 | let rePassword = repasswordTextField.text else{
77 | presentAlert(title: "Alert!", message: "Email and Password ?", buttonTitle: "Ok")
78 | return
79 | }
80 |
81 | guard password.isValidPassword(password: password) else {
82 |
83 | guard password.count >= 6 else {
84 | presentAlert(title: "Alert!", message: "Password must be at least 6 characters", buttonTitle: "Ok")
85 | return
86 | }
87 |
88 | guard password.containsDigits(password) else {
89 | presentAlert(title: "Alert!", message: "Password must contain at least 1 digit", buttonTitle: "Ok")
90 | return
91 | }
92 |
93 | guard password.containsLowerCase(password) else {
94 | presentAlert(title: "Alert!", message: "Password must contain at least 1 lowercase character", buttonTitle: "Ok")
95 | return
96 | }
97 |
98 | guard password.containsUpperCase(password) else {
99 | presentAlert(title: "Alert!", message: "Password must contain at least 1 uppercase character", buttonTitle: "Ok")
100 | return
101 | }
102 |
103 | guard password == rePassword else {
104 | presentAlert(title: "Alert!", message: "Password and password repeat are not the same", buttonTitle: "Ok")
105 | return
106 | }
107 | return
108 | }
109 |
110 | authVM?.changePassword(password: rePassword) { [weak self] success, error in
111 | guard let self = self else { return }
112 |
113 | if success {
114 | passwordTextField.text = ""
115 | repasswordTextField.text = ""
116 | self.presentAlert(title: "Alert!", message: "Password change Successful 🥳", buttonTitle: "Ok")
117 |
118 | } else {
119 | self.presentAlert(title: "Alert!", message: error, buttonTitle: "Ok")
120 | }
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Search/SearchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | protocol searchVCInterface: AnyObject {
12 | func configureViewDidLoad()
13 | func discoverTableReloadData()
14 | func alert(title: String, message: String, buttonTitle: String)
15 | func pushVC(vc: UIViewController)
16 |
17 | }
18 |
19 | final class SearchViewController: UIViewController {
20 |
21 | // MARK: - Properties
22 | private lazy var viewModel = SearchVM(view: self)
23 |
24 | // MARK: - UI Elements
25 | private let discoverTable: UITableView = {
26 | let table = UITableView()
27 | table.register(MovieTableViewCell.self, forCellReuseIdentifier: MovieTableViewCell.identifier)
28 | return table
29 | }()
30 |
31 | private let searchController: UISearchController = {
32 | let controller = UISearchController(searchResultsController: SearchResultsViewController())
33 | controller.searchBar.placeholder = "Search for a Movie or a Tv show"
34 | controller.searchBar.searchBarStyle = .minimal
35 | return controller
36 | }()
37 |
38 | // MARK: - View Lifecycle
39 | override func viewDidLoad() {
40 | super.viewDidLoad()
41 | viewModel.viewDidLoad()
42 | }
43 |
44 | override func viewDidLayoutSubviews() {
45 | super.viewDidLayoutSubviews()
46 | discoverTable.frame = view.bounds
47 | }
48 | }
49 |
50 | // MARK: - Table View Data Source and Delegate
51 | extension SearchViewController: UITableViewDataSource, UITableViewDelegate {
52 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
53 | return viewModel.movies.count;
54 | }
55 |
56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
57 |
58 | guard let cell = tableView.dequeueReusableCell(withIdentifier: MovieTableViewCell.identifier, for: indexPath) as? MovieTableViewCell else {
59 | return UITableViewCell()
60 | }
61 |
62 | let movie = viewModel.movies[indexPath.row]
63 | let model = MovieCellModel(titleName: movie.original_name ?? movie.original_title ?? "Unknown name",
64 | posterURL: movie.poster_path ?? "",
65 | vote_average: movie.vote_average ?? 0.0,
66 | release_date: movie.release_date ?? movie.first_air_date)
67 |
68 | cell.configure(with: model)
69 | return cell;
70 | }
71 |
72 |
73 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
74 | return 140
75 | }
76 |
77 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
78 | tableView.deselectRow(at: indexPath, animated: true)
79 | viewModel.didSelectRow(at: indexPath)
80 | }
81 | }
82 |
83 | // MARK: - UISearchResultsUpdating and SearchResultsViewControllerDelegate
84 | extension SearchViewController: UISearchResultsUpdating, SearchResultsViewControllerDelegate {
85 |
86 | func updateSearchResults(for searchController: UISearchController) {
87 | let searchBar = searchController.searchBar
88 |
89 | guard let query = searchBar.text,
90 | !query.trimmingCharacters(in: .whitespaces).isEmpty,
91 | query.trimmingCharacters(in: .whitespaces).count >= 3,
92 | let resultsController = searchController.searchResultsController as? SearchResultsViewController else {
93 | return
94 | }
95 | resultsController.delegate = self
96 |
97 | Task{
98 | do {
99 | let getUpcomingMovies = try await APICaller.shared.search(with: query).results.filter({$0.poster_path != nil})
100 | resultsController.movies = getUpcomingMovies
101 | resultsController.searchResultsCollectionView.reloadData()
102 |
103 | }catch {
104 | if let movieError = error as? MovieError {
105 | print(movieError.rawValue)
106 | } else {
107 |
108 | }
109 | }
110 | }
111 | }
112 |
113 |
114 |
115 | // MARK: - DidTapItem SearchResults
116 | func searchResultsViewControllerDidTapItem(_ viewModel: MoviePreviewModel, movieModel: Movie) {
117 |
118 | DispatchQueue.main.async { [weak self] in
119 | let vc = MoviePreviewViewController()
120 | vc.configure(with: viewModel,moviModelIsFavori: movieModel)
121 | self?.navigationController?.pushViewController(vc, animated: true)
122 | }
123 | }
124 | }
125 |
126 | extension SearchViewController: searchVCInterface {
127 |
128 | func pushVC(vc: UIViewController) {
129 | self.navigationController?.pushViewController(vc, animated: true)
130 | }
131 |
132 | func configureViewDidLoad() {
133 | title = "Search"
134 | navigationController?.navigationBar.prefersLargeTitles = true
135 | navigationController?.navigationItem.largeTitleDisplayMode = .always
136 |
137 | view.backgroundColor = .systemBackground
138 |
139 | view.addSubview(discoverTable)
140 | discoverTable.delegate = self
141 | discoverTable.dataSource = self
142 | navigationItem.searchController = searchController
143 |
144 | navigationController?.navigationBar.tintColor = MovieColor.playButonBG
145 |
146 | searchController.searchResultsUpdater = self
147 | }
148 |
149 | func discoverTableReloadData() {
150 | DispatchQueue.main.async {
151 | self.discoverTable.reloadData()
152 | }
153 | }
154 |
155 | func alert(title: String, message: String, buttonTitle: String) {
156 | presentAlert(title: title, message: message, buttonTitle: buttonTitle)
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Home/HeroHeaderUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeroHeaderUIView.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol HeroHeaderUIViewProtocol: AnyObject {
11 | func showDetail(movie: Movie)
12 | }
13 |
14 | final class HeroHeaderUIView: UIView {
15 | // MARK: - Properties
16 | weak var delegate: HeroHeaderUIViewProtocol?
17 | var movie: Movie?
18 |
19 | //MARK: - UI Elements
20 | // Movie Name Label
21 | private lazy var movieName: UILabel = {
22 | let label = UILabel()
23 | label.text = "MoviName"
24 | label.textColor = .label
25 | label.font = UIFont.systemFont(ofSize: 25,weight: .bold)
26 | return label
27 | }()
28 |
29 | // Stack View for IMDb Rating
30 | private lazy var stackView: UIStackView = {
31 | let stackView = UIStackView()
32 | stackView.axis = .horizontal
33 | stackView.spacing = 8
34 | stackView.alignment = .center
35 | return stackView
36 | }()
37 |
38 | // IMDb Rating Label
39 | lazy var imdbLabel: UILabel = {
40 | let label = UILabel()
41 | label.text = "8.7"
42 | label.textColor = .label
43 | label.font = UIFont.systemFont(ofSize: 25,weight: .bold)
44 | return label
45 | }()
46 |
47 | // IMDb Star Icon
48 | private lazy var imdbImageView: UIImageView = {
49 | let imageView = UIImageView()
50 | imageView.contentMode = .scaleAspectFill
51 | imageView.clipsToBounds = true
52 | imageView.tintColor = MovieColor.goldColor
53 | imageView.image = UIImage(systemName: "star.fill")
54 | return imageView
55 | }()
56 |
57 | // Play Button
58 | private lazy var playButton = MovieButton(bgColor: MovieColor.playButonBG,
59 | color: MovieColor.playButonBG,
60 | title: "Play",
61 | systemImageName: "arrowtriangle.right.fill",
62 | cornerStyle: .small)
63 | // Download Button
64 | private lazy var downloadButton = MovieButton(bgColor: .systemRed,
65 | color: .systemRed,
66 | title: "Download",
67 | systemImageName: "arrow.down.to.line",
68 | cornerStyle: .small)
69 |
70 | // Hero Image View
71 | private lazy var heroImageView: UIImageView = {
72 | let imageView = UIImageView()
73 | imageView.contentMode = .scaleAspectFill
74 | imageView.clipsToBounds = true
75 | imageView.image = UIImage(named: "heroImage")
76 | return imageView
77 | }()
78 |
79 | // Gradient Layer
80 | let gradientLayer = CAGradientLayer()
81 |
82 |
83 | //MARK: - Initializers
84 | override init(frame: CGRect) {
85 | super.init(frame: frame)
86 | addSubview(heroImageView)
87 | addGradient()
88 | addSubviewsExt(playButton, downloadButton, movieName, stackView)
89 | stackView.addArrangedSubview(imdbLabel)
90 | stackView.addArrangedSubview(imdbImageView)
91 |
92 | ConfigureUI()
93 | }
94 |
95 | required init?(coder: NSCoder) {
96 | fatalError()
97 | }
98 |
99 | // MARK: - LayoutSubviews
100 | override func layoutSubviews() {
101 | super.layoutSubviews()
102 | heroImageView.frame = bounds
103 | gradientLayer.colors = [
104 | UIColor.clear.cgColor,
105 | UIColor.tertiarySystemGroupedBackground.cgColor
106 | ]
107 | downloadButton.layer.borderColor = UIColor.label.cgColor
108 | }
109 |
110 | private func addGradient() {
111 | gradientLayer.colors = [
112 | UIColor.clear.cgColor,
113 | UIColor.tertiarySystemGroupedBackground.cgColor
114 | ]
115 | gradientLayer.frame = bounds
116 | layer.addSublayer(gradientLayer)
117 | }
118 |
119 | // MARK: - ConfigureUI
120 | private func ConfigureUI() {
121 | configurePlayButton()
122 | configureDownloadButton()
123 | configuremoviName()
124 | configureStackView()
125 | }
126 |
127 | private func configurePlayButton() {
128 | playButton.anchor(leading: leadingAnchor,
129 | bottom: bottomAnchor,
130 | padding: .init(leading: 20, bottom: 30),
131 | size: .init(width: 120, height: 46))
132 | playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
133 | }
134 |
135 | private func configureDownloadButton() {
136 | downloadButton.anchor(leading: playButton.trailingAnchor,
137 | bottom: bottomAnchor,
138 | padding: .init(leading: 20, bottom: 30),
139 | size: .init(width: 140, height: 46))
140 | }
141 |
142 | private func configuremoviName() {
143 | movieName.anchor(leading:playButton.leadingAnchor,
144 | bottom: playButton.topAnchor,
145 | padding: .init(bottom: 20))
146 | }
147 |
148 | private func configureStackView() {
149 | stackView.anchor(leading: movieName.leadingAnchor,
150 | bottom: movieName.topAnchor,
151 | padding: .init(bottom: 10))
152 | }
153 |
154 | @objc private func playButtonTapped() {
155 | DispatchQueue.main.async {
156 | self.delegate?.showDetail(movie: self.movie!)
157 | }
158 | }
159 |
160 | // MARK: - UpdateData
161 | func configure(with model: Movie) {
162 | movie = model
163 |
164 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(String(describing: model.poster_path!))") else {
165 | return
166 | }
167 |
168 | heroImageView.sd_setImage(with: url, completed: nil)
169 |
170 | if let voteAverage = model.vote_average {
171 | let formattedValue = String(format: "%.1f", voteAverage)
172 | DispatchQueue.main.async {
173 | self.imdbLabel.text = formattedValue
174 | self.movieName.text = model.original_title ?? model.original_name
175 | }
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Extensions/UIView+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Ext.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 |
10 | import UIKit
11 |
12 | extension UIView{
13 |
14 | // MARK: - Adding Subviews
15 | func addSubviewsExt(_ views: UIView...) {
16 | for view in views { addSubview(view)}
17 | }
18 |
19 | // MARK: - Auto Layout Constraints
20 | @discardableResult
21 | func anchor(top: NSLayoutYAxisAnchor? = nil,
22 | leading: NSLayoutXAxisAnchor? = nil,
23 | bottom: NSLayoutYAxisAnchor? = nil,
24 | trailing: NSLayoutXAxisAnchor? = nil,
25 | padding: UIEdgeInsets = .zero,
26 | size: CGSize = .zero) -> AnchoredConstraints {
27 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
28 | translatesAutoresizingMaskIntoConstraints = false
29 |
30 | var anchoredConstraints = AnchoredConstraints()
31 |
32 | // MARK: - Top Anchor
33 | if let top = top { anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top) }
34 |
35 | // MARK: - Leading Anchor
36 | if let leading = leading {anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)}
37 |
38 | // MARK: - Bottom Anchor
39 | if let bottom = bottom { anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom) }
40 |
41 | // MARK: - Trailing Anchor
42 | if let trailing = trailing {anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)}
43 |
44 | // MARK: - Width Anchor
45 | if size.width != 0 { anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width) }
46 |
47 | // MARK: - Height Anchor
48 | if size.height != 0 { anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height) }
49 |
50 | // Activate constraints
51 | [anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach { $0?.isActive = true }
52 |
53 | return anchoredConstraints
54 | }
55 |
56 | func fillSuperview(padding: UIEdgeInsets = .zero) {
57 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
58 | translatesAutoresizingMaskIntoConstraints = false
59 |
60 | // MARK: - Top Anchor
61 | if let superviewTopAnchor = superview?.topAnchor {
62 | topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
63 | }
64 |
65 | // MARK: - Bottom Anchor
66 | if let superviewBottomAnchor = superview?.bottomAnchor {
67 | bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
68 | }
69 |
70 | // MARK: - Leading Anchor
71 | if let superviewLeadingAnchor = superview?.leadingAnchor {
72 | leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
73 | }
74 |
75 | // MARK: - Trailing Anchor
76 | if let superviewTrailingAnchor = superview?.trailingAnchor {
77 | trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
78 | }
79 | }
80 |
81 | func centerInSuperview(size: CGSize = .zero) {
82 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
83 | translatesAutoresizingMaskIntoConstraints = false
84 |
85 | // MARK: - Center X Anchor
86 | if let superviewCenterXAnchor = superview?.centerXAnchor {
87 | centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
88 | }
89 |
90 | // MARK: - Center Y Anchor
91 | if let superviewCenterYAnchor = superview?.centerYAnchor {
92 | centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
93 | }
94 |
95 | // MARK: - Width Anchor
96 | if size.width != 0 {
97 | widthAnchor.constraint(equalToConstant: size.width).isActive = true
98 | }
99 |
100 | // MARK: - Height Anchor
101 | if size.height != 0 {
102 | heightAnchor.constraint(equalToConstant: size.height).isActive = true
103 | }
104 | }
105 |
106 | // MARK: - Center in Superview
107 | func centerXInSuperview() {
108 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
109 | translatesAutoresizingMaskIntoConstraints = false
110 |
111 | // MARK: - Center X Anchor
112 | if let superViewCenterXAnchor = superview?.centerXAnchor {
113 | centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
114 | }
115 | }
116 |
117 | func centerYInSuperview() {
118 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
119 | translatesAutoresizingMaskIntoConstraints = false
120 |
121 | // MARK: - Center Y Anchor
122 | if let centerY = superview?.centerYAnchor {
123 | centerYAnchor.constraint(equalTo: centerY).isActive = true
124 | }
125 |
126 | func constrainWidth(constant: CGFloat) {
127 | // Set translatesAutoresizingMaskIntoConstraints to false for auto layout
128 | translatesAutoresizingMaskIntoConstraints = false
129 |
130 | // MARK: - Width Anchor
131 | widthAnchor.constraint(equalToConstant: constant).isActive = true
132 | }
133 |
134 | func constrainHeight(constant: CGFloat) {
135 | // Set translates to false for auto layout
136 | translatesAutoresizingMaskIntoConstraints = false
137 |
138 | // MARK: - Height Anchor
139 | heightAnchor.constraint(equalToConstant: constant).isActive = true
140 | }
141 | }
142 |
143 | /// A structure that holds Auto Layout constraints for anchoring views.
144 | struct AnchoredConstraints {
145 | var top, leading, bottom, trailing, width, height: NSLayoutConstraint?
146 | }
147 | }
148 |
149 | // UIEdgeInsets initinde değer verilmek istenmeyen parametrelerin default değeri 0 olarak ayarlandı.
150 | extension UIEdgeInsets {
151 | init(top: CGFloat = .zero, leading: CGFloat = .zero, bottom: CGFloat = .zero, trailing: CGFloat = .zero) {
152 | self.init(top: top, left: leading, bottom: bottom, right: trailing)
153 | }
154 | }
155 |
156 | // CGSize initinde değer verilmek istenmeyen parametrelerin default değeri 0 olarak ayarlandı.
157 | extension CGSize {
158 | init(widthSize: CGFloat = .zero, heightSize: CGFloat = .zero) {
159 | self.init(width: widthSize, height: heightSize)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Search/MovieTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TitleTableViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | final class MovieTableViewCell: UITableViewCell {
12 |
13 | static let identifier = "TitleTableViewCell"
14 |
15 | //MARK: - UI Elements
16 | private lazy var containerView: UIView = {
17 | let container = UIView()
18 | container.backgroundColor = .secondarySystemBackground
19 | container.layer.cornerRadius = 15
20 | container.layer.shadowColor = UIColor.label.cgColor
21 | container.layer.shadowOffset = CGSize(width: 0, height: 0)
22 | container.layer.shadowOpacity = 0.6
23 | container.layer.shadowRadius = 4
24 | container.layer.masksToBounds = false
25 | return container
26 | }()
27 |
28 | private lazy var titleStackView: UIStackView = {
29 | let stackView = UIStackView()
30 | stackView.axis = .horizontal
31 | stackView.spacing = 8
32 | stackView.alignment = .center
33 | return stackView
34 | }()
35 |
36 | lazy var titleLabel: UILabel = {
37 | let label = UILabel()
38 | label.text = "Başlık glecek Inanıyorum"
39 | label.lineBreakMode = .byTruncatingTail
40 | label.numberOfLines = 1
41 | return label
42 | }()
43 |
44 | private let playTitleButton: UIButton = {
45 | let button = UIButton()
46 | let image = UIImage(systemName: "play.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
47 | button.setImage(image, for: .normal)
48 | button.tintColor = .label
49 | return button
50 | }()
51 |
52 |
53 |
54 | lazy var titlesPosterUIImageView: UIImageView = {
55 | let imageView = UIImageView()
56 | imageView.contentMode = .scaleAspectFill
57 | imageView.image = UIImage(named: "heroImage")
58 | imageView.layer.masksToBounds = true
59 | imageView.layer.maskedCorners = [
60 | .layerMinXMinYCorner,
61 | .layerMinXMaxYCorner
62 | ]
63 | imageView.layer.cornerRadius = 15
64 | return imageView
65 | }()
66 |
67 | private lazy var DateStackView: UIStackView = {
68 | let stackView = UIStackView()
69 | stackView.axis = .horizontal
70 | stackView.spacing = 8
71 | stackView.alignment = .center
72 | return stackView
73 | }()
74 |
75 | lazy var movieReleaseDate: UILabel = {
76 | let label = UILabel()
77 | label.text = "2023-09-13"
78 | label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
79 | label.textColor = .secondaryLabel
80 | return label
81 | }()
82 |
83 | private lazy var ReleaseDateImage: UIImageView = {
84 | let image = UIImageView()
85 | image.image = UIImage(systemName: "calendar.badge.clock")
86 | image.contentMode = .scaleAspectFill
87 | image.clipsToBounds = true
88 | image.tintColor = .label
89 | return image
90 | }()
91 |
92 | private lazy var imdbStackView: UIStackView = {
93 | let stackView = UIStackView()
94 | stackView.axis = .horizontal
95 | stackView.spacing = 8
96 | stackView.alignment = .center
97 | return stackView
98 | }()
99 |
100 | lazy var imdbLabel: UILabel = {
101 | let label = UILabel()
102 | label.text = "8.7"
103 | label.textColor = .label
104 | label.font = UIFont.systemFont(ofSize: 15,weight: .bold)
105 | return label
106 | }()
107 |
108 | private lazy var imdbImageView: UIImageView = {
109 | let imageView = UIImageView()
110 | imageView.contentMode = .scaleAspectFit
111 | imageView.clipsToBounds = true
112 | imageView.tintColor = MovieColor.goldColor
113 | imageView.image = UIImage(systemName: "star.fill")
114 | return imageView
115 | }()
116 |
117 | // MARK: - Initializers
118 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
119 | super.init(style: style, reuseIdentifier: reuseIdentifier)
120 | backgroundColor = .secondarySystemBackground
121 | selectionStyle = .none
122 | contentView.addSubview(containerView)
123 | containerView.addSubviewsExt(titlesPosterUIImageView,titleStackView,DateStackView,imdbStackView)
124 |
125 | titleStackView.addArrangedSubviewsExt(titleLabel, playTitleButton)
126 | DateStackView.addArrangedSubviewsExt(ReleaseDateImage, movieReleaseDate)
127 | imdbStackView.addArrangedSubviewsExt(imdbLabel, imdbImageView)
128 |
129 | applyConstraints()
130 | }
131 |
132 | // MARK: - Constraints
133 | private func applyConstraints() {
134 |
135 | containerView.anchor(top: contentView.topAnchor,
136 | leading: contentView.leadingAnchor,
137 | bottom: contentView.bottomAnchor,
138 | trailing: contentView.trailingAnchor,
139 | padding: .init(top: 10, left: 10, bottom: 10, right: 10))
140 |
141 | titlesPosterUIImageView.anchor(top: containerView.topAnchor,
142 | leading: containerView.leadingAnchor,
143 | bottom: containerView.bottomAnchor,
144 | size: .init(widthSize: 100))
145 |
146 | titleStackView.anchor(top: containerView.topAnchor,
147 | leading: titlesPosterUIImageView.trailingAnchor,
148 | trailing: containerView.trailingAnchor,
149 | padding: .init(top: 25, leading: 10,trailing: 10))
150 |
151 | playTitleButton.anchor(size: .init(width: 35, height: 35))
152 |
153 | DateStackView.anchor(leading: titleLabel.leadingAnchor,
154 | bottom: containerView.bottomAnchor,
155 | padding: .init(top: 15, bottom: 10))
156 |
157 | imdbStackView.anchor(bottom: containerView.bottomAnchor,
158 | trailing: containerView.trailingAnchor,
159 | padding: .init(top: 15, bottom: 10, trailing: 10))
160 | }
161 |
162 | // MARK: - Public Methods
163 | func configure(with model: MovieCellModel) {
164 |
165 | guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(model.posterURL)") else {
166 | return
167 | }
168 | titlesPosterUIImageView.sd_setImage(with: url, completed: nil)
169 | titleLabel.text = model.titleName
170 | movieReleaseDate.text = model.release_date
171 |
172 | if let voteAverage = model.vote_average {
173 | let formattedValue = String(format: "%.1f", voteAverage)
174 | self.imdbLabel.text = formattedValue
175 | }
176 | }
177 |
178 | required init?(coder: NSCoder) {
179 | fatalError()
180 | }
181 | }
182 |
183 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Views/Dowload/DownloadTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DowloadTableViewCell.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 3.11.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | final class DownloadTableViewCell: UITableViewCell {
11 | //MARK: - Variables
12 | static let reuseID = "dowloadTableViewCell"
13 | private var movies: [Movie] = []
14 |
15 | //MARK: - UI Elements
16 | private lazy var containerView: UIView = {
17 | let container = UIView()
18 | container.backgroundColor = .secondarySystemBackground
19 | container.layer.cornerRadius = 15
20 | container.layer.shadowColor = UIColor.label.cgColor
21 | container.layer.shadowOffset = CGSize(width: 0, height: 0)
22 | container.layer.shadowOpacity = 0.6
23 | container.layer.shadowRadius = 4
24 | container.layer.masksToBounds = false
25 | return container
26 | }()
27 |
28 | lazy var movieImage: UIImageView = {
29 | let image = UIImageView()
30 | image.contentMode = .scaleAspectFill
31 | image.image = UIImage(named: "heroImage")
32 | image.layer.masksToBounds = true
33 | image.layer.maskedCorners = [
34 | .layerMinXMinYCorner,
35 | .layerMinXMaxYCorner
36 | ]
37 | image.layer.cornerRadius = 15
38 | return image
39 | }()
40 |
41 | lazy var movieName: UILabel = {
42 | let label = UILabel()
43 | label.text = "A Haunting in Venice"
44 | label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
45 | label.textColor = .label
46 | return label
47 | }()
48 |
49 | lazy var movieOverview: UILabel = {
50 | let label = UILabel()
51 | label.text = "Celebrated sleuth Hercule Poirot, now retired and living in self-imposed exile in Venice, reluctantly attends a Halloween séance at a decaying,"
52 | label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
53 | label.numberOfLines = 2
54 | label.textColor = .label
55 | return label
56 | }()
57 |
58 | private lazy var DateStackView: UIStackView = {
59 | let stackView = UIStackView()
60 | stackView.axis = .horizontal
61 | stackView.spacing = 8
62 | stackView.alignment = .center
63 | return stackView
64 | }()
65 |
66 | lazy var movieReleaseDate: UILabel = {
67 | let label = UILabel()
68 | label.text = "2023-09-13"
69 | label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
70 | label.textColor = .secondaryLabel
71 | return label
72 | }()
73 |
74 | private lazy var ReleaseDateImage: UIImageView = {
75 | let image = UIImageView()
76 | image.image = UIImage(systemName: "calendar.badge.clock")
77 | image.contentMode = .scaleAspectFill
78 | image.clipsToBounds = true
79 | image.tintColor = .label
80 | return image
81 | }()
82 |
83 | private lazy var imdbStackView: UIStackView = {
84 | let stackView = UIStackView()
85 | stackView.axis = .horizontal
86 | stackView.spacing = 8
87 | stackView.alignment = .center
88 | return stackView
89 | }()
90 |
91 | lazy var imdbLabel: UILabel = {
92 | let label = UILabel()
93 | label.text = "8.7"
94 | label.textColor = .label
95 | label.font = UIFont.systemFont(ofSize: 15,weight: .bold)
96 | return label
97 | }()
98 |
99 | private lazy var imdbImageView: UIImageView = {
100 | let imageView = UIImageView()
101 | imageView.contentMode = .scaleAspectFit
102 | imageView.clipsToBounds = true
103 | imageView.tintColor = MovieColor.goldColor
104 | imageView.image = UIImage(systemName: "star.fill")
105 | return imageView
106 | }()
107 |
108 | //MARK: - Initializers
109 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
110 | super.init(style: style, reuseIdentifier: reuseIdentifier)
111 | backgroundColor = .secondarySystemBackground
112 | configureUI()
113 | }
114 |
115 | required init?(coder: NSCoder) {
116 | fatalError("init(coder:) has not been implemented")
117 | }
118 |
119 | override func layoutSubviews() {
120 | super.layoutSubviews()
121 | containerView.layer.shadowColor = UIColor.label.cgColor
122 | }
123 |
124 | // MARK: - UI Configiration
125 | private func configureUI() {
126 | contentView.addSubview(containerView)
127 | containerView.addSubviewsExt(movieName,movieImage,movieOverview,DateStackView,imdbStackView)
128 | DateStackView.addArrangedSubviewsExt(ReleaseDateImage, movieReleaseDate)
129 | imdbStackView.addArrangedSubviewsExt(imdbLabel, imdbImageView)
130 |
131 | configureContainerView()
132 | configureMovieImage()
133 | configureMovieName()
134 | configureMovieOverview()
135 | configureDateStackView()
136 | configureImdbStackView()
137 | }
138 |
139 | private func configureContainerView(){
140 | containerView.anchor(top: contentView.topAnchor,
141 | leading: contentView.leadingAnchor,
142 | bottom: contentView.bottomAnchor,
143 | trailing: contentView.trailingAnchor,
144 | padding: .init(top: 10, left: 10, bottom: 10, right: 10))
145 | }
146 |
147 | private func configureMovieImage(){
148 | movieImage.anchor(top: containerView.topAnchor,
149 | leading: containerView.leadingAnchor,
150 | bottom: containerView.bottomAnchor,
151 | size: .init(widthSize: 100)
152 | )
153 | }
154 | private func configureMovieName(){
155 | movieName.anchor(top: containerView.topAnchor,
156 | leading: movieImage.trailingAnchor,
157 | trailing: imdbImageView.leadingAnchor,
158 | padding: .init(top: 18, leading: 10, trailing: 5))
159 | }
160 | private func configureMovieOverview(){
161 | movieOverview.anchor(top: movieName.bottomAnchor,
162 | leading: movieImage.trailingAnchor,
163 | trailing: containerView.trailingAnchor,
164 | padding: .init(top: 10, leading: 10, trailing: 10))
165 | }
166 |
167 | private func configureDateStackView(){
168 | DateStackView.anchor(top: movieOverview.bottomAnchor,
169 | leading: movieImage.trailingAnchor,
170 | bottom: containerView.bottomAnchor,
171 | padding: .init(top: 10, leading: 10, trailing: 10))
172 | }
173 |
174 | private func configureImdbStackView(){
175 | imdbStackView.anchor(top: movieOverview.bottomAnchor,
176 | bottom: containerView.bottomAnchor,
177 | trailing: containerView.trailingAnchor,
178 | padding: .init(top: 10, leading: 10, trailing: 10))
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Managers/APICaller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APICaller.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import Foundation
10 |
11 | struct Constants {
12 | static let API_KEY = "9f34b030b7187aab01fbc340d02601ee"
13 | static let baseURL = "https://api.themoviedb.org"
14 | //static let YoutubeAPI_KEY = "AIzaSyCPmahsG3SOBFZ7TD5bYVfKygfIrxpbjnE"
15 | static let YoutubeAPI_KEY = "AIzaSyBaWHkGN5wJs9rJzawpDJ40cNV1C7FYsC4"
16 | static let YoutubeBaseURL = "https://youtube.googleapis.com/youtube/v3/search?"
17 | }
18 |
19 | final class APICaller {
20 | static let shared = APICaller()
21 | let decoder = JSONDecoder()
22 |
23 | func getTrendingMovies() async throws -> MovieResponse {
24 |
25 | guard let url = URL(string: "\(Constants.baseURL)/3/trending/movie/day?api_key=\(Constants.API_KEY)") else {
26 | throw MovieError.invalidUrl
27 | }
28 |
29 | do {
30 | let (data, response) = try await URLSession.shared.data(from: url)
31 |
32 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
33 | throw MovieError.invalidResponse
34 | }
35 |
36 | return try decoder.decode(MovieResponse.self, from: data)
37 | } catch {
38 | throw MovieError.invalidData
39 | }
40 | }
41 |
42 | func getTrendingTVs() async throws -> MovieResponse {
43 |
44 | guard let url = URL(string: "\(Constants.baseURL)/3/trending/tv/day?api_key=\(Constants.API_KEY)") else {
45 | throw MovieError.invalidUrl
46 | }
47 |
48 | do {
49 | let (data, response) = try await URLSession.shared.data(from: url)
50 |
51 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
52 | throw MovieError.invalidResponse
53 | }
54 |
55 | return try decoder.decode(MovieResponse.self, from: data)
56 | } catch {
57 | throw MovieError.invalidData
58 | }
59 | }
60 |
61 | func getUpcomingMovies() async throws -> MovieResponse {
62 |
63 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/upcoming?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {
64 | throw MovieError.invalidUrl
65 | }
66 |
67 | do {
68 | let (data, response) = try await URLSession.shared.data(from: url)
69 |
70 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
71 | throw MovieError.invalidResponse
72 | }
73 |
74 | return try decoder.decode(MovieResponse.self, from: data)
75 | } catch {
76 | throw MovieError.invalidData
77 | }
78 | }
79 |
80 |
81 | func getPopular() async throws -> MovieResponse {
82 |
83 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/popular?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {
84 | throw MovieError.invalidUrl
85 | }
86 |
87 | do {
88 | let (data, response) = try await URLSession.shared.data(from: url)
89 |
90 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
91 | throw MovieError.invalidResponse
92 | }
93 |
94 | return try decoder.decode(MovieResponse.self, from: data)
95 | } catch {
96 | throw MovieError.invalidData
97 | }
98 | }
99 |
100 | func getTopRated() async throws -> MovieResponse {
101 |
102 | guard let url = URL(string: "\(Constants.baseURL)/3/movie/top_rated?api_key=\(Constants.API_KEY)&language=en-US&page=1") else {
103 | throw MovieError.invalidUrl
104 | }
105 |
106 | do {
107 | let (data, response) = try await URLSession.shared.data(from: url)
108 |
109 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
110 | throw MovieError.invalidResponse
111 | }
112 |
113 | return try decoder.decode(MovieResponse.self, from: data)
114 | } catch {
115 | throw MovieError.invalidData
116 | }
117 | }
118 |
119 |
120 | func getDiscoverMovies() async throws -> MovieResponse {
121 |
122 | 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 {
123 | throw MovieError.invalidUrl
124 | }
125 |
126 | do {
127 | let (data, response) = try await URLSession.shared.data(from: url)
128 |
129 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
130 | throw MovieError.invalidResponse
131 | }
132 |
133 | return try decoder.decode(MovieResponse.self, from: data)
134 | } catch {
135 | throw MovieError.invalidData
136 | }
137 | }
138 |
139 |
140 |
141 | func search(with query: String) async throws -> MovieResponse {
142 | guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { throw MovieError.invalidUrl }
143 |
144 | guard let url = URL(string: "\(Constants.baseURL)/3/search/movie?api_key=\(Constants.API_KEY)&query=\(query)") else {
145 | throw MovieError.invalidUrl
146 | }
147 |
148 |
149 | do {
150 | let (data, response) = try await URLSession.shared.data(from: url)
151 |
152 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
153 | throw MovieError.invalidResponse
154 | }
155 |
156 | return try decoder.decode(MovieResponse.self, from: data)
157 | } catch {
158 | throw MovieError.invalidData
159 | }
160 | }
161 |
162 | func getMovie(with query: String) async throws -> VideoElement {
163 | guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { throw MovieError.invalidUrl}
164 | guard let url = URL(string: "\(Constants.YoutubeBaseURL)q=\(query)&key=\(Constants.YoutubeAPI_KEY)") else { throw MovieError.invalidUrl}
165 |
166 | do {
167 | let (data, response) = try await URLSession.shared.data(from: url)
168 |
169 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
170 | throw MovieError.invalidResponse
171 | }
172 |
173 | let results = try JSONDecoder().decode(YoutubeSearchResponse.self, from: data)
174 |
175 | return results.items[0]
176 | } catch {
177 | throw MovieError.invalidData
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Auth/LoginVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 15.10.2023.
6 | //
7 |
8 |
9 |
10 | import UIKit
11 | import Firebase
12 |
13 | final class LoginVC: UIViewController {
14 | // MARK: - Properties
15 | private let HeadLabel = TitleLabel(textAlignment: .left, fontSize: 20)
16 | private lazy var emailTextField = CustomTextField(fieldType: .email)
17 | private lazy var passwordTextField = CustomTextField(fieldType: .password)
18 | private lazy var signInButton = MovieButton( bgColor:MovieColor.playButonBG ,color: MovieColor.playButonBG , title: "Sign In", fontSize: .big)
19 | private let infoLabel = SecondaryTitleLabel(fontSize: 16)
20 | private lazy var newUserButton = MovieButton( bgColor:.clear ,color: .label, title: "Sign Up.", fontSize: .small)
21 | private lazy var forgotPasswordButton = MovieButton( bgColor:.clear ,color: MovieColor.playButonBG , title: "Forgot password?", fontSize: .small)
22 |
23 | private lazy var stackView = UIStackView()
24 | private let authVM : AuthVM? = AuthVM()
25 | // MARK: - View Controller Lifecycle
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 | view.backgroundColor = .systemBackground
29 | view.addSubviewsExt(HeadLabel, emailTextField, passwordTextField, forgotPasswordButton, signInButton,stackView)
30 |
31 | configureHeadLabel()
32 | configureTextField()
33 | configureForgotPassword()
34 | configureSignIn()
35 | configureStackView()
36 | }
37 | // MARK: - UI Configuration
38 |
39 | private func configureHeadLabel() {
40 | HeadLabel.text = "Let's sign you in"
41 |
42 | HeadLabel.anchor(top: view.topAnchor,
43 | leading: view.leadingAnchor,
44 | padding: .init(top: 80, leading: 20))
45 | }
46 |
47 | private func configureTextField() {
48 | emailTextField.anchor(top: HeadLabel.bottomAnchor,
49 | leading: view.leadingAnchor,
50 | trailing: view.trailingAnchor,
51 | padding: .init(top: 40, leading: 20),
52 | size: .init(width: 0, height: 50))
53 |
54 | passwordTextField.anchor(top: emailTextField.bottomAnchor,
55 | leading: view.leadingAnchor,
56 | trailing: view.trailingAnchor,
57 | padding: .init(top: 20, leading: 20, trailing: 20),
58 | size: .init(heightSize: 50))
59 | }
60 |
61 | private func configureForgotPassword(){
62 | forgotPasswordButton.tintColor = .systemPurple
63 |
64 | forgotPasswordButton.anchor(top: passwordTextField.bottomAnchor,
65 | trailing: passwordTextField.trailingAnchor,
66 | padding: .init(top: 10))
67 |
68 | forgotPasswordButton.addTarget(self, action: #selector(didTapForgotPassword), for: .touchUpInside)
69 | }
70 |
71 | private func configureSignIn(){
72 | signInButton.configuration?.cornerStyle = .capsule
73 |
74 | signInButton.anchor(top: forgotPasswordButton.bottomAnchor,
75 | leading: view.leadingAnchor,
76 | trailing: view.trailingAnchor,
77 | padding: .init(top: 20, leading: 20, trailing: 20),
78 | size: .init(heightSize: 50))
79 |
80 | signInButton.addTarget(self, action: #selector(didTapSignIn), for: .touchUpInside)
81 | }
82 |
83 |
84 |
85 | private func configureStackView() {
86 | stackView.axis = .horizontal
87 |
88 | stackView.addArrangedSubview(infoLabel)
89 | stackView.addArrangedSubview(newUserButton)
90 |
91 | infoLabel.text = "Don't have an account?"
92 |
93 |
94 | stackView.anchor(top: signInButton.bottomAnchor,
95 | padding: .init(top: 5))
96 |
97 | stackView.centerXInSuperview()
98 |
99 | newUserButton.addTarget(self, action: #selector(didTapNewUser), for: .touchUpInside)
100 | }
101 |
102 | @objc private func didTapSignIn() {
103 | //Email & Password Validation
104 |
105 | guard let email = emailTextField.text,
106 | let password = passwordTextField.text else{
107 | presentAlert(title: "Alert!", message: "Email and Password ?", buttonTitle: "Ok")
108 | return
109 | }
110 | guard email.isValidEmail(email: email) else {
111 | presentAlert(title: "Alert!", message: "Email Invalid", buttonTitle: "Ok")
112 | return
113 | }
114 |
115 | guard password.isValidPassword(password: password) else {
116 |
117 | guard password.count >= 6 else {
118 | presentAlert(title: "Alert!", message: "Password must be at least 6 characters", buttonTitle: "Ok")
119 | return
120 | }
121 |
122 | guard password.containsDigits(password) else {
123 | presentAlert(title: "Alert!", message: "Password must contain at least 1 digit", buttonTitle: "Ok")
124 | return
125 | }
126 |
127 | guard password.containsLowerCase(password) else {
128 | presentAlert(title: "Alert!", message: "Password must contain at least 1 lowercase character", buttonTitle: "Ok")
129 | return
130 | }
131 |
132 | guard password.containsUpperCase(password) else {
133 | presentAlert(title: "Alert!", message: "Password must contain at least 1 uppercase character", buttonTitle: "Ok")
134 | return
135 | }
136 |
137 | return
138 | }
139 |
140 | authVM?.login(email: email, password: password) { [weak self] success, error in
141 | guard let self = self else { return }
142 |
143 | if success {
144 | self.presentAlert(title: "Alert!", message: "Entry Successful 🥳", buttonTitle: "Ok")
145 | self.dismiss(animated: true) {
146 |
147 | let tabBar = MainTabBarViewController()
148 | tabBar.modalPresentationStyle = .fullScreen
149 | self.present(tabBar, animated: true, completion: nil)
150 | }
151 | } else {
152 | self.presentAlert(title: "Alert!", message: error, buttonTitle: "Ok")
153 | }
154 | }
155 | }
156 |
157 | // MARK: - ACTİON
158 | @objc private func didTapNewUser() {
159 | let vc = RegisterVC()
160 | self.navigationController?.pushViewController(vc, animated: true)
161 | }
162 |
163 | @objc private func didTapForgotPassword() {
164 | let vc = ForgotPasswordVC()
165 | self.navigationController?.pushViewController(vc, animated: true)
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Detail/MoviePreviewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TitlePreviewViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 | import WebKit
11 |
12 | final class MoviePreviewViewController: UIViewController {
13 |
14 | //MARK: - Variables
15 | private var movies: Movie? = nil
16 | private lazy var vm: DetailVM? = DetailVM()
17 | private lazy var isFavorited = false
18 |
19 | // MARK: - UI Elements
20 | private let titleLabel: UILabel = {
21 | let label = UILabel()
22 | label.font = .systemFont(ofSize: 22, weight: .bold)
23 | label.text = "Harry potter"
24 | return label
25 | }()
26 |
27 | private let overviewLabel: UILabel = {
28 | let label = UILabel()
29 | label.font = .systemFont(ofSize: 18, weight: .regular)
30 | label.numberOfLines = 0
31 | label.text = "This is the best movie ever to watch as a kid!"
32 | return label
33 | }()
34 |
35 | private lazy var downloadButton = MovieButton(bgColor: .red,
36 | color: .red,
37 | title: "Download",
38 | systemImageName: "arrow.down.circle.dotted",
39 | cornerStyle: .small)
40 |
41 | private lazy var webView: WKWebView = {
42 | let webView = WKWebView()
43 | return webView
44 | }()
45 |
46 | private let scrollView: UIScrollView = {
47 | let scrollView = UIScrollView()
48 | scrollView.isScrollEnabled = true
49 | scrollView.showsVerticalScrollIndicator = true
50 | scrollView.showsHorizontalScrollIndicator = false
51 | return scrollView
52 | }()
53 |
54 | private let stackView: UIStackView = {
55 | let stackView = UIStackView()
56 | stackView.axis = .vertical
57 | stackView.spacing = 25
58 | return stackView
59 | }()
60 |
61 | private lazy var DateStackView: UIStackView = {
62 | let stackView = UIStackView()
63 | stackView.axis = .horizontal
64 | stackView.spacing = 8
65 | stackView.alignment = .center
66 | return stackView
67 | }()
68 |
69 | lazy var movieReleaseDate: UILabel = {
70 | let label = UILabel()
71 | label.text = "2023-09-13"
72 | label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
73 | label.textColor = .secondaryLabel
74 | return label
75 | }()
76 |
77 | private lazy var ReleaseDateImage: UIImageView = {
78 | let image = UIImageView()
79 | image.image = UIImage(systemName: "calendar.badge.clock")
80 | image.contentMode = .scaleAspectFit
81 | image.clipsToBounds = true
82 | image.tintColor = .label
83 | return image
84 | }()
85 |
86 | // MARK: - View Lifecycle
87 | override func viewDidLoad() {
88 | super.viewDidLoad()
89 | view.backgroundColor = .systemBackground
90 | view.addSubviewsExt(webView, scrollView)
91 | scrollView.addSubview(stackView)
92 | stackView.addArrangedSubviewsExt(titleLabel, DateStackView)
93 |
94 | DateStackView.addArrangedSubviewsExt(ReleaseDateImage, movieReleaseDate)
95 | stackView.addArrangedSubviewsExt(overviewLabel, downloadButton)
96 |
97 | configureConstraints()
98 | configureDownloadButton()
99 |
100 | }
101 |
102 | // MARK: - Constraints
103 | private func configureConstraints() {
104 | webView.anchor(top: view.safeAreaLayoutGuide.topAnchor,
105 | leading: view.leadingAnchor,
106 | trailing: view.trailingAnchor,
107 | size: .init(heightSize: 300))
108 |
109 | scrollView.anchor(top: webView.bottomAnchor,
110 | leading: view.leadingAnchor,
111 | bottom: view.safeAreaLayoutGuide.bottomAnchor,
112 | trailing: view.trailingAnchor,
113 | padding: .init(top: 10))
114 |
115 | stackView.anchor(top: scrollView.topAnchor,
116 | leading: scrollView.leadingAnchor,
117 | bottom: scrollView.bottomAnchor,
118 | trailing: scrollView.trailingAnchor)
119 |
120 | stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
121 |
122 | titleLabel.anchor(top: stackView.topAnchor,
123 | leading: stackView.leadingAnchor,
124 | trailing: stackView.trailingAnchor,
125 | padding: .init(top: 20, leading: 20))
126 |
127 | DateStackView.anchor(top: titleLabel.bottomAnchor,
128 | leading: stackView.leadingAnchor,
129 | trailing: stackView.trailingAnchor,
130 | padding: .init(top: 10, leading: 20))
131 |
132 | ReleaseDateImage.anchor(size: .init(width: 20, height: 20))
133 |
134 |
135 | overviewLabel.anchor(top: DateStackView.bottomAnchor,
136 | leading: stackView.leadingAnchor,
137 | trailing: stackView.trailingAnchor,
138 | padding: .init(top: 25, leading: 20, trailing: 20))
139 |
140 | downloadButton.anchor(top: overviewLabel.bottomAnchor,
141 | leading: stackView.leadingAnchor,
142 | bottom: stackView.bottomAnchor,
143 | trailing: stackView.trailingAnchor,
144 | padding: .init(top: 25, left: 20, bottom: 20, right: 20),
145 | size: .init(heightSize: 50))
146 | downloadButton.addTarget(self, action: #selector(downloadButtonTapped), for: .touchUpInside)
147 | }
148 | private func configureDownloadButton() {
149 | vm!.isFavorited(movies: movies!) { bool in
150 | self.isFavorited = bool
151 | self.downloadButton.setImage(UIImage(systemName: bool ? "arrow.down.circle.fill" : "rrow.down.circle.dotted"), for: .normal)
152 | }
153 | }
154 |
155 | //MARK: - @Actions
156 | @objc private func downloadButtonTapped() {
157 | if isFavorited {
158 | vm!.removeFromFavorites(movies: movies!) { bool in
159 | self.isFavorited = bool
160 | self.downloadButton.setImage(UIImage(systemName: "arrow.down.circle.dotted"), for: .normal)
161 | }
162 | } else {
163 | vm!.addToFavorites(movies: movies!) { bool in
164 | self.isFavorited = bool
165 | self.downloadButton.setImage(UIImage(systemName: "arrow.down.circle.fill"), for: .normal)
166 | }
167 | }
168 | }
169 |
170 | // MARK: - Public Methods
171 | public func configure(with model: MoviePreviewModel, moviModelIsFavori: Movie) {
172 | self.movies = moviModelIsFavori
173 |
174 | titleLabel.text = model.title
175 | overviewLabel.text = model.movieOverview
176 | movieReleaseDate.text = model.release_date
177 |
178 | guard let url = URL(string: "https://www.youtube.com/embed/\(model.youtubeView.id.videoId)") else {
179 | return
180 | }
181 |
182 | webView.load(URLRequest(url: url))
183 | }
184 |
185 | }
186 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | // MARK: - HomeViewInterface
11 | protocol HomeViewInterface: AnyObject {
12 | func configureHeaderView(with moviePath: [Movie])
13 | func showLoadingIndicator()
14 | func dismissLoadingIndicator()
15 | func tableViewReloadData()
16 | func configureViewDidLoad()
17 | func pushVC(_ vc: UIViewController)
18 | func alert(title: String, message: String, buttonTitle: String)
19 | }
20 |
21 | final class HomeViewController: UIViewController{
22 |
23 | // MARK: - Properties
24 | private lazy var viewModel = HomeVM(view: self)
25 |
26 | // MARK: - Header View
27 | private var headerView: HeroHeaderUIView?
28 |
29 | // MARK: - Section Titles
30 | private let sectionTitles: [String] = ["Trending Movies", "Trending Tv", "Popular", "Upcoming Movies", "Top rated"]
31 |
32 | // MARK: - TableView
33 | private let homeFeedTable: UITableView = {
34 | let table = UITableView(frame: .zero, style: .grouped)
35 | table.separatorStyle = .none
36 | table.register(CollectionViewTableViewCell.self, forCellReuseIdentifier: CollectionViewTableViewCell.identifier)
37 | return table
38 | }()
39 |
40 | // MARK: - ViewDidLoad
41 | override func viewDidLoad() {
42 | super.viewDidLoad()
43 | viewModel.viewDidLoad()
44 | }
45 |
46 | // MARK: - LayoutSubviews
47 | override func viewDidLayoutSubviews() {
48 | super.viewDidLayoutSubviews()
49 | homeFeedTable.frame = view.frame
50 | // Güvenli alanın altındaki boşluğu hesaplayın
51 | let safeAreaBottom = view.safeAreaInsets.bottom
52 | //SafeAreaKadar Boşluk bıraktık
53 | homeFeedTable.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: safeAreaBottom, right: 0)
54 |
55 | }
56 |
57 | // MARK: - UI Configuration
58 | private func configureUI() {
59 | navigationController?.navigationBar.topItem?.backButtonTitle = "Home"
60 | navigationController?.navigationBar.tintColor = MovieColor.playButonBG
61 | }
62 |
63 | // MARK: - Configure TableView
64 | private func configureTableView() {
65 | configureHeaderView()
66 | view.addSubview(homeFeedTable)
67 |
68 | homeFeedTable.delegate = self
69 | homeFeedTable.dataSource = self
70 | homeFeedTable.tableHeaderView = headerView
71 | homeFeedTable.backgroundColor = .tertiarySystemGroupedBackground
72 | homeFeedTable.contentInsetAdjustmentBehavior = .never
73 | }
74 |
75 | // MARK: - Configure HeaderView
76 | private func configureHeaderView() {
77 | headerView = HeroHeaderUIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 500))
78 | headerView?.delegate = self
79 | }
80 | }
81 |
82 | // MARK: - UITableViewDelegate and UITableViewDataSource
83 | extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
84 |
85 | func numberOfSections(in tableView: UITableView) -> Int {
86 | return sectionTitles.count
87 | }
88 |
89 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
90 | return 1
91 | }
92 |
93 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
94 |
95 | guard let cell = tableView.dequeueReusableCell(withIdentifier: CollectionViewTableViewCell.identifier, for: indexPath) as? CollectionViewTableViewCell else {
96 | return UITableViewCell()
97 | }
98 | cell.delegate = self
99 |
100 | switch indexPath.section {
101 | case Sections.TrendingMovies.rawValue:
102 | cell.configure(with: viewModel.trendingMovies)
103 |
104 | case Sections.TrendingTv.rawValue:
105 | cell.configure(with: viewModel.TrendingTVs)
106 |
107 | case Sections.Popular.rawValue:
108 | cell.configure(with: viewModel.UpcomingMovies)
109 |
110 | case Sections.Upcoming.rawValue:
111 | cell.configure(with: viewModel.Popular)
112 |
113 | case Sections.TopRated.rawValue:
114 | cell.configure(with: viewModel.TopRated)
115 | default:
116 | return UITableViewCell()
117 | }
118 | return cell
119 | }
120 |
121 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
122 | return 200
123 | }
124 |
125 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
126 | return 40
127 | }
128 |
129 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
130 | guard let header = view as? UITableViewHeaderFooterView else {return}
131 | header.textLabel?.font = .systemFont(ofSize: 20, weight: .semibold)
132 | header.textLabel?.frame = CGRect(x: header.bounds.origin.x + 20, y: header.bounds.origin.y, width: 100, height: header.bounds.height)
133 | header.textLabel?.textColor = .label
134 | header.textLabel?.text = header.textLabel?.text?.capitalizeFirstLetter()
135 | }
136 |
137 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
138 | return sectionTitles[section]
139 | }
140 | }
141 |
142 | // MARK: - HomeViewInterface
143 | extension HomeViewController: HomeViewInterface {
144 | func configureViewDidLoad() {
145 | configureUI()
146 | configureTableView()
147 | }
148 |
149 | func tableViewReloadData() {
150 | DispatchQueue.main.async {
151 | self.homeFeedTable.reloadData()
152 | }
153 | }
154 |
155 | //Random Image
156 | func configureHeaderView(with moviePath: [Movie]) {
157 | let selectedTitle = moviePath.randomElement()
158 | headerView?.configure(with: selectedTitle!)
159 | }
160 |
161 | // Displays the loading indicator.
162 | func showLoadingIndicator() {
163 | DispatchQueue.main.async {
164 | self.showLoading()
165 | }
166 | }
167 | // Dismisses the loading indicator.
168 | func dismissLoadingIndicator() {
169 |
170 | DispatchQueue.main.async {
171 | self.dismissLoading()
172 | }
173 | }
174 |
175 | func alert(title: String, message: String, buttonTitle: String) {
176 | presentAlert(title: title, message: message, buttonTitle: buttonTitle)
177 | }
178 |
179 | func pushVC(_ vc: UIViewController) {
180 | DispatchQueue.main.async{
181 | self.navigationController?.pushViewController(vc, animated: true)
182 | }
183 | }
184 | }
185 |
186 | // MARK: - CollectionView DidSelect
187 | extension HomeViewController: CollectionViewTableViewCellDelegate {
188 | func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel: MoviePreviewModel, movieModel: Movie) {
189 | DispatchQueue.main.async { [weak self] in
190 | let vc = MoviePreviewViewController()
191 | vc.configure(with: viewModel,moviModelIsFavori: movieModel)
192 | self?.navigationController?.pushViewController(vc, animated: true)
193 | }
194 | }
195 | }
196 | // MARK: - HeroHeaderUIViewProtocol
197 | extension HomeViewController: HeroHeaderUIViewProtocol {
198 | func showDetail(movie: Movie) {
199 | viewModel.showDetail(movie: movie)
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Profile/ProfileViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewController.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 30.10.2023.
6 | //
7 |
8 |
9 | import UIKit
10 | import FirebaseAuth
11 | import SDWebImage
12 |
13 | protocol ProfileVCInterface{
14 | func configureViewDidLoad()
15 | }
16 |
17 | final class ProfileViewController : UIViewController, UIImagePickerControllerDelegate , UINavigationControllerDelegate {
18 | // MARK: - Properties
19 | private lazy var viewModel = ProfileVM(view: self)
20 |
21 | private lazy var tableView: UITableView = {
22 | let table = UITableView(frame: .zero, style: .grouped)
23 | table.register(SettingTableViewCell.self, forCellReuseIdentifier: SettingTableViewCell.identifier)
24 | table.register(SwitchTableViewCell.self, forCellReuseIdentifier: SwitchTableViewCell.identifier)
25 | return table
26 | }()
27 |
28 | // MARK: - Header View
29 | private var headerView: ProfileUIView?
30 |
31 | // MARK: - View Controller Lifecycle
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | viewModel.viewDidLoad()
35 | }
36 |
37 | // MARK: - Configure HeaderView
38 | private func configureHeaderView() {
39 | headerView = ProfileUIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 180))
40 |
41 | //image tıklana bilir hale getirdik
42 | headerView?.userImage.isUserInteractionEnabled = true
43 | let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(chooeseImage))
44 | headerView?.userImage.addGestureRecognizer(gestureRecognizer)
45 |
46 | viewModel.fetchUserPhoto { url in
47 | guard let url = URL(string: url) else {
48 | return
49 | }
50 | self.headerView?.userImage.sd_setImage(with: url, completed: nil)
51 | }
52 |
53 | viewModel.fetchUserName { userName in
54 | self.headerView?.userName.text = userName
55 |
56 | //Flitered userName
57 | let nameSurname = userName.components(separatedBy: " ")
58 | if nameSurname.count >= 2 {
59 | let userNamex = nameSurname[0]
60 | self.headerView?.userMesage.text = "Tekrardan Hoşgeldin \(userNamex) 🎉"
61 | } else {
62 | print("Tekrardan Hoşgeldin 🎉")
63 | }
64 | }
65 | }
66 |
67 | // MARK: - Configure UI
68 | private func configureUI(){
69 | setupTableView()
70 | configureTableViewCell()
71 | }
72 |
73 | private func setupTableView() {
74 | view.addSubview(tableView)
75 | tableView.delegate = self
76 | tableView.dataSource = self
77 | tableView.frame = view.frame
78 | tableView.backgroundColor = .systemBackground
79 | tableView.separatorStyle = .none
80 | }
81 |
82 | let isDarkModeOn = UserDefaults.standard.bool(forKey: "DarkMode")
83 |
84 | private func configureTableViewCell() {
85 | viewModel.models.append(Section(title: "", options: [
86 | .switchCell(model: SettingsSwitchOption(title: "Dark Mode", icon: UIImage(systemName: "moon.stars"), iconBackgrondColor: MovieColor.goldColor, handler: {
87 |
88 | }, isOn: isDarkModeOn)),
89 |
90 | .staticCell(model: SettingsOption(title: "Change Password", icon: UIImage(systemName: "exclamationmark.lock.fill"), iconBackgrondColor: MovieColor.goldColor, handler: {
91 | let vc = ChangePasswordVC()
92 | self.navigationController?.pushViewController(vc, animated: true)
93 | })),
94 | .staticCell(model: SettingsOption(title: "Help and Support", icon: UIImage(systemName: "questionmark.circle"), iconBackgrondColor: MovieColor.goldColor, handler: {
95 | let vc = HelpAndSupportVC()
96 | self.navigationController?.pushViewController(vc, animated: true)
97 | })),
98 | .staticCell(model: SettingsOption(title: "Log out", icon: UIImage(systemName: "rectangle.portrait.and.arrow.forward"), iconBackgrondColor: MovieColor.goldColor, handler: {
99 | do {
100 | try Auth.auth().signOut()
101 | let loginVC = LoginVC()
102 | let nav = UINavigationController(rootViewController: loginVC)
103 | nav.modalPresentationStyle = .fullScreen
104 | self.present(nav, animated: true, completion: nil)
105 |
106 | } catch {
107 | print(error.localizedDescription )
108 | }
109 | })),
110 | ]))
111 | }
112 |
113 | // MARK: - Action
114 | @objc private func chooeseImage() {
115 | let pickerController = UIImagePickerController()
116 | pickerController.delegate = self
117 | pickerController.sourceType = .photoLibrary
118 | present(pickerController, animated: true)
119 | }
120 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
121 | headerView?.userImage.image = info[.originalImage] as? UIImage
122 | viewModel.uploadUserPhoto(imageData: (headerView?.userImage.image!)!)
123 | self.dismiss(animated: true)
124 | }
125 | }
126 |
127 | // MARK: - Table View Data Source
128 | extension ProfileViewController: UITableViewDataSource{
129 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
130 | return viewModel.models[section].options.count
131 | }
132 |
133 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
134 |
135 | let model = viewModel.models[indexPath.section].options[indexPath.row]
136 |
137 | switch model.self {
138 | case .staticCell(let model):
139 | guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingTableViewCell.identifier, for: indexPath) as? SettingTableViewCell else {
140 | return UITableViewCell()
141 | }
142 | cell.configure(with: model)
143 | return cell
144 |
145 | case .switchCell(let model):
146 |
147 | guard let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.identifier, for: indexPath) as? SwitchTableViewCell else {
148 | return UITableViewCell()
149 | }
150 | cell.configure(with: model)
151 | return cell
152 | }
153 | }
154 | }
155 |
156 | // MARK: - Table View Delegate
157 | extension ProfileViewController: UITableViewDelegate{
158 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
159 | tableView.deselectRow(at: indexPath, animated: true)
160 | let model = viewModel.models[indexPath.section].options[indexPath.row]
161 |
162 | switch model.self {
163 | case .staticCell(let model):
164 | model.handler()
165 |
166 | case .switchCell(let model):
167 | model.handler()
168 | }
169 | }
170 |
171 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
172 | return 60.0
173 | }
174 | }
175 |
176 | extension ProfileViewController: ProfileVCInterface {
177 | func configureViewDidLoad() {
178 | configureUI()
179 | configureHeaderView()
180 | tableView.tableHeaderView = headerView
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Movie-App/Movie-App/Controllers/Auth/RegisterVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegisterVC.swift
3 | // Movie-App
4 | //
5 | // Created by Yaşar Duman on 18.10.2023.
6 | //
7 |
8 | import UIKit
9 | import FirebaseAuth
10 |
11 | final class RegisterVC: UIViewController {
12 | // MARK: - Properties
13 | private let HeadLabel = TitleLabel(textAlignment: .left, fontSize: 20)
14 | private lazy var userNameTextField = CustomTextField(fieldType: .username)
15 | private lazy var emailTextField = CustomTextField(fieldType: .email)
16 | private lazy var passwordTextField = CustomTextField(fieldType: .password)
17 | private lazy var repasswordTextField = CustomTextField(fieldType: .password)
18 | private lazy var signUpButton = MovieButton( bgColor:MovieColor.playButonBG ,color: MovieColor.playButonBG, title: "Sign Up", fontSize: .big)
19 | private let infoLabel = SecondaryTitleLabel(fontSize: 16)
20 | private lazy var signInButton = MovieButton( bgColor:.clear ,color: .label, title: "Sign In.", fontSize: .small)
21 |
22 | private lazy var stackView = UIStackView()
23 | private let authVM : AuthVM? = AuthVM()
24 |
25 | // MARK: - View Controller Lifecycle
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 | configureViewController()
29 | configureHeadLabel()
30 | configureTextField()
31 | configureSignUp()
32 | configureStackView()
33 | }
34 |
35 | // MARK: - UI Configuration
36 | func configureViewController() {
37 | view.backgroundColor = .systemBackground
38 | self.navigationItem.setHidesBackButton(true, animated: true)
39 | view.addSubviewsExt(HeadLabel, userNameTextField, emailTextField, passwordTextField, repasswordTextField, signUpButton, signInButton, stackView)
40 | }
41 |
42 | private func configureHeadLabel() {
43 | HeadLabel.text = "Create an account"
44 |
45 | HeadLabel.anchor(top: view.topAnchor,
46 | leading: view.leadingAnchor,
47 | padding: .init(top: 80, leading: 20))
48 |
49 | }
50 |
51 | private func configureTextField() {
52 | userNameTextField.anchor(top: HeadLabel.bottomAnchor,
53 | leading: view.leadingAnchor,
54 | trailing: view.trailingAnchor,
55 | padding: .init(top: 40, leading: 20,trailing: 20),
56 | size: .init(heightSize: 50))
57 |
58 |
59 | emailTextField.anchor(top: userNameTextField.bottomAnchor,
60 | leading: view.leadingAnchor,
61 | trailing: view.trailingAnchor,
62 | padding: .init(top: 20, leading: 20, trailing: 20),
63 | size: .init(heightSize: 50))
64 |
65 |
66 |
67 | passwordTextField.anchor(top: emailTextField.bottomAnchor,
68 | leading: view.leadingAnchor,
69 | trailing: view.trailingAnchor,
70 | padding: .init(top: 20, leading: 20, trailing: 20),
71 | size: .init(heightSize: 50))
72 |
73 |
74 | repasswordTextField.placeholder = "Repassword"
75 |
76 | repasswordTextField.anchor(top: passwordTextField.bottomAnchor,
77 | leading: view.leadingAnchor,
78 | trailing: view.trailingAnchor,
79 | padding: .init(top: 20, leading: 20, trailing: 20),
80 | size: .init(heightSize: 50))
81 |
82 |
83 | }
84 |
85 | private func configureSignUp(){
86 | signUpButton.configuration?.cornerStyle = .capsule
87 |
88 | signUpButton.anchor(top: repasswordTextField.bottomAnchor,
89 | leading: view.leadingAnchor,
90 | trailing: view.trailingAnchor,
91 | padding: .init(top: 20, leading: 20, trailing: 20),
92 | size: .init(heightSize: 50))
93 |
94 | signUpButton.addTarget(self, action: #selector(didTapSignUp), for: .touchUpInside)
95 | }
96 |
97 | private func configureStackView() {
98 | stackView.axis = .horizontal
99 |
100 | stackView.addArrangedSubview(infoLabel)
101 | stackView.addArrangedSubview(signInButton)
102 |
103 | infoLabel.text = "Already have an account?"
104 |
105 | stackView.anchor(top: signUpButton.bottomAnchor,
106 | padding: .init(top: 5, left: 0, bottom: 0, right: 0))
107 |
108 | stackView.centerXInSuperview()
109 |
110 | signInButton.addTarget(self, action: #selector(didTapSignIn), for: .touchUpInside)
111 | }
112 |
113 | // MARK: - Action
114 | @objc private func didTapSignUp() {
115 |
116 | //Email & Password Validation
117 | guard let userName = userNameTextField.text,
118 | let email = emailTextField.text,
119 | let password = passwordTextField.text,
120 | let rePassword = repasswordTextField.text else{
121 | presentAlert(title: "Alert!", message: "Username, email, password, rePassword ?", buttonTitle: "Ok")
122 | return
123 | }
124 | guard email.isValidEmail(email: email) else {
125 | presentAlert(title: "Alert!", message: "Email Invalid", buttonTitle: "Ok")
126 | return
127 | }
128 |
129 | guard password.isValidPassword(password: password) else {
130 |
131 | guard password.count >= 6 else {
132 | presentAlert(title: "Alert!", message: "Password must be at least 6 characters", buttonTitle: "Ok")
133 | return
134 | }
135 |
136 | guard password.containsDigits(password) else {
137 | presentAlert(title: "Alert!", message: "Password must contain at least 1 digit", buttonTitle: "Ok")
138 | return
139 | }
140 |
141 | guard password.containsLowerCase(password) else {
142 | presentAlert(title: "Alert!", message: "Password must contain at least 1 lowercase character", buttonTitle: "Ok")
143 | return
144 | }
145 |
146 | guard password.containsUpperCase(password) else {
147 | presentAlert(title: "Alert!", message: "Password must contain at least 1 uppercase character", buttonTitle: "Ok")
148 | return
149 | }
150 |
151 | guard password == rePassword else {
152 | presentAlert(title: "Alert!", message: "Password and password repeat are not the same", buttonTitle: "Ok")
153 | return
154 | }
155 |
156 | return
157 | }
158 |
159 | authVM?.register(userName: userName, email: email, password: password) { [weak self] success, error in
160 | guard let self = self else { return }
161 |
162 | if success {
163 | self.presentAlert(title: "Alert!", message: "Registration Successful 🥳", buttonTitle: "Ok")
164 | self.dismiss(animated: true) {
165 |
166 | let tabBar = MainTabBarViewController()
167 | tabBar.modalPresentationStyle = .fullScreen
168 | self.present(tabBar, animated: true, completion: nil)
169 | }
170 | } else {
171 | self.presentAlert(title: "Alert!", message: error, buttonTitle: "Ok")
172 | }
173 | }
174 | }
175 |
176 | @objc private func didTapSignIn() {
177 |
178 | self.navigationController?.popToRootViewController(animated: true)
179 | }
180 | }
181 |
--------------------------------------------------------------------------------