Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image Comments Composition #20

Merged
merged 14 commits into from
May 12, 2024
Merged
Changes from all commits
Commits
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
20 changes: 12 additions & 8 deletions EssentialApp/EssentialApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
E83BE59D2BEFAA1700F9A8F4 /* CommentsUIIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83BE59C2BEFAA1700F9A8F4 /* CommentsUIIntegrationTests.swift */; };
E83BE59F2BEFAD3200F9A8F4 /* CommentsUIComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83BE59E2BEFAD3200F9A8F4 /* CommentsUIComposer.swift */; };
E84384642BC86A500022E869 /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84384632BC86A500022E869 /* XCTestCase+MemoryLeakTracking.swift */; };
E84384662BC86B160022E869 /* SharedTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84384652BC86B160022E869 /* SharedTestHelpers.swift */; };
E8733A982BC2AB050023CF63 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8733A972BC2AB050023CF63 /* AppDelegate.swift */; };
@@ -31,9 +33,8 @@
E8BF06362BCCFDF0008D2B6A /* UIRefreshControl+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF062C2BCCFDF0008D2B6A /* UIRefreshControl+TestHelpers.swift */; };
E8BF06372BCCFDF0008D2B6A /* UIImage+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF062D2BCCFDF0008D2B6A /* UIImage+TestHelpers.swift */; };
E8BF06382BCCFDF0008D2B6A /* UIControl+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF062F2BCCFDF0008D2B6A /* UIControl+TestHelpers.swift */; };
E8BF06392BCCFDF0008D2B6A /* FeedViewController+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF06322BCCFDF0008D2B6A /* FeedViewController+Localization.swift */; };
E8BF063A2BCCFDF0008D2B6A /* FeedViewControllerTests+LoaderSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF06312BCCFDF0008D2B6A /* FeedViewControllerTests+LoaderSpy.swift */; };
E8BF063B2BCCFDF0008D2B6A /* FeedViewController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF062E2BCCFDF0008D2B6A /* FeedViewController+TestHelpers.swift */; };
E8BF063B2BCCFDF0008D2B6A /* ListViewController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF062E2BCCFDF0008D2B6A /* ListViewController+TestHelpers.swift */; };
E8BF063D2BCD00F1008D2B6A /* FeedAcceptanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF063C2BCD00F1008D2B6A /* FeedAcceptanceTests.swift */; };
E8BF063F2BCD2520008D2B6A /* HTTPClientStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF063E2BCD2520008D2B6A /* HTTPClientStub.swift */; };
E8BF06412BCD2555008D2B6A /* InMemoryFeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BF06402BCD2555008D2B6A /* InMemoryFeedStore.swift */; };
@@ -67,6 +68,8 @@

/* Begin PBXFileReference section */
E823024E2BCA9140006E1F23 /* EssentialApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialApp.xctestplan; sourceTree = "<group>"; };
E83BE59C2BEFAA1700F9A8F4 /* CommentsUIIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsUIIntegrationTests.swift; sourceTree = "<group>"; };
E83BE59E2BEFAD3200F9A8F4 /* CommentsUIComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsUIComposer.swift; sourceTree = "<group>"; };
E84384632BC86A500022E869 /* XCTestCase+MemoryLeakTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+MemoryLeakTracking.swift"; sourceTree = "<group>"; };
E84384652BC86B160022E869 /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = "<group>"; };
E845BC082BC46D5B00441128 /* EssentialAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -90,11 +93,10 @@
E8BF062B2BCCFDF0008D2B6A /* UIButton+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF062C2BCCFDF0008D2B6A /* UIRefreshControl+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF062D2BCCFDF0008D2B6A /* UIImage+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF062E2BCCFDF0008D2B6A /* FeedViewController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewController+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF062E2BCCFDF0008D2B6A /* ListViewController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListViewController+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF062F2BCCFDF0008D2B6A /* UIControl+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF06302BCCFDF0008D2B6A /* FeedImageCell+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedImageCell+TestHelpers.swift"; sourceTree = "<group>"; };
E8BF06312BCCFDF0008D2B6A /* FeedViewControllerTests+LoaderSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+LoaderSpy.swift"; sourceTree = "<group>"; };
E8BF06322BCCFDF0008D2B6A /* FeedViewController+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewController+Localization.swift"; sourceTree = "<group>"; };
E8BF063C2BCD00F1008D2B6A /* FeedAcceptanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedAcceptanceTests.swift; sourceTree = "<group>"; };
E8BF063E2BCD2520008D2B6A /* HTTPClientStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClientStub.swift; sourceTree = "<group>"; };
E8BF06402BCD2555008D2B6A /* InMemoryFeedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryFeedStore.swift; sourceTree = "<group>"; };
@@ -128,11 +130,10 @@
E8BF062C2BCCFDF0008D2B6A /* UIRefreshControl+TestHelpers.swift */,
E8BF062B2BCCFDF0008D2B6A /* UIButton+TestHelpers.swift */,
E8BF062F2BCCFDF0008D2B6A /* UIControl+TestHelpers.swift */,
E8BF062E2BCCFDF0008D2B6A /* FeedViewController+TestHelpers.swift */,
E8BF062E2BCCFDF0008D2B6A /* ListViewController+TestHelpers.swift */,
E8BF06302BCCFDF0008D2B6A /* FeedImageCell+TestHelpers.swift */,
E8BF06312BCCFDF0008D2B6A /* FeedViewControllerTests+LoaderSpy.swift */,
E8BF062A2BCCFDF0008D2B6A /* FeedViewControllerTests+Assertions.swift */,
E8BF06322BCCFDF0008D2B6A /* FeedViewController+Localization.swift */,
E84384632BC86A500022E869 /* XCTestCase+MemoryLeakTracking.swift */,
E84384652BC86B160022E869 /* SharedTestHelpers.swift */,
E8BF063E2BCD2520008D2B6A /* HTTPClientStub.swift */,
@@ -148,6 +149,7 @@
E84384622BC86A0A0022E869 /* Helpers */,
E8BF060E2BCCF6B8008D2B6A /* SceneDelegateTests.swift */,
E8BF06282BCCFDDC008D2B6A /* FeedUIIntegrationTests.swift */,
E83BE59C2BEFAA1700F9A8F4 /* CommentsUIIntegrationTests.swift */,
E8BF063C2BCD00F1008D2B6A /* FeedAcceptanceTests.swift */,
);
path = EssentialAppTests;
@@ -184,6 +186,7 @@
E8BF06202BCCFDC8008D2B6A /* WeakRefVirtualProxy.swift */,
E8BF061D2BCCFDC8008D2B6A /* FeedViewAdapter.swift */,
E8BF061F2BCCFDC8008D2B6A /* LoadResourcePresentationAdapter.swift */,
E83BE59E2BEFAD3200F9A8F4 /* CommentsUIComposer.swift */,
E8733AA02BC2AB060023CF63 /* Assets.xcassets */,
E8733AA22BC2AB060023CF63 /* LaunchScreen.storyboard */,
E8733AA52BC2AB060023CF63 /* Info.plist */,
@@ -303,9 +306,9 @@
files = (
E84384662BC86B160022E869 /* SharedTestHelpers.swift in Sources */,
E8BF063A2BCCFDF0008D2B6A /* FeedViewControllerTests+LoaderSpy.swift in Sources */,
E8BF063B2BCCFDF0008D2B6A /* FeedViewController+TestHelpers.swift in Sources */,
E83BE59D2BEFAA1700F9A8F4 /* CommentsUIIntegrationTests.swift in Sources */,
E8BF063B2BCCFDF0008D2B6A /* ListViewController+TestHelpers.swift in Sources */,
E8BF06292BCCFDDC008D2B6A /* FeedUIIntegrationTests.swift in Sources */,
E8BF06392BCCFDF0008D2B6A /* FeedViewController+Localization.swift in Sources */,
E8BF06342BCCFDF0008D2B6A /* FeedViewControllerTests+Assertions.swift in Sources */,
E8BF060F2BCCF6B8008D2B6A /* SceneDelegateTests.swift in Sources */,
E8BF06352BCCFDF0008D2B6A /* FeedImageCell+TestHelpers.swift in Sources */,
@@ -329,6 +332,7 @@
E8BF06252BCCFDC8008D2B6A /* FeedUIComposer.swift in Sources */,
E8C3FAD82BDD088100A5934C /* CombineHelpers.swift in Sources */,
E8733A9C2BC2AB050023CF63 /* ViewController.swift in Sources */,
E83BE59F2BEFAD3200F9A8F4 /* CommentsUIComposer.swift in Sources */,
E8733A982BC2AB050023CF63 /* AppDelegate.swift in Sources */,
E8733A9A2BC2AB050023CF63 /* SceneDelegate.swift in Sources */,
E8BF06272BCCFDC8008D2B6A /* LoadResourcePresentationAdapter.swift in Sources */,
64 changes: 64 additions & 0 deletions EssentialApp/EssentialApp/CommentsUIComposer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// CommentsUIComposer.swift
// EssentialApp
//
// Created by Finn Ebeling on 11.05.24.
//

import UIKit
import Combine
import EssentialFeed
import EssentialFeediOS

public final class CommentsUIComposer {
private init() {}

private typealias CommentsPresentationAdapter = LoadResourcePresentationAdapter<[ImageComment], CommentsViewAdapter>

public static func commentsComposedWith(
commentsLoader: @escaping () -> AnyPublisher<[ImageComment], Error>
) -> ListViewController {
let presentationAdapter = CommentsPresentationAdapter(
loader: commentsLoader
)

let commentsController = Self.makeCommentsViewController(
title: ImageCommentsPresenter.title
)
commentsController.onRefresh = presentationAdapter.loadResource

presentationAdapter.presenter = LoadResourcePresenter(
resourceView: CommentsViewAdapter(controller: commentsController),
loadingView: WeakRefVirtualProxy(commentsController),
errorView: WeakRefVirtualProxy(commentsController),
mapper: { ImageCommentsPresenter.map($0) }
)

return commentsController
}

private static func makeCommentsViewController(title: String) -> ListViewController {
let bundle = Bundle(for: ListViewController.self)
let storyboard = UIStoryboard(name: "ImageComments", bundle: bundle)
let controller = storyboard.instantiateInitialViewController { coder in
ListViewController(coder: coder)
}!
controller.title = title

return controller
}
}

final class CommentsViewAdapter: ResourceView {
private weak var controller: ListViewController?

init(controller: ListViewController) {
self.controller = controller
}

func display(_ viewModel: ImageCommentsViewModel) {
controller?.display(viewModel.comments.map { viewModel in
CellController(id: viewModel, ImageCommentCellController(model: viewModel))
})
}
}
8 changes: 5 additions & 3 deletions EssentialApp/EssentialApp/FeedUIComposer.swift
Original file line number Diff line number Diff line change
@@ -17,7 +17,8 @@ public final class FeedUIComposer {

public static func feedComposedWith(
feedLoader: @escaping () -> AnyPublisher<[FeedImage], Error>,
imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher
imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher,
selection: @escaping (FeedImage) -> Void = { _ in }
) -> ListViewController {
let presentationAdapter = FeedPresentationAdapter(
loader: feedLoader
@@ -31,7 +32,8 @@ public final class FeedUIComposer {
presentationAdapter.presenter = LoadResourcePresenter(
resourceView: FeedViewAdapter(
controller: feedController,
imageLoader: imageLoader
imageLoader: imageLoader,
selection: selection
),
loadingView: WeakRefVirtualProxy(feedController),
errorView: WeakRefVirtualProxy(feedController),
@@ -47,7 +49,7 @@ public final class FeedUIComposer {
let feedController = storyboard.instantiateInitialViewController { coder in
ListViewController(coder: coder)
}!
feedController.title = FeedPresenter.title
feedController.title = title

return feedController
}
13 changes: 11 additions & 2 deletions EssentialApp/EssentialApp/FeedViewAdapter.swift
Original file line number Diff line number Diff line change
@@ -12,12 +12,18 @@ import EssentialFeediOS
final class FeedViewAdapter: ResourceView {
private weak var controller: ListViewController?
private let imageLoader: (URL) -> FeedImageDataLoader.Publisher
private let selection: (FeedImage) -> Void

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

init(controller: ListViewController, imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher) {
init(
controller: ListViewController,
imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher,
selection: @escaping (FeedImage) -> Void
) {
self.controller = controller
self.imageLoader = imageLoader
self.selection = selection
}

func display(_ viewModel: FeedViewModel) {
@@ -30,7 +36,10 @@ final class FeedViewAdapter: ResourceView {
)
let view = FeedImageCellController(
viewModel: FeedImagePresenter.map(model),
delegate: adapter
delegate: adapter,
selection: { [selection] in
selection(model)
}
)

adapter.presenter = LoadResourcePresenter(
36 changes: 28 additions & 8 deletions EssentialApp/EssentialApp/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -30,9 +30,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
LocalFeedLoader(store: store, currentDate: Date.init)
}()

private lazy var baseURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed")!

private lazy var navigationController = UINavigationController(
rootViewController: FeedUIComposer.feedComposedWith(
feedLoader: makeRemoteFeedLoaderWithLocalFallback,
imageLoader: makeLocalImageLoaderWithRemoteFallback,
selection: showComments
)
)

// Declare it as attribute to hold a strong reference to it.
private lazy var remoteFeedLoaderPublisher: AnyPublisher<[FeedImage], Error> = {
let remoteURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed/v1/feed")!
let remoteURL = FeedEndpoint.get.url(baseURL: baseURL)

return httpClient
.getPublisher(url: remoteURL)
@@ -54,20 +64,30 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}

func configureWindow() {
window?.rootViewController = UINavigationController(
rootViewController: FeedUIComposer.feedComposedWith(
feedLoader: makeRemoteFeedLoaderWithLocalFallback,
imageLoader: makeLocalImageLoaderWithRemoteFallback
)
)

window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}

func sceneWillResignActive(_ scene: UIScene) {
localFeedLoader.validateCache { _ in }
}

private func showComments(for image: FeedImage) {
let url = ImageCommentsEndpoint.get(image.id).url(baseURL: baseURL)
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: makeRemoteCommentsLoader(url: url))

navigationController.pushViewController(comments, animated: true)
}

private func makeRemoteCommentsLoader(url: URL) -> () -> AnyPublisher<[ImageComment], Error> {
return { [httpClient] in
httpClient
.getPublisher(url: url)
.tryMap(ImageCommentsMapper.map)
.eraseToAnyPublisher()
}
}

private func makeRemoteFeedLoaderWithLocalFallback() -> AnyPublisher<[FeedImage], Error> {
remoteFeedLoaderPublisher
.caching(to: localFeedLoader)
Loading
Loading