Skip to content

Commit

Permalink
Merge pull request #27 from seunbanwo/feature/image-feed/ui/mvp-refac…
Browse files Browse the repository at this point in the history
…toring

MVP Refactoring
  • Loading branch information
seunbanwo authored Nov 28, 2023
2 parents 1071320 + c0943c9 commit 66f1724
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 146 deletions.
26 changes: 19 additions & 7 deletions EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@
EDFBE2692B15754100EFB793 /* FeedRefreshViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2682B15754100EFB793 /* FeedRefreshViewController.swift */; };
EDFBE26B2B157EBA00EFB793 /* FeedImageCellController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE26A2B157EBA00EFB793 /* FeedImageCellController.swift */; };
EDFBE26E2B15830500EFB793 /* FeedUIComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE26D2B15830500EFB793 /* FeedUIComposer.swift */; };
EDFBE2712B159E5200EFB793 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2702B159E5200EFB793 /* FeedViewModel.swift */; };
EDFBE2732B15A5BD00EFB793 /* FeedImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2722B15A5BD00EFB793 /* FeedImageViewModel.swift */; };
EDFBE2762B15BA4700EFB793 /* FeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2752B15BA4700EFB793 /* FeedPresenter.swift */; };
EDFBE2782B15C8EB00EFB793 /* FeedImagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */; };
EDFBE27A2B15CC5700EFB793 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */; };
EDFBE27C2B15CDC600EFB793 /* FeedLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -189,8 +192,11 @@
EDFBE2682B15754100EFB793 /* FeedRefreshViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRefreshViewController.swift; sourceTree = "<group>"; };
EDFBE26A2B157EBA00EFB793 /* FeedImageCellController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCellController.swift; sourceTree = "<group>"; };
EDFBE26D2B15830500EFB793 /* FeedUIComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUIComposer.swift; sourceTree = "<group>"; };
EDFBE2702B159E5200EFB793 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
EDFBE2722B15A5BD00EFB793 /* FeedImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageViewModel.swift; sourceTree = "<group>"; };
EDFBE2752B15BA4700EFB793 /* FeedPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = "<group>"; };
EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImagePresenter.swift; sourceTree = "<group>"; };
EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedLoadingViewModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -326,6 +332,7 @@
ED0EDEBC2B0FB681004857B7 /* EssentialFeediOS */ = {
isa = PBXGroup;
children = (
EDFBE2742B15B0FB00EFB793 /* Feed Presentation */,
EDFBE24E2B14FD2400EFB793 /* Feed Image Loader */,
EDFBE24F2B14FD3E00EFB793 /* Feed UI */,
);
Expand Down Expand Up @@ -464,7 +471,6 @@
EDFBE24F2B14FD3E00EFB793 /* Feed UI */ = {
isa = PBXGroup;
children = (
EDFBE26F2B159E3E00EFB793 /* Models */,
EDFBE26C2B1582F400EFB793 /* Composers */,
EDFBE2512B14FDB600EFB793 /* Views */,
EDFBE2502B14FDA700EFB793 /* Controllers */,
Expand Down Expand Up @@ -538,13 +544,16 @@
path = Composers;
sourceTree = "<group>";
};
EDFBE26F2B159E3E00EFB793 /* Models */ = {
EDFBE2742B15B0FB00EFB793 /* Feed Presentation */ = {
isa = PBXGroup;
children = (
EDFBE2702B159E5200EFB793 /* FeedViewModel.swift */,
EDFBE2722B15A5BD00EFB793 /* FeedImageViewModel.swift */,
EDFBE2752B15BA4700EFB793 /* FeedPresenter.swift */,
EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */,
EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */,
EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */,
);
path = Models;
path = "Feed Presentation";
sourceTree = "<group>";
};
/* End PBXGroup section */
Expand Down Expand Up @@ -840,14 +849,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EDFBE2762B15BA4700EFB793 /* FeedPresenter.swift in Sources */,
EDFBE2672B15741100EFB793 /* FeedImageDataLoader.swift in Sources */,
EDFBE2692B15754100EFB793 /* FeedRefreshViewController.swift in Sources */,
EDFBE24D2B13186200EFB793 /* UIView+Shimmering.swift in Sources */,
EDFBE2782B15C8EB00EFB793 /* FeedImagePresenter.swift in Sources */,
EDFBE27A2B15CC5700EFB793 /* FeedViewModel.swift in Sources */,
EDFBE2492B10534300EFB793 /* FeedViewController.swift in Sources */,
EDFBE27C2B15CDC600EFB793 /* FeedLoadingViewModel.swift in Sources */,
EDFBE24B2B107C8900EFB793 /* FeedImageCell.swift in Sources */,
EDFBE2732B15A5BD00EFB793 /* FeedImageViewModel.swift in Sources */,
EDFBE26B2B157EBA00EFB793 /* FeedImageCellController.swift in Sources */,
EDFBE2712B159E5200EFB793 /* FeedViewModel.swift in Sources */,
EDFBE26E2B15830500EFB793 /* FeedUIComposer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// FeedImagePresenter.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

import Foundation
import EssentialFeed

protocol FeedImageView {
associatedtype Image

func display(_ model: FeedImageViewModel<Image>)
}

final class FeedImagePresenter<View: FeedImageView, Image> where View.Image == Image {
private let view: View
private let imageTransformer: (Data) -> Image?

internal init(view: View, imageTransformer: @escaping (Data) -> Image?) {
self.view = view
self.imageTransformer = imageTransformer
}

func didStartLoadingImageData(for model: FeedImage) {
view.display(FeedImageViewModel(
description: model.description,
location: model.location,
image: nil,
isLoading: true,
shouldRetry: false))
}

private struct InvalidImageDataError: Error {}

func didFinishLoadingImageData(with data: Data, for model: FeedImage) {
guard let image = imageTransformer(data) else {
return didFinishLoadingImageData(with: InvalidImageDataError(), for: model)
}

view.display(FeedImageViewModel(
description: model.description,
location: model.location,
image: image,
isLoading: false,
shouldRetry: false))
}

func didFinishLoadingImageData(with error: Error, for model: FeedImage) {
view.display(FeedImageViewModel(
description: model.description,
location: model.location,
image: nil,
isLoading: false,
shouldRetry: true))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// FeedImageViewModel.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

struct FeedImageViewModel<Image> {
let description: String?
let location: String?
let image: Image?
let isLoading: Bool
let shouldRetry: Bool

var hasLocation: Bool {
return location != nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// FeedLoadingViewModel.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

struct FeedLoadingViewModel {
let isLoading: Bool
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// FeedPresenter.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

import EssentialFeed

protocol FeedLoadingView {
func display(_ viewModel: FeedLoadingViewModel)
}

protocol FeedView {
func display(_ viewModel: FeedViewModel)
}

final class FeedPresenter {
private let feedView: FeedView
private let loadingView: FeedLoadingView

init(feedView: FeedView, loadingView: FeedLoadingView) {
self.feedView = feedView
self.loadingView = loadingView
}

func didStartLoadingFeed() {
loadingView.display(FeedLoadingViewModel(isLoading: true))
}

func didFinishLoadingFeed(with feed: [FeedImage]) {
feedView.display(FeedViewModel(feed: feed))
loadingView.display(FeedLoadingViewModel(isLoading: false))
}

func didFinishLoadingFeed(with error: Error) {
loadingView.display(FeedLoadingViewModel(isLoading: false))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// FeedViewModel.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

import EssentialFeed

struct FeedViewModel {
let feed: [FeedImage]
}
109 changes: 102 additions & 7 deletions EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,113 @@ public final class FeedUIComposer {
private init() {}

public static func feedComposedWith(feedLoader: FeedLoader, imageLoader: FeedImageDataLoader) -> FeedViewController {
let feedViewModel = FeedViewModel(feedLoader: feedLoader)
let refreshController = FeedRefreshViewController(viewModel: feedViewModel)
let presentationAdapter = FeedLoaderPresentationAdapter(feedLoader: feedLoader)
let refreshController = FeedRefreshViewController(delegate: presentationAdapter)
let feedController = FeedViewController(refreshController: refreshController)
feedViewModel.onFeedLoad = adaptFeedToCellControllers(forwardingTo: feedController, loader: imageLoader)

presentationAdapter.presenter = FeedPresenter(
feedView: FeedViewAdapter(controller: feedController, imageLoader: imageLoader),
loadingView: WeakRefVirtualProxy(refreshController)
)

return feedController
}
}

private final class WeakRefVirtualProxy<T: AnyObject> {
private weak var object: T?

init(_ object: T) {
self.object = object
}
}

extension WeakRefVirtualProxy: FeedLoadingView where T: FeedLoadingView {
func display(_ viewModel: FeedLoadingViewModel) {
object?.display(viewModel)
}
}

extension WeakRefVirtualProxy: FeedImageView where T: FeedImageView, T.Image == UIImage {
func display(_ model: FeedImageViewModel<UIImage>) {
object?.display(model)
}
}

private final class FeedViewAdapter: FeedView {
private weak var controller: FeedViewController?
private let imageLoader: FeedImageDataLoader

init(controller: FeedViewController, imageLoader: FeedImageDataLoader) {
self.controller = controller
self.imageLoader = imageLoader
}

func display(_ viewModel: FeedViewModel) {
controller?.tableModel = viewModel.feed.map { model in
let adapter = FeedImageDataLoaderPresentationAdapter<WeakRefVirtualProxy<FeedImageCellController>, UIImage>(model: model, imageLoader: imageLoader)
let view = FeedImageCellController(delegate: adapter)

adapter.presenter = FeedImagePresenter(
view: WeakRefVirtualProxy(view),
imageTransformer: UIImage.init)

return view
}
}
}

private final class FeedLoaderPresentationAdapter: FeedRefreshViewControllerDelegate {
private let feedLoader: FeedLoader
var presenter: FeedPresenter?

init(feedLoader: FeedLoader) {
self.feedLoader = feedLoader
}

func didRequestFeedRefresh() {
presenter?.didStartLoadingFeed()

private static func adaptFeedToCellControllers(forwardingTo controller: FeedViewController, loader: FeedImageDataLoader) -> ([FeedImage]) -> Void {
return { [weak controller] feed in
controller?.tableModel = feed.map { model in
FeedImageCellController(viewModel: FeedImageViewModel(model: model, imageLoader: loader, imageTransformer: UIImage.init))
feedLoader.load { [weak self] result in
switch result {
case let .success(feed):
self?.presenter?.didFinishLoadingFeed(with: feed)

case let .failure(error):
self?.presenter?.didFinishLoadingFeed(with: error)
}
}
}
}

private final class FeedImageDataLoaderPresentationAdapter<View: FeedImageView, Image>: FeedImageCellControllerDelegate where View.Image == Image {
private let model: FeedImage
private let imageLoader: FeedImageDataLoader
private var task: FeedImageDataLoaderTask?

var presenter: FeedImagePresenter<View, Image>?

init(model: FeedImage, imageLoader: FeedImageDataLoader) {
self.model = model
self.imageLoader = imageLoader
}

func didRequestImage() {
presenter?.didStartLoadingImageData(for: model)

let model = self.model
task = imageLoader.loadImageData(from: model.url) { [weak self] result in
switch result {
case let .success(data):
self?.presenter?.didFinishLoadingImageData(with: data, for: model)

case let .failure(error):
self?.presenter?.didFinishLoadingImageData(with: error, for: model)
}
}
}

func didCancelImageRequest() {
task?.cancel()
}
}
Loading

0 comments on commit 66f1724

Please sign in to comment.