├── 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 | | OnBoarding | LoginAndRegister | LoginAndRegister | ForgotPassword | 37 | 38 | | Home | Movie Detail | Search | Search Result | 39 | | --- | --- | --- | --- | 40 | | HomePage | MovieDetail | SearchPage | ForgotPassword | 41 | 42 | | Download | User Profile | Reset Password | Help and Support | 43 | | --- | --- | --- | --- | 44 | | DownloadPage | UserProfile | ResetPassword | HelpAndSupport | 45 | 46 | 47 | 48 | ## Video Preview 🎥 49 |
50 |
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 | --------------------------------------------------------------------------------