Skip to content

Commit

Permalink
Merge pull request #26 from ispiryan/Storyboard-vs-Code-Layout-DI-and…
Browse files Browse the repository at this point in the history
…-Composition

Storyboard vs code layout di and composition
  • Loading branch information
ispiryan authored Jun 7, 2024
2 parents df01727 + 8b87ac9 commit 525e71b
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 132 deletions.
20 changes: 16 additions & 4 deletions FeedApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
97102C5B2BE05B60007C90B3 /* FeedStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97102C5A2BE05B60007C90B3 /* FeedStoreSpecs.swift */; };
97102C5D2BE05E19007C90B3 /* XCTestCase+FeedStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97102C5C2BE05E19007C90B3 /* XCTestCase+FeedStoreSpecs.swift */; };
97192B8B2BD69E9800AE5E69 /* CacheFeedUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97192B8A2BD69E9800AE5E69 /* CacheFeedUseCaseTests.swift */; };
9728EA9A2C11F22900F28C4A /* Feed.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9728EA992C11F22900F28C4A /* Feed.storyboard */; };
9728EA9C2C11F29800F28C4A /* Feed.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9728EA9B2C11F29800F28C4A /* Feed.xcassets */; };
9728EA9E2C13238200F28C4A /* UITableView+Dequeueing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9728EA9D2C13238200F28C4A /* UITableView+Dequeueing.swift */; };
9728EAA02C13240E00F28C4A /* UIImage+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9728EA9F2C13240E00F28C4A /* UIImage+Animation.swift */; };
973BA9CE2BC5478C00013B53 /* FeedApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 973BA9C52BC5478900013B53 /* FeedApp.framework */; };
973BA9D32BC5478C00013B53 /* LoadFeedFromRemoteUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973BA9D22BC5478C00013B53 /* LoadFeedFromRemoteUseCaseTests.swift */; };
973BA9D42BC5478C00013B53 /* FeedApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 973BA9C82BC5478900013B53 /* FeedApp.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand All @@ -39,7 +43,6 @@
974D6F552BF4DF7500F7211C /* FeedImageCell+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F542BF4DF7500F7211C /* FeedImageCell+TestHelpers.swift */; };
974D6F572BF4DFDD00F7211C /* FeedViewControllerTests+LoaderSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F562BF4DFDD00F7211C /* FeedViewControllerTests+LoaderSpy.swift */; };
974D6F5B2BF4E2E100F7211C /* FeedViewControllerTests+Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F5A2BF4E2E100F7211C /* FeedViewControllerTests+Assertions.swift */; };
974D6F5D2BF4E3D000F7211C /* FeedRefreshViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F5C2BF4E3D000F7211C /* FeedRefreshViewController.swift */; };
974D6F602BF4EC9700F7211C /* FeedImageCellController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F5F2BF4EC9700F7211C /* FeedImageCellController.swift */; };
974D6F622BF4EF5C00F7211C /* FeedUIComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F612BF4EF5C00F7211C /* FeedUIComposer.swift */; };
974D6F682BF6575400F7211C /* FeedImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F672BF6575400F7211C /* FeedImageViewModel.swift */; };
Expand Down Expand Up @@ -127,6 +130,10 @@
97102C5A2BE05B60007C90B3 /* FeedStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStoreSpecs.swift; sourceTree = "<group>"; };
97102C5C2BE05E19007C90B3 /* XCTestCase+FeedStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FeedStoreSpecs.swift"; sourceTree = "<group>"; };
97192B8A2BD69E9800AE5E69 /* CacheFeedUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheFeedUseCaseTests.swift; sourceTree = "<group>"; };
9728EA992C11F22900F28C4A /* Feed.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Feed.storyboard; sourceTree = "<group>"; };
9728EA9B2C11F29800F28C4A /* Feed.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Feed.xcassets; sourceTree = "<group>"; };
9728EA9D2C13238200F28C4A /* UITableView+Dequeueing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Dequeueing.swift"; sourceTree = "<group>"; };
9728EA9F2C13240E00F28C4A /* UIImage+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Animation.swift"; sourceTree = "<group>"; };
973BA9C52BC5478900013B53 /* FeedApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FeedApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
973BA9C82BC5478900013B53 /* FeedApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedApp.h; sourceTree = "<group>"; };
973BA9CD2BC5478C00013B53 /* FeedAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -145,7 +152,6 @@
974D6F542BF4DF7500F7211C /* FeedImageCell+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedImageCell+TestHelpers.swift"; sourceTree = "<group>"; };
974D6F562BF4DFDD00F7211C /* FeedViewControllerTests+LoaderSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+LoaderSpy.swift"; sourceTree = "<group>"; };
974D6F5A2BF4E2E100F7211C /* FeedViewControllerTests+Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Assertions.swift"; sourceTree = "<group>"; };
974D6F5C2BF4E3D000F7211C /* FeedRefreshViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRefreshViewController.swift; sourceTree = "<group>"; };
974D6F5F2BF4EC9700F7211C /* FeedImageCellController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCellController.swift; sourceTree = "<group>"; };
974D6F612BF4EF5C00F7211C /* FeedUIComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUIComposer.swift; sourceTree = "<group>"; };
974D6F672BF6575400F7211C /* FeedImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -364,7 +370,6 @@
isa = PBXGroup;
children = (
9708B1C72BF28E9800D170EA /* FeedViewController.swift */,
974D6F5C2BF4E3D000F7211C /* FeedRefreshViewController.swift */,
974D6F5F2BF4EC9700F7211C /* FeedImageCellController.swift */,
);
path = Controllers;
Expand All @@ -375,6 +380,10 @@
children = (
974D6F402BF3A07B00F7211C /* UIView+Shimmering.swift */,
974D6F3E2BF366A700F7211C /* FeedImageCell.swift */,
9728EA992C11F22900F28C4A /* Feed.storyboard */,
9728EA9B2C11F29800F28C4A /* Feed.xcassets */,
9728EA9D2C13238200F28C4A /* UITableView+Dequeueing.swift */,
9728EA9F2C13240E00F28C4A /* UIImage+Animation.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -735,6 +744,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9728EA9A2C11F22900F28C4A /* Feed.storyboard in Resources */,
9728EA9C2C11F29800F28C4A /* Feed.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -811,10 +822,11 @@
buildActionMask = 2147483647;
files = (
974D6F6B2BF78D2800F7211C /* FeedPresenter.swift in Sources */,
974D6F5D2BF4E3D000F7211C /* FeedRefreshViewController.swift in Sources */,
9762D4972BF8A60D00FCCC7E /* FeedViewModel.swift in Sources */,
974D6F472BF4DB9900F7211C /* FeedImageDataLoader.swift in Sources */,
9728EA9E2C13238200F28C4A /* UITableView+Dequeueing.swift in Sources */,
9762D4952BF8A5D900FCCC7E /* FeedLoadingViewModel.swift in Sources */,
9728EAA02C13240E00F28C4A /* UIImage+Animation.swift in Sources */,
974D6F412BF3A07B00F7211C /* UIView+Shimmering.swift in Sources */,
9762D4932BF8A4B200FCCC7E /* FeedImagePresenter.swift in Sources */,
9708B1C82BF28E9800D170EA /* FeedViewController.swift in Sources */,
Expand Down
47 changes: 25 additions & 22 deletions FeedAppiOS/Feed UI/Composers/FeedUIComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@ import FeedApp

public final class FeedUIComposer {
private init() {}

public static func feedComposedWith(feedLoader: FeedLoader, imageLoader: FeedImageDataLoader) -> FeedViewController {
let presentationAdapter = FeedLoaderPresentationAdapter(feedLoader: feedLoader)
let refreshController = FeedRefreshViewController(delegate: presentationAdapter)
let feedController = FeedViewController(refreshController: refreshController)


let bundle = Bundle(for: FeedViewController.self)
let storyboard = UIStoryboard(name: "Feed", bundle: bundle)
let feedController = storyboard.instantiateInitialViewController() as! FeedViewController
feedController.delegate = presentationAdapter

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

loadingView: WeakRefVirtualProxy(feedController))
return feedController
}
}

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

