Compare commits
15 Commits
master
...
developmen
Author | SHA1 | Date |
---|---|---|
![]() |
17389aa2e0 | |
![]() |
a52420bb16 | |
![]() |
bd980bb062 | |
![]() |
f3cfa9a622 | |
![]() |
f53d097d67 | |
![]() |
8054c4c707 | |
![]() |
dced6087d7 | |
![]() |
479d89a47c | |
![]() |
c4b072dcf0 | |
![]() |
93fcb4f3a2 | |
![]() |
54aee8775c | |
![]() |
439a7e2bde | |
![]() |
6dc64129f2 | |
![]() |
a8ec22590f | |
![]() |
635b637ef9 |
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Array+Extension.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 1.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array {
|
||||
|
||||
struct IndexOutOfBoundsError: Error { }
|
||||
|
||||
func element(at index: Int) throws -> Element {
|
||||
guard index >= 0 && index < self.count else {
|
||||
throw IndexOutOfBoundsError()
|
||||
}
|
||||
return self[index]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Optional+Extension.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 1.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Optional {
|
||||
|
||||
struct FoundNilWhileUnwrappingError: Error { }
|
||||
|
||||
func unwrap() throws -> Wrapped {
|
||||
switch self {
|
||||
case .some(let wrapped):
|
||||
return wrapped
|
||||
case .none:
|
||||
throw FoundNilWhileUnwrappingError()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// UICollectionView+Extension.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 18.12.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UICollectionView {
|
||||
func registerCell<T: UICollectionViewCell>(type: T.Type) where T: Reuseable {
|
||||
self.register(UINib(nibName: T.reuseIdentifier, bundle: nil), forCellWithReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
func dequeueReusableCell<T: UICollectionViewCell> (forIndexPath indexPath: IndexPath) -> T where T: Reuseable {
|
||||
return dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath as IndexPath) as! T
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// UIImageView+Extension.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
// MARK: - Kingfiher
|
||||
extension UIImageView {
|
||||
|
||||
func setKfImage(for urlString: String?) {
|
||||
guard let urlString = urlString else {
|
||||
return
|
||||
}
|
||||
let url = URL(string: urlString)
|
||||
self.kf.indicatorType = .activity
|
||||
self.kf.setImage(with: url, options: KingfisherManager.shared.defaultOptions)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// UITableView+Extension.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 18.12.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UITableView {
|
||||
func registerCell<T: UITableViewCell>(type: T.Type) where T: Reuseable {
|
||||
self.register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
func dequeueReusableCell<T: UITableViewCell> (forIndexPath indexPath: IndexPath) -> T where T: Reuseable {
|
||||
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath as IndexPath) as! T
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// UIViewController+Extension.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 18.12.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
func embedInNavigationController(
|
||||
_ modalPresentationStyle: UIModalPresentationStyle = .fullScreen
|
||||
) -> UINavigationController {
|
||||
let navigationController = UINavigationController(rootViewController: self)
|
||||
navigationController.modalPresentationStyle = modalPresentationStyle
|
||||
return navigationController
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// MoviePresentation.swift
|
||||
// MovieBoxMVC
|
||||
//
|
||||
// Created by Ilter Cengiz on 18/11/18.
|
||||
// Copyright © 2018 Late Night Muhabbetleri. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import iMoviesAPI
|
||||
|
||||
typealias MoviePresentations = [MoviePresentation]
|
||||
|
||||
struct MoviePresentation: Hashable {
|
||||
let title: String?
|
||||
let summary: String?
|
||||
let imageUrl: String?
|
||||
}
|
||||
|
||||
|
||||
extension Movie {
|
||||
func map() -> MoviePresentation {
|
||||
return MoviePresentation(title: self.displayTitle,
|
||||
summary: self.summaryShort,
|
||||
imageUrl: self.multimedia?.src)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Reuseable.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 18.12.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol Reuseable: AnyObject {
|
||||
static var reuseIdentifier: String { get }
|
||||
}
|
||||
|
||||
extension Reuseable {
|
||||
static var reuseIdentifier: String {
|
||||
return String(describing: self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Screen.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 7.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct ScreenSize {
|
||||
|
||||
static let bounds = UIScreen.main.bounds
|
||||
|
||||
static let width: CGFloat = UIScreen.main.bounds.width
|
||||
|
||||
static let height: CGFloat = UIScreen.main.bounds.height
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// ThemeManager.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 18.12.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
public enum Theme: String {
|
||||
case light, dark // , graphical
|
||||
|
||||
var barStyle: UIBarStyle {
|
||||
.default
|
||||
}
|
||||
}
|
||||
|
||||
public enum AppFont: String {
|
||||
/// Avenir-Book
|
||||
case Book = "Avenir-Book"
|
||||
/// Avenir-Roman
|
||||
case Roman = "Avenir-Roman"
|
||||
/// Avenir-Light
|
||||
case Light = "Avenir-Light"
|
||||
/// Avenir-Medium
|
||||
case Medium = "Avenir-Medium"
|
||||
/// Avenir-Heavy
|
||||
case Heavy = "Avenir-Heavy"
|
||||
/// Avenir-Black
|
||||
case Black = "Avenir-Black"
|
||||
|
||||
public enum Size: CGFloat {
|
||||
/// 10pt
|
||||
case xxsmall = 10
|
||||
/// 12.4pt
|
||||
case xsmall = 12.4
|
||||
/// 13pt
|
||||
case small = 13
|
||||
/// 14pt
|
||||
case smallmedium = 14
|
||||
/// 15pt
|
||||
case medium = 15
|
||||
/// 18pt
|
||||
case mediumlarge = 18
|
||||
///20pt
|
||||
case large = 20
|
||||
/// 22pt
|
||||
case larger = 22
|
||||
/// 24pt
|
||||
case xlarge = 24
|
||||
/// 28pt
|
||||
case xxlarge = 28
|
||||
}
|
||||
|
||||
/// size as predifined size
|
||||
public func font(size: Size) -> UIFont {
|
||||
let fontSize = size.rawValue
|
||||
let fontName = self.rawValue
|
||||
return UIFont(name: fontName, size: fontSize) ?? UIFont()
|
||||
}
|
||||
|
||||
/// size as CGFloat
|
||||
public func font(size: CGFloat) -> UIFont {
|
||||
let fontSize = size
|
||||
let fontName = self.rawValue
|
||||
return UIFont(name: fontName, size: fontSize) ?? UIFont()
|
||||
}
|
||||
|
||||
/// size as predifined size
|
||||
public func font(size: Size) -> Font {
|
||||
let fontSize = size.rawValue
|
||||
let fontName = self.rawValue
|
||||
let uiFont: UIFont = UIFont(name: fontName, size: fontSize) ?? UIFont()
|
||||
return Font(uiFont)
|
||||
}
|
||||
|
||||
/// size as CGFloat
|
||||
public func font(size: CGFloat) -> Font {
|
||||
let fontSize = size
|
||||
let fontName = self.rawValue
|
||||
let uiFont: UIFont = UIFont(name: fontName, size: fontSize) ?? UIFont()
|
||||
return Font(uiFont)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
|
@ -7,33 +7,33 @@
|
|||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Splash Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="SplashController" id="BYZ-38-t0r" customClass="SplashController" customModule="iMoviesMVC" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="applelogo" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="ge7-OE-eno">
|
||||
<rect key="frame" x="40" y="403.5" width="334" height="88.5"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="applelogo" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="cWH-AK-2bM">
|
||||
<rect key="frame" x="157" y="398.5" width="100" height="98.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="90" id="XE6-sM-vQZ"/>
|
||||
<constraint firstAttribute="height" constant="100" id="c4m-pu-aDN"/>
|
||||
<constraint firstAttribute="width" constant="100" id="mkQ-bz-G7M"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" white="0.95615433673469385" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="ge7-OE-eno" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="40" id="8in-U5-78v"/>
|
||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="ge7-OE-eno" secondAttribute="trailing" constant="40" id="aaX-mF-hX0"/>
|
||||
<constraint firstItem="ge7-OE-eno" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="lZp-th-Wgi"/>
|
||||
<constraint firstItem="cWH-AK-2bM" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="Map-pc-UHU"/>
|
||||
<constraint firstItem="cWH-AK-2bM" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="puy-o2-8Yu"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3" y="54"/>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
|
|
@ -16,7 +16,7 @@ final class AppRouter {
|
|||
}
|
||||
|
||||
func start() {
|
||||
window.rootViewController = SplashBuilder.make()
|
||||
window.rootViewController = HomeBuilder.make()
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,3 @@
|
|||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>https://api.nytimes.com</key>
|
||||
<dict>
|
||||
<key>NSExceptionRequiresForwardSecrecy</key>
|
||||
<false/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// ControllerProtocol.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 17.12.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol BaseAppController {
|
||||
associatedtype ViewType: BaseAppView
|
||||
var viewImpl: ViewType? { get set }
|
||||
func registerObservers()
|
||||
func configureController()
|
||||
}
|
||||
|
||||
extension BaseAppController where Self: UIViewController {
|
||||
public var viewImpl: ViewType? {
|
||||
get { return self.view as? ViewType }
|
||||
set {}
|
||||
}
|
||||
|
||||
public func configureController() {
|
||||
self.registerObservers()
|
||||
self.navigationController?.isNavigationBarHidden = false
|
||||
self.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
|
@ -8,17 +8,17 @@
|
|||
import Combine
|
||||
import UIKit
|
||||
|
||||
// MARK: Base View Protocol
|
||||
// MARK: Base App View Protocol
|
||||
|
||||
public protocol ViewProtocol: AnyObject {
|
||||
public protocol BaseAppView: AnyObject {
|
||||
func setup()
|
||||
func setConstraints()
|
||||
func registerObservers()
|
||||
}
|
||||
|
||||
extension ViewProtocol where Self: UIView {
|
||||
extension BaseAppView where Self: UIView {
|
||||
|
||||
/// Start initializing the base `AppView` protocol methods
|
||||
/// Start initializing the base `BaseAppView` protocol methods
|
||||
public func configureContents() {
|
||||
self.setup()
|
||||
self.registerObservers()
|
|
@ -9,7 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
class DetailController: UIViewController, ControllerProtocol {
|
||||
class DetailController: UIViewController, BaseAppController {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
final class DetailView: UIView, ViewProtocol, UIScrollViewDelegate {
|
||||
final class DetailView: UIView, BaseAppView, UIScrollViewDelegate {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
@ -69,7 +68,7 @@ final class DetailView: UIView, ViewProtocol, UIScrollViewDelegate {
|
|||
}
|
||||
|
||||
|
||||
final class HeaderView: UIView, ViewProtocol {
|
||||
final class HeaderView: UIView, BaseAppView {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
@ -84,7 +83,7 @@ final class HeaderView: UIView, ViewProtocol {
|
|||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = ThemeManager.Font.Black.font(size: .xlarge)
|
||||
label.font = AppFont.Black.font(size: .xlarge)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
|
@ -132,7 +131,7 @@ final class HeaderView: UIView, ViewProtocol {
|
|||
|
||||
}
|
||||
|
||||
final class BodyView: UIView, ViewProtocol {
|
||||
final class BodyView: UIView, BaseAppView {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
@ -140,7 +139,7 @@ final class BodyView: UIView, ViewProtocol {
|
|||
|
||||
lazy var summaryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = ThemeManager.Font.Book.font(size: .medium)
|
||||
label.font = AppFont.Book.font(size: .medium)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
|
|
@ -11,7 +11,8 @@ final class HomeBuilder {
|
|||
|
||||
static func make() -> HomeController {
|
||||
let view = HomeView()
|
||||
let controller = HomeController(view: view, service: app.service)
|
||||
let controller = HomeController(view: view,
|
||||
service: app.service)
|
||||
return controller
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Combine
|
|||
import Kingfisher
|
||||
import iMoviesAPI
|
||||
|
||||
class HomeCell: UITableViewCell, ViewProtocol, Reuseable {
|
||||
class HomeCell: UITableViewCell, BaseAppView, Reuseable {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
@ -27,7 +27,7 @@ class HomeCell: UITableViewCell, ViewProtocol, Reuseable {
|
|||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = ThemeManager.Font.Medium.font(size: .mediumlarge)
|
||||
label.font = AppFont.Medium.font(size: .mediumlarge)
|
||||
label.numberOfLines = 1
|
||||
|
||||
return label
|
||||
|
@ -35,7 +35,7 @@ class HomeCell: UITableViewCell, ViewProtocol, Reuseable {
|
|||
|
||||
lazy var detailLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = ThemeManager.Font.Roman.font(size: .smallmedium)
|
||||
label.font = AppFont.Roman.font(size: .smallmedium)
|
||||
label.numberOfLines = 3
|
||||
return label
|
||||
}()
|
||||
|
@ -87,18 +87,3 @@ class HomeCell: UITableViewCell, ViewProtocol, Reuseable {
|
|||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Kingfiher
|
||||
extension UIImageView {
|
||||
|
||||
func setKfImage(for urlString: String?) {
|
||||
guard let urlString = urlString else {
|
||||
return
|
||||
}
|
||||
let url = URL(string: urlString)
|
||||
self.kf.indicatorType = .activity
|
||||
self.kf.setImage(with: url, options: KingfisherManager.shared.defaultOptions)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
class HomeController: UIViewController, ControllerProtocol {
|
||||
class HomeController: UIViewController, BaseAppController {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
|
@ -59,7 +59,7 @@ class HomeController: UIViewController, ControllerProtocol {
|
|||
guard let response = response else {
|
||||
return
|
||||
}
|
||||
self?.viewImpl?.movies = response.map(MoviePresentation.init)
|
||||
self?.viewImpl?.movies = response.map({ $0.map() })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -7,15 +7,14 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
final class HomeView: UIView, ViewProtocol {
|
||||
final class HomeView: UIView, BaseAppView {
|
||||
|
||||
var onCellSelect: ((_ movie: MoviePresentation) -> Void)?
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published public var movies: [MoviePresentation] = []
|
||||
@Published public var movies: MoviePresentations = []
|
||||
@Published public var isLoading: Bool = false
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
|
@ -45,6 +44,7 @@ final class HomeView: UIView, ViewProtocol {
|
|||
|
||||
func registerObservers() {
|
||||
$movies
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
self.tableView.reloadData()
|
||||
|
@ -80,6 +80,7 @@ extension HomeView: UITableViewDelegate {
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
self.onCellSelect?(movies[indexPath.row])
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// SplashBuilder.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 17.12.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class SplashBuilder {
|
||||
|
||||
static func make() -> SplashController {
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
let controller = storyboard.instantiateViewController(withIdentifier: "SplashController") as! SplashController
|
||||
return controller
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// SplashController.swift
|
||||
// iMoviesMVC
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 17.12.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class SplashController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
|
||||
let homeController = HomeBuilder.make()
|
||||
self.present(homeController.embedInNavigationController(), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// ResourceLoader.swift
|
||||
// iMoviesMVCTests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 1.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import iMoviesAPI
|
||||
|
||||
class ResourceLoader {
|
||||
|
||||
enum MovieResource: String {
|
||||
case movie1
|
||||
case movie2
|
||||
case movie3
|
||||
}
|
||||
|
||||
static func loadMovie(resource: MovieResource) throws -> Movie {
|
||||
let bundle = Bundle.test
|
||||
let url = try bundle.url(forResource: resource.rawValue, withExtension: ".json").unwrap()
|
||||
let data = try Data(contentsOf: url)
|
||||
let decoder = Decoders.plainDateDecoder
|
||||
let movie = try decoder.decode(Movie.self, from: data)
|
||||
return movie
|
||||
}
|
||||
}
|
||||
|
||||
private extension Bundle {
|
||||
class Dummy { }
|
||||
static let test = Bundle(for: Dummy.self)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"display_title": "The 22nd Annual Animation Show of Shows",
|
||||
"mpaa_rating": "",
|
||||
"critics_pick": 0,
|
||||
"byline": "Jeannette Catsoulis",
|
||||
"headline": "‘Annual Animation Show of Shows’ Review: A Mix of Whimsy and Dread",
|
||||
"summary_short": "This festival’s 22nd edition covers themes of crisis, both personal and planetary, with short works from the likes of Gil Alkabetz and Frédéric Back.",
|
||||
"publication_date": "2022-12-29",
|
||||
"opening_date": null,
|
||||
"date_updated": "2022-12-29 19:42:13",
|
||||
"link": {
|
||||
"type": "article",
|
||||
"url": "https://www.nytimes.com/2022/12/29/movies/annual-animation-show-of-shows-review.html",
|
||||
"suggested_link_text": "Read the New York Times Review of The 22nd Annual Animation Show of Shows"
|
||||
},
|
||||
"multimedia": {
|
||||
"type": "mediumThreeByTwo210",
|
||||
"src": "https://static01.nyt.com/images/2022/12/29/multimedia/29animation-show-review-1-856a/29animation-show-review-1-856a-mediumThreeByTwo440.jpg",
|
||||
"height": 140,
|
||||
"width": 210
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
|
||||
{
|
||||
"display_title": "Farha",
|
||||
"mpaa_rating": "",
|
||||
"critics_pick": 0,
|
||||
"byline": "Beatrice Loayza",
|
||||
"headline": "‘Farha’ Review: A Most Brutal Coming-of-Age Story",
|
||||
"summary_short": "Set in the early days of the Israeli-Palestinian conflict, this drama depicts the upheaval of Palestinian society from a 14-year-old girl’s perspective.",
|
||||
"publication_date": "2022-12-01",
|
||||
"opening_date": null,
|
||||
"date_updated": "2022-12-01 16:32:03",
|
||||
"link": {
|
||||
"type": "article",
|
||||
"url": "https://www.nytimes.com/2022/12/01/movies/farha-review.html",
|
||||
"suggested_link_text": "Read the New York Times Review of Farha"
|
||||
},
|
||||
"multimedia": {
|
||||
"type": "mediumThreeByTwo210",
|
||||
"src": "https://static01.nyt.com/images/2022/12/01/multimedia/01farha-review-1-26c4/01farha-review-1-26c4-mediumThreeByTwo440.jpg",
|
||||
"height": 140,
|
||||
"width": 210
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
{
|
||||
"display_title": "A Man Called Otto",
|
||||
"mpaa_rating": "PG-13",
|
||||
"critics_pick": 0,
|
||||
"byline": "Glenn Kenny",
|
||||
"headline": "‘A Man Called Otto’ Review: Tom Hanks Learns Life Lessons",
|
||||
"summary_short": "Going against nice-guy type (at first), the star plays a misanthrope who’s pulled into caring for a neighboring family in need.",
|
||||
"publication_date": "2022-12-29",
|
||||
"opening_date": "2023-01-04",
|
||||
"date_updated": "2022-12-29 12:02:02",
|
||||
"link": {
|
||||
"type": "article",
|
||||
"url": "https://www.nytimes.com/2022/12/29/movies/a-man-called-otto-review.html",
|
||||
"suggested_link_text": "Read the New York Times Review of A Man Called Otto"
|
||||
},
|
||||
"multimedia": {
|
||||
"type": "mediumThreeByTwo210",
|
||||
"src": "https://static01.nyt.com/images/2022/12/30/multimedia/29man-called-otto-1-438a/29man-called-otto-1-438a-mediumThreeByTwo440.jpg",
|
||||
"height": 140,
|
||||
"width": 210
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,30 +6,49 @@
|
|||
//
|
||||
|
||||
import XCTest
|
||||
@testable import iMoviesAPI
|
||||
@testable import iMoviesMVC
|
||||
|
||||
class iMoviesMVCTests: XCTestCase {
|
||||
|
||||
fileprivate var service: MockService!
|
||||
var view: HomeView!
|
||||
var controller: HomeController!
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
service = MockService()
|
||||
view = HomeView()
|
||||
controller = HomeController()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
service = nil
|
||||
view = nil
|
||||
controller = nil
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
func testMovieList() throws {
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
// Given
|
||||
let movie1 = try ResourceLoader.loadMovie(resource: .movie1)
|
||||
service.movies = [movie1]
|
||||
|
||||
// When
|
||||
controller.loadViewIfNeeded()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(view.movies.count, 1)
|
||||
XCTAssertEqual(try view.movies.element(at: 0).title, movie1.displayTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockService: WebServiceProtocol {
|
||||
|
||||
var movies: [Movie] = []
|
||||
|
||||
func search(movie: String, completion: @escaping ([Movie]?) -> Void) {
|
||||
completion(movies)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// AppContainer.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 7.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import iMoviesAPI
|
||||
|
||||
let app = AppContainer()
|
||||
|
||||
final class AppContainer {
|
||||
|
||||
// let router = AppRouter()
|
||||
let service = WebService()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
//
|
||||
// Approuter.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 7.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// iMoviesMVVM_SwiftUIApp.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct iMoviesMVVM_SwiftUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
HomeBuilder.make()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// DetailView.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 7.01.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct DetailView: View {
|
||||
|
||||
@State var movie: MoviePresentation?
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack (spacing: 15) {
|
||||
HeaderView(movie: movie)
|
||||
BodyView(summary: movie?.summary)
|
||||
Spacer()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Header View
|
||||
|
||||
struct HeaderView: View {
|
||||
|
||||
var movie: MoviePresentation?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
KFImage(movie?.imageUrl?.getURL())
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: ScreenSize.width,
|
||||
height: ScreenSize.height / 3,
|
||||
alignment: .center)
|
||||
.opacity(0.7)
|
||||
|
||||
Text(movie?.title ?? "")
|
||||
.frame(width: ScreenSize.width - 30,
|
||||
height: ScreenSize.height / 3 - 15,
|
||||
alignment: .bottomLeading)
|
||||
.font(AppFont.Black.font(size: .xlarge))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.background(.black)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Body View
|
||||
|
||||
struct BodyView: View {
|
||||
|
||||
var summary: String?
|
||||
|
||||
var body: some View {
|
||||
Text(summary ?? "")
|
||||
.font(AppFont.Book.font(size: .medium))
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DetailView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// HomeBuilder.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class HomeBuilder {
|
||||
|
||||
static func make() -> HomeView {
|
||||
let viewModel = HomeViewModel(service: app.service)
|
||||
let view = HomeView(viewModel: viewModel)
|
||||
return view
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
//
|
||||
// HomeContracts.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import iMoviesAPI
|
||||
|
||||
struct HomeView: View {
|
||||
|
||||
@State var title: String = "Movies"
|
||||
|
||||
@ObservedObject internal var viewModel: HomeViewModel
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
HomeTableView(movies: viewModel.movies)
|
||||
.navigationBarTitle(Text(title), displayMode: .inline)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - HomeTableView
|
||||
|
||||
struct HomeTableView: View {
|
||||
|
||||
@State var isSelected: Bool = false
|
||||
|
||||
var movies: [MoviePresentation] = []
|
||||
|
||||
var body: some View {
|
||||
List(movies, id: \.self) { movie in
|
||||
NavigationLink(destination: DetailView(movie: movie)) {
|
||||
HomeCellView(movie: movie)
|
||||
.padding([.top, .bottom], 10)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - HomeCellView
|
||||
|
||||
struct HomeCellView: View {
|
||||
|
||||
@State var movie: MoviePresentation?
|
||||
|
||||
var body: some View {
|
||||
HStack (spacing: 15) {
|
||||
KFImage(movie?.imageUrl?.getURL())
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 85, alignment: .center)
|
||||
.cornerRadius(12)
|
||||
|
||||
VStack (alignment: .leading, spacing: 3) {
|
||||
Text(movie?.title ?? "")
|
||||
.font(AppFont.Medium.font(size: .mediumlarge))
|
||||
.lineLimit(1)
|
||||
Text(movie?.summary ?? "")
|
||||
.font(AppFont.Medium.font(size: .smallmedium))
|
||||
.lineLimit(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct HomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HomeView(viewModel: HomeViewModel(service: app.service))
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func getURL() -> URL? {
|
||||
return URL(string: self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// HomeViewModel.swift
|
||||
// iMoviesMVVM-SwiftUI
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import iMoviesAPI
|
||||
|
||||
final class HomeViewModel: ObservableObject {
|
||||
|
||||
@Published var movies: MoviePresentations = []
|
||||
|
||||
internal var service: WebServiceProtocol?
|
||||
|
||||
init(service: WebServiceProtocol?) {
|
||||
self.service = service
|
||||
searchReviews(for: "all")
|
||||
}
|
||||
|
||||
func searchReviews(for movie: String) {
|
||||
service?.search(movie: movie, completion: { [weak self] (response) in
|
||||
guard let response = response else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.movies = response.map({ $0.map() })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// iMoviesMVVM_SwiftUITests.swift
|
||||
// iMoviesMVVM-SwiftUITests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import iMoviesMVVM_SwiftUI
|
||||
|
||||
class iMoviesMVVM_SwiftUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
//import iMoviesAPI
|
||||
import iMoviesAPI
|
||||
|
||||
let app = AppContainer()
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ import UIKit
|
|||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
app.router.start()
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ final class AppRouter {
|
|||
}
|
||||
|
||||
func start() {
|
||||
let viewController = ViewController()
|
||||
let navigationController = UINavigationController(rootViewController: viewController)
|
||||
let homeController = HomeBuilder.make()
|
||||
let navigationController = homeController.embedInNavigationController()
|
||||
window.rootViewController = navigationController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Home Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="HomeController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
|
@ -1,10 +1,11 @@
|
|||
//
|
||||
// ControllerProtocol.swift
|
||||
// iMoviesMVC
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 17.12.2022.
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol ControllerProtocol {
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// BaseViewProtocol.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
// MARK: View Protocol
|
||||
|
||||
public protocol ViewProtocol: AnyObject {
|
||||
associatedtype ViewModel
|
||||
var viewModel: ViewModel? { get set }
|
||||
func setup()
|
||||
func registerObservers()
|
||||
func setConstraints()
|
||||
func configureView(_ viewModel: ViewModel?)
|
||||
}
|
||||
|
||||
extension ViewProtocol where Self: UIView {
|
||||
|
||||
/// Start initializing the base `ViewProtocol` methods
|
||||
public func configureContents() {
|
||||
self.setup()
|
||||
self.registerObservers()
|
||||
self.setConstraints()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// DetailBuilder.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class DetailBuilder {
|
||||
|
||||
static func make(movie: MoviePresentation) -> DetailController {
|
||||
let view = DetailView()
|
||||
let viewModel = DetailViewModel(movie: movie)
|
||||
let controller = DetailController(view: view,
|
||||
viewModel: viewModel)
|
||||
return controller
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// DetailController.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
class DetailController: UIViewController, ControllerProtocol {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published internal var viewImpl: DetailView?
|
||||
@Published internal var viewModel: DetailViewModel?
|
||||
|
||||
convenience init(
|
||||
view: DetailView?,
|
||||
viewModel: DetailViewModel?
|
||||
) {
|
||||
self.init(nibName: nil, bundle: nil)
|
||||
self.viewImpl = view
|
||||
self.viewModel = viewModel
|
||||
self.configureController()
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
|
||||
$viewImpl.sink { [weak self] _view in
|
||||
guard let view = _view else { return }
|
||||
view.frame = UIScreen.main.bounds
|
||||
// TODO: view implementation
|
||||
self?.view.addSubview(view)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
viewModel?.$movie.sink { [weak self] (movie) in
|
||||
self?.viewImpl?.viewModel = movie
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// DetailView.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class DetailView: UIView, ViewProtocol {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published public var viewModel: MoviePresentation?
|
||||
@Published public var isLoading: Bool = false
|
||||
|
||||
lazy var headerView: HeaderView = {
|
||||
let view = HeaderView()
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bodyView: BodyView = {
|
||||
let view = BodyView()
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
configureContents()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .systemBackground
|
||||
addSubview(headerView)
|
||||
addSubview(bodyView)
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$viewModel
|
||||
.dropFirst()
|
||||
.sink { model in
|
||||
self.configureView(model)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
func configureView(_ viewModel: MoviePresentation?) {
|
||||
self.headerView.viewModel = viewModel
|
||||
self.bodyView.viewModel = viewModel?.summary
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bodyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
headerView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height / 3),
|
||||
|
||||
bodyView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
bodyView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
bodyView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bodyView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
final class HeaderView: UIView, ViewProtocol {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published public var viewModel: MoviePresentation?
|
||||
|
||||
lazy var thumbImage: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.alpha = 0.7
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Black.font(size: .xlarge)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
configureContents()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .black
|
||||
[thumbImage, titleLabel].forEach({ addSubview($0) })
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$viewModel
|
||||
.dropFirst()
|
||||
.sink { model in
|
||||
self.configureView(model)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
func configureView(_ viewModel: MoviePresentation?) {
|
||||
self.titleLabel.text = viewModel?.title
|
||||
self.thumbImage.setKfImage(for: viewModel?.imageUrl)
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
[thumbImage, titleLabel].forEach({
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
})
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
thumbImage.topAnchor.constraint(equalTo: topAnchor),
|
||||
thumbImage.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
thumbImage.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
thumbImage.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
|
||||
titleLabel.bottomAnchor.constraint(equalTo: thumbImage.bottomAnchor, constant: -5)
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class BodyView: UIView, ViewProtocol {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published public var viewModel: String?
|
||||
|
||||
lazy var summaryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Book.font(size: .medium)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
configureContents()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .systemBackground
|
||||
addSubview(summaryLabel)
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$viewModel
|
||||
.dropFirst()
|
||||
.sink { model in
|
||||
self.configureView(model)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
func configureView(_ viewModel: String?) {
|
||||
self.summaryLabel.text = viewModel
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
summaryLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
summaryLabel.topAnchor.constraint(equalTo: topAnchor, constant: 25),
|
||||
summaryLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
|
||||
summaryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// DetailViewModel.swift
|
||||
// iMovies
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
final class DetailViewModel {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published var movie: MoviePresentation?
|
||||
|
||||
init(movie: MoviePresentation?) {
|
||||
self.movie = movie
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// HomeBuilder.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HomeBuilder {
|
||||
|
||||
static func make() -> HomeController {
|
||||
let view = HomeView()
|
||||
let viewModel = HomeViewModel(service: app.service)
|
||||
let controller = HomeController(view: view,
|
||||
viewModel: viewModel)
|
||||
return controller
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// HomeCell.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import Kingfisher
|
||||
import iMoviesAPI
|
||||
|
||||
class HomeCell: UITableViewCell, ViewProtocol, Reuseable {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published internal var viewModel: MoviePresentation?
|
||||
|
||||
lazy var thumbImage: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.layer.cornerRadius = 12
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Medium.font(size: .mediumlarge)
|
||||
label.numberOfLines = 1
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var detailLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Roman.font(size: .smallmedium)
|
||||
label.numberOfLines = 3
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
self.configureContents()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
[thumbImage, titleLabel, detailLabel].forEach { contentView.addSubview($0) }
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$viewModel.sink { model in
|
||||
self.configureView(model)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
func configureView(_ viewModel: MoviePresentation?) {
|
||||
self.titleLabel.text = viewModel?.title
|
||||
self.detailLabel.text = viewModel?.summary
|
||||
self.thumbImage.setKfImage(for: viewModel?.imageUrl)
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
[contentView, thumbImage, titleLabel, detailLabel]
|
||||
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.topAnchor.constraint(equalTo: topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
thumbImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||
thumbImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 18),
|
||||
thumbImage.heightAnchor.constraint(equalToConstant: 85),
|
||||
thumbImage.widthAnchor.constraint(equalToConstant: 100),
|
||||
|
||||
titleLabel.topAnchor.constraint(equalTo: thumbImage.topAnchor),
|
||||
titleLabel.leadingAnchor.constraint(equalTo: thumbImage.trailingAnchor, constant: 15),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
|
||||
|
||||
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
|
||||
detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||
detailLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -18),
|
||||
detailLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -15)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// HomeController.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
class HomeController: UIViewController, ControllerProtocol {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published internal var viewImpl: HomeView?
|
||||
@Published internal var viewModel: HomeViewModel?
|
||||
|
||||
convenience init(
|
||||
view: HomeView?,
|
||||
viewModel: HomeViewModel?
|
||||
) {
|
||||
self.init(nibName: nil, bundle: nil)
|
||||
self.viewImpl = view
|
||||
self.viewModel = viewModel
|
||||
self.configureController()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
viewModel?.searchReviews(for: "all")
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
title = "iMovies"
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$viewImpl.sink { [weak self] _view in
|
||||
guard let view = _view else { return }
|
||||
view.frame = UIScreen.main.bounds
|
||||
|
||||
view.onCellSelect = { movie in
|
||||
let controller = DetailBuilder.make(movie: movie)
|
||||
self?.navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
self?.view.addSubview(view)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
viewModel?.$movies.sink{ [weak self] (movies) in
|
||||
self?.viewImpl?.movies = movies
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// HomeView.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class HomeView: UIView, ViewProtocol {
|
||||
|
||||
var onCellSelect: ((_ movie: MoviePresentation) -> Void)?
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
var viewModel: String?
|
||||
|
||||
@Published public var movies: MoviePresentations = []
|
||||
@Published public var isLoading: Bool = false
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
configureContents()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .systemBackground
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.registerCell(type: HomeCell.self)
|
||||
|
||||
addSubview(tableView)
|
||||
}
|
||||
|
||||
func registerObservers() {
|
||||
$movies
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
self.tableView.reloadData()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
$isLoading.sink { isLoading in
|
||||
//
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
func configureView(_ viewModel: String?) { }
|
||||
|
||||
func setConstraints() {
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension HomeView: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as HomeCell
|
||||
cell.viewModel = movies[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
self.onCellSelect?(movies[indexPath.row])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
extension HomeView: UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return movies.count
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// HomeViewModel.swift
|
||||
// iMoviesMVVM
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 2.01.2023.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import iMoviesAPI
|
||||
|
||||
final class HomeViewModel {
|
||||
|
||||
private var subscribers = Set<AnyCancellable>()
|
||||
|
||||
@Published var movies: MoviePresentations = []
|
||||
|
||||
internal var service: WebServiceProtocol?
|
||||
|
||||
init(service: WebServiceProtocol?) {
|
||||
self.service = service
|
||||
}
|
||||
|
||||
func searchReviews(for movie: String) {
|
||||
service?.search(movie: movie, completion: { [weak self] (response) in
|
||||
guard let response = response else {
|
||||
return
|
||||
}
|
||||
self?.movies = response.map({ $0.map() })
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// AppContainer.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import iMoviesAPI
|
||||
|
||||
let app = AppContainer()
|
||||
|
||||
final class AppContainer {
|
||||
|
||||
let router = AppRouter()
|
||||
let service = WebService()
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
app.router.start()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// AppRouter.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class AppRouter {
|
||||
|
||||
let window: UIWindow
|
||||
|
||||
init() {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func start() {
|
||||
let homeController = HomeBuilder.make()
|
||||
let navigationController = homeController.embedInNavigationController()
|
||||
window.rootViewController = navigationController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// DetailBuilder.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class DetailBuilder {
|
||||
|
||||
static func make(moviePresentation: MoviePresentation) -> DetailViewController {
|
||||
let viewController = DetailViewController()
|
||||
let presenter = DetailPresenter(view: viewController, moviePresentation: moviePresentation)
|
||||
viewController.presenter = presenter
|
||||
return viewController
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// DetailContracts.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol DetailPresenterProtocol {
|
||||
func load()
|
||||
}
|
||||
|
||||
protocol DetailViewProtocol: AnyObject {
|
||||
func update(_ presentation: MoviePresentation)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// DetailPresenter.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class DetailPresenter: DetailPresenterProtocol {
|
||||
|
||||
unowned var view: DetailViewProtocol
|
||||
|
||||
private let moviePresentation: MoviePresentation
|
||||
|
||||
init(
|
||||
view: DetailViewProtocol,
|
||||
moviePresentation: MoviePresentation
|
||||
) {
|
||||
self.view = view
|
||||
self.moviePresentation = moviePresentation
|
||||
}
|
||||
|
||||
func load() {
|
||||
view.update(moviePresentation)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// DetailView.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class DetailViewController: UIViewController, DetailViewProtocol {
|
||||
|
||||
internal var presenter: DetailPresenterProtocol?
|
||||
|
||||
lazy var headerView: HeaderView = {
|
||||
let view = HeaderView()
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bodyView: BodyView = {
|
||||
let view = BodyView()
|
||||
return view
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setup()
|
||||
presenter?.load()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
view.backgroundColor = .systemBackground
|
||||
view.addSubview(headerView)
|
||||
view.addSubview(bodyView)
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bodyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
headerView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height / 3),
|
||||
|
||||
bodyView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
bodyView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
bodyView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
bodyView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func update(_ presentation: MoviePresentation) {
|
||||
self.headerView.movie = presentation
|
||||
self.bodyView.summary = presentation.summary
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Header View
|
||||
|
||||
final class HeaderView: UIView {
|
||||
|
||||
internal var movie: MoviePresentation? {
|
||||
didSet {
|
||||
self.titleLabel.text = movie?.title
|
||||
self.thumbImage.setKfImage(for: movie?.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
lazy var thumbImage: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.alpha = 0.7
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Black.font(size: .xlarge)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .black
|
||||
[thumbImage, titleLabel].forEach({ addSubview($0) })
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
[thumbImage, titleLabel].forEach({
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
})
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
thumbImage.topAnchor.constraint(equalTo: topAnchor),
|
||||
thumbImage.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
thumbImage.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
thumbImage.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
|
||||
titleLabel.bottomAnchor.constraint(equalTo: thumbImage.bottomAnchor, constant: -5)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Body View
|
||||
|
||||
final class BodyView: UIView {
|
||||
|
||||
internal var summary: String? {
|
||||
didSet {
|
||||
self.summaryLabel.text = summary
|
||||
}
|
||||
}
|
||||
|
||||
lazy var summaryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Book.font(size: .medium)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
self.backgroundColor = .systemBackground
|
||||
addSubview(summaryLabel)
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
summaryLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
summaryLabel.topAnchor.constraint(equalTo: topAnchor, constant: 25),
|
||||
summaryLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
|
||||
summaryLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// HomeBuilder.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class HomeBuilder {
|
||||
|
||||
static func make() -> HomeViewController {
|
||||
let view = HomeViewController()
|
||||
let router = HomeRouter(view: view)
|
||||
let interactor = HomeInteractor(service: app.service)
|
||||
let presenter = HomePresenter(view: view,
|
||||
interactor: interactor,
|
||||
router: router)
|
||||
view.presenter = presenter
|
||||
return view
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// HomeCell.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
class HomeCell: UITableViewCell, Reuseable {
|
||||
|
||||
lazy var thumbImage: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.layer.cornerRadius = 12
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Medium.font(size: .mediumlarge)
|
||||
label.numberOfLines = 1
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var detailLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = AppFont.Roman.font(size: .smallmedium)
|
||||
label.numberOfLines = 3
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setup() {
|
||||
[thumbImage, titleLabel, detailLabel].forEach { contentView.addSubview($0) }
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
func update(_ presentation: MoviePresentation?) {
|
||||
self.titleLabel.text = presentation?.title
|
||||
self.detailLabel.text = presentation?.summary
|
||||
self.thumbImage.setKfImage(for: presentation?.imageUrl)
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
[contentView, thumbImage, titleLabel, detailLabel]
|
||||
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.topAnchor.constraint(equalTo: topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
thumbImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||
thumbImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 18),
|
||||
thumbImage.heightAnchor.constraint(equalToConstant: 85),
|
||||
thumbImage.widthAnchor.constraint(equalToConstant: 100),
|
||||
|
||||
titleLabel.topAnchor.constraint(equalTo: thumbImage.topAnchor),
|
||||
titleLabel.leadingAnchor.constraint(equalTo: thumbImage.trailingAnchor, constant: 15),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
|
||||
|
||||
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
|
||||
detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||
detailLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -18),
|
||||
detailLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -15)
|
||||
])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// HomeContracts.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import iMoviesAPI
|
||||
|
||||
// MARK: - Interactor
|
||||
|
||||
protocol HomeInteractorProtocol: AnyObject {
|
||||
var delegate: HomeInteractorDelegate? { get set }
|
||||
var service: WebServiceProtocol { get set }
|
||||
var movies: [Movie] { get set }
|
||||
func searchReviews()
|
||||
func selectMovie(at index: Int)
|
||||
}
|
||||
|
||||
protocol HomeInteractorDelegate: AnyObject {
|
||||
func handleOutput(_ output: HomeInteractorOutput)
|
||||
}
|
||||
|
||||
enum HomeInteractorOutput {
|
||||
case showMovies([Movie])
|
||||
case showMovieDetail(MoviePresentation)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Presenter
|
||||
|
||||
protocol HomePresenterProtocol: AnyObject {
|
||||
var view: HomeViewProtocol { get set }
|
||||
var router: HomeRouterProtocol { get set }
|
||||
var interactor: HomeInteractorProtocol { get set }
|
||||
func load()
|
||||
func selectMovie(at index: Int)
|
||||
}
|
||||
|
||||
enum HomePresenterOutput {
|
||||
case updateTitle(String)
|
||||
case showMovies([MoviePresentation])
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View
|
||||
|
||||
protocol HomeViewProtocol: AnyObject {
|
||||
var movies: MoviePresentations { get set }
|
||||
var presenter: HomePresenterProtocol? { get set }
|
||||
func handleOutput(_ output: HomePresenterOutput)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Router
|
||||
|
||||
protocol HomeRouterProtocol: AnyObject {
|
||||
func navigate(to route: HomeRoute)
|
||||
}
|
||||
|
||||
enum HomeRoute {
|
||||
case detail(MoviePresentation)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// HomeInteractor.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import iMoviesAPI
|
||||
|
||||
final class HomeInteractor: HomeInteractorProtocol {
|
||||
|
||||
var delegate: HomeInteractorDelegate?
|
||||
|
||||
internal var movies: [Movie] = []
|
||||
|
||||
internal var service: WebServiceProtocol
|
||||
|
||||
init(service: WebServiceProtocol) {
|
||||
self.service = service
|
||||
}
|
||||
|
||||
func searchReviews() {
|
||||
service.search(movie: "all", completion: { [weak self] (response) in
|
||||
guard let response = response else {
|
||||
return
|
||||
}
|
||||
self?.movies = response
|
||||
self?.delegate?.handleOutput(.showMovies(response))
|
||||
})
|
||||
}
|
||||
|
||||
func selectMovie(at index: Int) {
|
||||
let movie = movies[index]
|
||||
let moviePresentation = movie.map()
|
||||
self.delegate?.handleOutput(.showMovieDetail(moviePresentation))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// HomePresenter.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class HomePresenter: HomePresenterProtocol {
|
||||
|
||||
internal var view: HomeViewProtocol
|
||||
internal var router: HomeRouterProtocol
|
||||
internal var interactor: HomeInteractorProtocol
|
||||
|
||||
init(view: HomeViewProtocol,
|
||||
interactor: HomeInteractorProtocol,
|
||||
router: HomeRouter
|
||||
) {
|
||||
self.view = view
|
||||
self.interactor = interactor
|
||||
self.router = router
|
||||
|
||||
interactor.delegate = self
|
||||
|
||||
view.handleOutput(.updateTitle("Movies"))
|
||||
}
|
||||
|
||||
func load() {
|
||||
interactor.searchReviews()
|
||||
}
|
||||
|
||||
func selectMovie(at index: Int) {
|
||||
interactor.selectMovie(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Home Interactor Delegate
|
||||
|
||||
extension HomePresenter: HomeInteractorDelegate {
|
||||
|
||||
func handleOutput(_ output: HomeInteractorOutput) {
|
||||
switch output {
|
||||
case .showMovies(let movies):
|
||||
let moviePresentations = movies.map({ $0.map() })
|
||||
view.handleOutput(.showMovies(moviePresentations))
|
||||
case .showMovieDetail(let movie):
|
||||
router.navigate(to: .detail(movie))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// HomeRouter.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HomeRouter: HomeRouterProtocol {
|
||||
|
||||
unowned let view: UIViewController
|
||||
|
||||
init(view: UIViewController) {
|
||||
self.view = view
|
||||
}
|
||||
|
||||
func navigate(to route: HomeRoute) {
|
||||
switch route {
|
||||
case .detail(let moviePresentation):
|
||||
let detailView = DetailBuilder.make(moviePresentation: moviePresentation)
|
||||
view.show(detailView, sender: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// HomeView.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HomeViewController: UIViewController, HomeViewProtocol {
|
||||
|
||||
internal var presenter: HomePresenterProtocol?
|
||||
|
||||
internal var movies: MoviePresentations = []
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.registerCell(type: HomeCell.self)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setup()
|
||||
presenter?.load()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
view.backgroundColor = .systemBackground
|
||||
view.addSubview(tableView)
|
||||
setConstraints()
|
||||
}
|
||||
|
||||
func setConstraints() {
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func handleOutput(_ output: HomePresenterOutput) {
|
||||
switch output {
|
||||
case .updateTitle(let title):
|
||||
self.title = title
|
||||
case .showMovies(let movies):
|
||||
self.movies = movies
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension HomeViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as HomeCell
|
||||
cell.update(movies[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
presenter?.selectMovie(at: indexPath.row)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
extension HomeViewController: UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return movies.count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
|
@ -7,19 +7,18 @@
|
|||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// SceneDelegate.swift
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
guard let _ = (scene as? UIWindowScene) else { return }
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
//
|
||||
// ViewController.swift
|
||||
// iMoviesMVVM
|
||||
// iMoviesVIPER
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 6.12.2022.
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
@ -11,8 +11,7 @@ class ViewController: UIViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
print("Hello MVVM")
|
||||
view.backgroundColor = .green
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// iMoviesVIPERTests.swift
|
||||
// iMoviesVIPERTests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import iMoviesVIPER
|
||||
|
||||
class iMoviesVIPERTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// iMoviesVIPERUITests.swift
|
||||
// iMoviesVIPERUITests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class iMoviesVIPERUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// iMoviesVIPERUITestsLaunchTests.swift
|
||||
// iMoviesVIPERUITests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class iMoviesVIPERUITestsLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// iMoviesVIPERTests.swift
|
||||
// iMoviesVIPERTests
|
||||
//
|
||||
// Created by Eyüp Yasuntimur on 5.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import iMoviesVIPER
|
||||
|
||||
class iMoviesVIPERTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue