Compare commits

..

15 Commits

Author SHA1 Message Date
eyupyasuntimur 17389aa2e0 target iMoviesMVVM+SwiftUI completed 2023-01-07 20:19:35 +03:00
eyupyasuntimur a52420bb16 HomeView completed for SwiftUI+MVVM 2023-01-07 17:28:02 +03:00
eyupyasuntimur bd980bb062 ThemeManager refactored for SwiftUI as AppFont 2023-01-07 17:05:58 +03:00
eyupyasuntimur f3cfa9a622 HomeContracts updated 2023-01-06 21:22:22 +03:00
eyupyasuntimur f53d097d67 +++ 2023-01-06 19:52:22 +03:00
eyupyasuntimur 8054c4c707 tableView cell selection updated 2023-01-06 19:52:00 +03:00
eyupyasuntimur dced6087d7 VIPER part completed 2023-01-06 19:46:10 +03:00
eyupyasuntimur 479d89a47c HomeInteractor added 2023-01-06 10:01:44 +03:00
eyupyasuntimur c4b072dcf0 HomeView added 2023-01-06 09:54:23 +03:00
eyupyasuntimur 93fcb4f3a2 all targets main.storyboard deleted, VIPER empty files added respect to architecture 2023-01-05 21:16:45 +03:00
eyupyasuntimur 54aee8775c VIPER target created 2023-01-05 18:06:38 +03:00
eyupyasuntimur 439a7e2bde MVC infoPlist updated 2023-01-05 15:50:01 +03:00
eyupyasuntimur 6dc64129f2 MVVM part completed 2023-01-03 09:18:16 +03:00
eyupyasuntimur a8ec22590f BaseProtocols added for MVVM target 2023-01-02 17:07:33 +03:00
eyupyasuntimur 635b637ef9 MVC unitTests improving 2023-01-02 16:42:49 +03:00
92 changed files with 3627 additions and 320 deletions

View File

@ -1,40 +0,0 @@
# iMovies
iMovies is an iOS demo project that showcases different software architectures such as MVC, MVVM, and VIPER in different targets. Additionally, there is also a target that demonstrates the use of SwiftUI with MVVM architecture.
### Features
- Demonstrates different software architectures such as MVC, MVVM, VIPER in different targets
- A separate target that demonstrates the use of SwiftUI with MVVM architecture
- Fetche top-rated movies in a list
- View movie details including ratings, release date, and overview
### Installation
1. Clone the repository: `git clone https://github.com/yasuntimure/iMovies.git`
3. Start the app: `open iMovies.xcworkspace`
### Technologies
- Swift, iOS
- The NYC Movie Database API
### Screenshots
#### Lightmode
<p float="left">
<img src="https://user-images.githubusercontent.com/58510802/212549113-cd48cadc-5b92-4828-9bb2-cc4032dd3864.png" width="300" hspace="20"/>
<img src="https://user-images.githubusercontent.com/58510802/212549115-bb3140a0-264f-4f8d-bd33-0b684d4ca4dd.png" width="300" hspace="20"/>
</p>
#### Darkmode
<p float="left">
<img src="https://user-images.githubusercontent.com/58510802/212549492-04d72967-470b-4427-a994-71d5a86ff826.png" width="300" hspace="20"/>
<img src="https://user-images.githubusercontent.com/58510802/212549497-5e6e44af-7b20-410c-9ffb-988333cec6d6.png" width="300" hspace="20"/>
</p>
### Contributing
If you would like to contribute to the project, please fork the repository and submit a pull request.

View File

@ -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]
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -7,7 +7,6 @@
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

View File

@ -16,7 +16,7 @@ final class AppRouter {
}
func start() {
window.rootViewController = SplashBuilder.make()
window.rootViewController = HomeBuilder.make()
window.makeKeyAndVisible()
}
}

View File

@ -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>

View File

@ -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()
}
}

View File

@ -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()

View File

@ -9,7 +9,7 @@ import UIKit
import Combine
import iMoviesAPI
class DetailController: UIViewController, ControllerProtocol {
class DetailController: UIViewController, BaseAppController {
private var subscribers = Set<AnyCancellable>()

View File

@ -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
}()

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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() })
})
}

View File

@ -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])
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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 festivals 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
}
}

View File

@ -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 girls 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
}
}

View File

@ -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 whos 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
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -0,0 +1,8 @@
//
// Approuter.swift
// iMoviesMVVM-SwiftUI
//
// Created by Eyüp Yasuntimur on 7.01.2023.
//
import UIKit

View File

@ -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()
}
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,8 @@
//
// HomeContracts.swift
// iMoviesMVVM-SwiftUI
//
// Created by Eyüp Yasuntimur on 6.01.2023.
//
import Foundation

View File

@ -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)
}
}

View File

@ -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() })
}
})
}
}

View File

@ -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.
}
}
}

View File

@ -6,7 +6,7 @@
//
import Foundation
//import iMoviesAPI
import iMoviesAPI
let app = AppContainer()

View File

@ -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
}

View File

@ -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()
}

View File

@ -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>

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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),
])
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
])
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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() })
})
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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>

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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),
])
}
}

View File

@ -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
}
}

View File

@ -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)
])
}
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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.
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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.
}
}

View File

@ -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.
}

View File

@ -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.
}
}
}

View File

@ -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 its 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()
}
}
}
}

View File

@ -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)
}
}

View File

@ -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.
}
}
}