init(_ object: T) {
self.object = object
}
Expand All @@ -47,42 +50,42 @@ extension WeakRefVirtualProxy: FeedImageView where T: FeedImageView, T.Image ==
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 final class FeedLoaderPresentationAdapter: FeedViewControllerDelegate {
private let feedLoader: FeedLoader
var presenter: FeedPresenter?

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

func didRequestFeedRefresh() {
presenter?.didStartLoadingFeed()

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)
}
Expand All @@ -94,29 +97,29 @@ private final class FeedImageDataLoaderPresentationAdapter<View: FeedImageView,
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()
}
Expand Down
32 changes: 22 additions & 10 deletions FeedAppiOS/Feed UI/Controllers/FeedImageCellController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,44 @@ protocol FeedImageCellControllerDelegate {

final class FeedImageCellController: FeedImageView {
private let delegate: FeedImageCellControllerDelegate
private lazy var cell = FeedImageCell()
private var cell: FeedImageCell?

init(delegate: FeedImageCellControllerDelegate) {
self.delegate = delegate
}

func view() -> UITableViewCell {
func view(in tableView: UITableView) -> UITableViewCell {
cell = tableView.dequeueReusableCell()
delegate.didRequestImage()
return cell
return cell!
}

func preload() {
delegate.didRequestImage()
}

func cancelLoad() {
releaseCellForReuse()
delegate.didCancelImageRequest()
}

func display(_ viewModel: FeedImageViewModel<UIImage>) {
cell.locationContainer.isHidden = !viewModel.hasLocation
cell.locationLabel.text = viewModel.location
cell.descriptionLabel.text = viewModel.description
cell.feedImageView.image = viewModel.image
cell.feedImageContainer.isShimmering = viewModel.isLoading
cell.feedImageRetryButton.isHidden = !viewModel.shouldRetry
cell.onRetry = delegate.didRequestImage
cell?.locationContainer.isHidden = !viewModel.hasLocation
cell?.locationLabel.text = viewModel.location
cell?.descriptionLabel.text = viewModel.description
cell?.feedImageView.setImageAnimated(viewModel.image)
cell?.feedImageContainer.isShimmering = viewModel.isLoading
cell?.feedImageRetryButton.isHidden = !viewModel.shouldRetry
cell?.onRetry = { [weak self] in
self?.delegate.didRequestImage()
}
cell?.onReuse = { [weak self] in
self?.releaseCellForReuse()
}
}

private func releaseCellForReuse() {
cell?.onReuse = nil
cell = nil
}
}
39 changes: 0 additions & 39 deletions FeedAppiOS/Feed UI/Controllers/FeedRefreshViewController.swift

This file was deleted.

60 changes: 30 additions & 30 deletions FeedAppiOS/Feed UI/Controllers/FeedViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,61 @@

import UIKit

public final class FeedViewController: UITableViewController, UITableViewDataSourcePrefetching {
private var refreshController: FeedRefreshViewController?
protocol FeedViewControllerDelegate {
func didRequestFeedRefresh()
}

public final class FeedViewController: UITableViewController, UITableViewDataSourcePrefetching, FeedLoadingView {
var delegate: FeedViewControllerDelegate?

var tableModel = [FeedImageCellController]() {
didSet { tableView.reloadData() }
}
private var isViewIsAppeared = false
private var cellControllers = [IndexPath: FeedImageCellController]()

convenience init(refreshController: FeedRefreshViewController) {
self.init()
self.refreshController = refreshController
}

public override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)

if !isViewIsAppeared {
refreshController?.refresh()
}
isViewIsAppeared = true
}

public override func viewDidLoad() {
super.viewDidLoad()

tableView.prefetchDataSource = self
refreshControl = refreshController?.view

refresh()
}

@IBAction private func refresh() {
delegate?.didRequestFeedRefresh()
}

func display(_ viewModel: FeedLoadingViewModel) {
if viewModel.isLoading {
refreshControl?.beginRefreshing()
} else {
refreshControl?.endRefreshing()
}
}

public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
tableModel.count
return tableModel.count
}

public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cellController(forRowAt: indexPath).view()
return cellController(forRowAt: indexPath).view(in: tableView)
}

public override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cancelCellControllerLoad(forRowAt: indexPath)
}

public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
indexPaths.forEach { indexPath in
cellController(forRowAt: indexPath).preload()
}
}

public func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
indexPaths.forEach(cancelCellControllerLoad)
}

private func cellController(forRowAt indexPath: IndexPath) -> FeedImageCellController {
tableModel[indexPath.row]
return tableModel[indexPath.row]
}

private func cancelCellControllerLoad(forRowAt indexPath: IndexPath) {
cellController(forRowAt: indexPath).cancelLoad()
}
Expand Down
Loading

0 comments on commit 525e71b

Please sign in to comment.