Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6753975
Duplicate FeedPresenter as LoadResourcePresenter
PortoCode Mar 14, 2025
0ad2f37
Remove title from generic presenter since it's specific to each prese…
PortoCode Mar 14, 2025
769ffe7
Rename method
PortoCode Mar 14, 2025
7c8a5c7
Displays mapped resource on successful resource loading
PortoCode Mar 14, 2025
c3b777c
Make LoadResourcePresenter generic over the Resource types
PortoCode Mar 14, 2025
2208bfb
Replace "FEED_VIEW_CONNECTION_ERROR" key with "GENERIC_CONNECTION_ERROR"
PortoCode Mar 14, 2025
18afb6c
Move "GENERIC_CONNECTION_ERROR" localization key to new Shared.strings
PortoCode Mar 14, 2025
cdd6e74
Remove duplication in the localization tests
PortoCode Mar 14, 2025
729614c
Rename and move ResourceLoadingView and VIewModel to Shared module
PortoCode Mar 14, 2025
75d7efb
Rename and move ResourceErrorView and ViewModel to Shared module
PortoCode Mar 15, 2025
0535344
Add FeedPresenter map
PortoCode Mar 15, 2025
f3530e2
Replace FeedPresenter with generic presenter
PortoCode Mar 15, 2025
ccb605f
Remove unused FeedPresenter logic
PortoCode Mar 15, 2025
48025c5
Inline param
PortoCode Mar 15, 2025
923e056
Make Presentation Adapter generic so it can be reused
PortoCode Mar 15, 2025
4792a1d
Add FeedImagePresenter map
PortoCode Mar 15, 2025
b520129
Display error on mapper error
PortoCode Mar 15, 2025
b7d85d2
Replace FeedImagePresenter with LoadResourcePresenter
PortoCode Mar 15, 2025
855a22f
Remove unused FeedImagePresenter logic
PortoCode Mar 15, 2025
83ac9c1
Add type aliases to shorten type definitions with generics
PortoCode Mar 15, 2025
1028394
Move UIImage creation to a `tryMake` extension
PortoCode Mar 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

36 changes: 0 additions & 36 deletions EssentialApp/EssentialApp/FeedLoaderPresentationAdapter.swift

This file was deleted.

11 changes: 7 additions & 4 deletions EssentialApp/EssentialApp/FeedUIComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ import EssentialFeediOS
public final class FeedUIComposer {
private init() {}

private typealias FeedPresentationAdapter = LoadResourcePresentationAdapter<[FeedImage], FeedViewAdapter>

public static func feedComposedWith(
feedLoader: @escaping () -> AnyPublisher<[FeedImage], Error>,
imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher
) -> FeedViewController {
let presentationAdapter = FeedLoaderPresentationAdapter(feedLoader: feedLoader)
let presentationAdapter = FeedPresentationAdapter(loader: feedLoader)

let feedController = makeFeedViewController(
delegate: presentationAdapter,
title: FeedPresenter.title)

presentationAdapter.presenter = FeedPresenter(
feedView: FeedViewAdapter(
presentationAdapter.presenter = LoadResourcePresenter(
resourceView: FeedViewAdapter(
controller: feedController,
imageLoader: imageLoader),
loadingView: WeakRefVirtualProxy(feedController),
errorView: WeakRefVirtualProxy(feedController))
errorView: WeakRefVirtualProxy(feedController),
mapper: FeedPresenter.map)

return feedController
}
Expand Down
32 changes: 26 additions & 6 deletions EssentialApp/EssentialApp/FeedViewAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,45 @@ import UIKit
import EssentialFeed
import EssentialFeediOS

final class FeedViewAdapter: FeedView {
final class FeedViewAdapter: ResourceView {
private weak var controller: FeedViewController?
private let imageLoader: (URL) -> FeedImageDataLoader.Publisher

private typealias ImageDataPresentationAdapter = LoadResourcePresentationAdapter<Data, WeakRefVirtualProxy<FeedImageCellController>>

init(controller: FeedViewController? = nil, imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher) {
self.controller = controller
self.imageLoader = imageLoader
}

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

let view = FeedImageCellController(
viewModel: FeedImagePresenter.map(model),
delegate: adapter)

adapter.presenter = FeedImagePresenter(
view: WeakRefVirtualProxy(view),
imageTransformer: UIImage.init)
adapter.presenter = LoadResourcePresenter(
resourceView: WeakRefVirtualProxy(view),
loadingView: WeakRefVirtualProxy(view),
errorView: WeakRefVirtualProxy(view),
mapper: UIImage.tryMake)

return view
})
}
}

extension UIImage {
struct InvalidImageData: Error {}

static func tryMake(data: Data) throws -> UIImage {
guard let image = UIImage(data: data) else {
throw InvalidImageData()
}
return image
}
}
53 changes: 53 additions & 0 deletions EssentialApp/EssentialApp/LoadResourcePresentationAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Created by Rodrigo Porto.
// Copyright © 2025 PortoCode. All Rights Reserved.
//

import Combine
import EssentialFeed
import EssentialFeediOS

final class LoadResourcePresentationAdapter<Resource, View: ResourceView> {
private let loader: () -> AnyPublisher<Resource, Error>
private var cancellable: Cancellable?
var presenter: LoadResourcePresenter<Resource, View>?

init(loader: @escaping () -> AnyPublisher<Resource, Error>) {
self.loader = loader
}

func loadResource() {
presenter?.didStartLoading()

cancellable = loader()
.dispatchOnMainQueue()
.sink(
receiveCompletion: { [weak self] completion in
switch completion {
case .finished: break

case let .failure(error):
self?.presenter?.didFinishLoading(with: error)
}
}, receiveValue: { [weak self] resource in
self?.presenter?.didFinishLoading(with: resource)
})
}
}

extension LoadResourcePresentationAdapter: FeedViewControllerDelegate {
func didRequestFeedRefresh() {
loadResource()
}
}

extension LoadResourcePresentationAdapter: FeedImageCellControllerDelegate {
func didRequestImage() {
loadResource()
}

func didCancelImageRequest() {
cancellable?.cancel()
cancellable = nil
}
}
12 changes: 6 additions & 6 deletions EssentialApp/EssentialApp/WeakRefVirtualProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ final class WeakRefVirtualProxy<T: AnyObject> {
}
}

extension WeakRefVirtualProxy: FeedErrorView where T: FeedErrorView {
func display(_ viewModel: FeedErrorViewModel) {
extension WeakRefVirtualProxy: ResourceErrorView where T: ResourceErrorView {
func display(_ viewModel: ResourceErrorViewModel) {
object?.display(viewModel)
}
}

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

extension WeakRefVirtualProxy: FeedImageView where T: FeedImageView, T.Image == UIImage {
func display(_ model: FeedImageViewModel<UIImage>) {
extension WeakRefVirtualProxy: ResourceView where T: ResourceView, T.ResourceViewModel == UIImage {
func display(_ model: UIImage) {
object?.display(model)
}
}
4 changes: 2 additions & 2 deletions EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class FeedUIIntegrationTests: XCTestCase {

sut.simulateAppearance()

XCTAssertEqual(sut.title, localized("FEED_VIEW_TITLE"))
XCTAssertEqual(sut.title, feedTitle)
}

func test_loadFeedActions_requestFeedFromLoader() {
Expand Down Expand Up @@ -112,7 +112,7 @@ final class FeedUIIntegrationTests: XCTestCase {
XCTAssertEqual(sut.errorMessage, nil)

loader.completeFeedLoadingWithError(at: 0)
XCTAssertEqual(sut.errorMessage, localized("FEED_VIEW_CONNECTION_ERROR"))
XCTAssertEqual(sut.errorMessage, loadError)

sut.simulateUserInitiatedFeedReload()
XCTAssertEqual(sut.errorMessage, nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import XCTest
import EssentialFeed

extension FeedUIIntegrationTests {
func localized(_ key: String, file: StaticString = #file, line: UInt = #line) -> String {
let table = "Feed"
let bundle = Bundle(for: FeedPresenter.self)
let value = bundle.localizedString(forKey: key, value: nil, table: table)
if value == key {
XCTFail("Missing localized string for key: \(key) in table: \(table)", file: file, line: line)
}
return value
private class DummyView: ResourceView {
func display(_ viewModel: Any) {}
}

var loadError: String {
LoadResourcePresenter<Any, DummyView>.loadError
}

var feedTitle: String {
FeedPresenter.title
}
}
Loading