diff --git a/.$FeedAppDiagram.drawio.bkp b/.$FeedAppDiagram.drawio.bkp
index ca8ad37..3f64d7d 100644
--- a/.$FeedAppDiagram.drawio.bkp
+++ b/.$FeedAppDiagram.drawio.bkp
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/FeedApp.xcodeproj/project.pbxproj b/FeedApp.xcodeproj/project.pbxproj
index d30b1ee..d8513f9 100644
--- a/FeedApp.xcodeproj/project.pbxproj
+++ b/FeedApp.xcodeproj/project.pbxproj
@@ -42,13 +42,16 @@
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 */; };
- 974D6F662BF646BF00F7211C /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F652BF646BF00F7211C /* FeedViewModel.swift */; };
974D6F682BF6575400F7211C /* FeedImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F672BF6575400F7211C /* FeedImageViewModel.swift */; };
+ 974D6F6B2BF78D2800F7211C /* FeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974D6F6A2BF78D2800F7211C /* FeedPresenter.swift */; };
975945E22BCA777C005F6F16 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975945E12BCA777C005F6F16 /* HTTPClient.swift */; };
975945E42BCA77E6005F6F16 /* FeedItemsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975945E32BCA77E6005F6F16 /* FeedItemsMapper.swift */; };
975F8C8C2BE060CC008489E7 /* XCTestCase+FailableInsertFeedStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F8C8B2BE060CC008489E7 /* XCTestCase+FailableInsertFeedStoreSpecs.swift */; };
975F8C8E2BE0614E008489E7 /* XCTestCase+FailableDeleteFeedStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F8C8D2BE0614E008489E7 /* XCTestCase+FailableDeleteFeedStoreSpecs.swift */; };
975F8C902BE061E7008489E7 /* XCTestCase+FailableRetrieveFeedStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F8C8F2BE061E7008489E7 /* XCTestCase+FailableRetrieveFeedStoreSpecs.swift */; };
+ 9762D4932BF8A4B200FCCC7E /* FeedImagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9762D4922BF8A4B200FCCC7E /* FeedImagePresenter.swift */; };
+ 9762D4952BF8A5D900FCCC7E /* FeedLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9762D4942BF8A5D900FCCC7E /* FeedLoadingViewModel.swift */; };
+ 9762D4972BF8A60D00FCCC7E /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9762D4962BF8A60D00FCCC7E /* FeedViewModel.swift */; };
97993B642BEE50D500C453F7 /* FeedAppiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97993B5C2BEE50D400C453F7 /* FeedAppiOS.framework */; };
97AAE3A02BE504EC0073BD75 /* CoreDataFeedStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AAE39F2BE504EC0073BD75 /* CoreDataFeedStoreTests.swift */; };
97AAE3A22BE5057F0073BD75 /* CoreDataFeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AAE3A12BE5057F0073BD75 /* CoreDataFeedStore.swift */; };
@@ -145,13 +148,16 @@
974D6F5C2BF4E3D000F7211C /* FeedRefreshViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRefreshViewController.swift; sourceTree = ""; };
974D6F5F2BF4EC9700F7211C /* FeedImageCellController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCellController.swift; sourceTree = ""; };
974D6F612BF4EF5C00F7211C /* FeedUIComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUIComposer.swift; sourceTree = ""; };
- 974D6F652BF646BF00F7211C /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = ""; };
974D6F672BF6575400F7211C /* FeedImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageViewModel.swift; sourceTree = ""; };
+ 974D6F6A2BF78D2800F7211C /* FeedPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; };
975945E12BCA777C005F6F16 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; };
975945E32BCA77E6005F6F16 /* FeedItemsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemsMapper.swift; sourceTree = ""; };
975F8C8B2BE060CC008489E7 /* XCTestCase+FailableInsertFeedStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FailableInsertFeedStoreSpecs.swift"; sourceTree = ""; };
975F8C8D2BE0614E008489E7 /* XCTestCase+FailableDeleteFeedStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FailableDeleteFeedStoreSpecs.swift"; sourceTree = ""; };
975F8C8F2BE061E7008489E7 /* XCTestCase+FailableRetrieveFeedStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FailableRetrieveFeedStoreSpecs.swift"; sourceTree = ""; };
+ 9762D4922BF8A4B200FCCC7E /* FeedImagePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImagePresenter.swift; sourceTree = ""; };
+ 9762D4942BF8A5D900FCCC7E /* FeedLoadingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedLoadingViewModel.swift; sourceTree = ""; };
+ 9762D4962BF8A60D00FCCC7E /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = ""; };
97993B5C2BEE50D400C453F7 /* FeedAppiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FeedAppiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
97993B632BEE50D500C453F7 /* FeedAppiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedAppiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
97993B732BEE565B00C453F7 /* CI_iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CI_iOS.xctestplan; sourceTree = ""; };
@@ -347,7 +353,6 @@
974D6F422BF4DB1300F7211C /* Feed UI */ = {
isa = PBXGroup;
children = (
- 974D6F642BF646AE00F7211C /* Models */,
974D6F442BF4DB3100F7211C /* Views */,
974D6F432BF4DB1E00F7211C /* Controllers */,
974D6F632BF4EF6100F7211C /* Composers */,
@@ -429,13 +434,16 @@
path = Composers;
sourceTree = "";
};
- 974D6F642BF646AE00F7211C /* Models */ = {
+ 974D6F642BF646AE00F7211C /* Feed Presentation */ = {
isa = PBXGroup;
children = (
- 974D6F652BF646BF00F7211C /* FeedViewModel.swift */,
974D6F672BF6575400F7211C /* FeedImageViewModel.swift */,
+ 974D6F6A2BF78D2800F7211C /* FeedPresenter.swift */,
+ 9762D4922BF8A4B200FCCC7E /* FeedImagePresenter.swift */,
+ 9762D4942BF8A5D900FCCC7E /* FeedLoadingViewModel.swift */,
+ 9762D4962BF8A60D00FCCC7E /* FeedViewModel.swift */,
);
- path = Models;
+ path = "Feed Presentation";
sourceTree = "";
};
975F8C912BE06295008489E7 /* FeedStoreSpecs */ = {
@@ -498,6 +506,7 @@
isa = PBXGroup;
children = (
974D6F452BF4DB4200F7211C /* Feed Image Loader */,
+ 974D6F642BF646AE00F7211C /* Feed Presentation */,
974D6F422BF4DB1300F7211C /* Feed UI */,
);
path = FeedAppiOS;
@@ -801,14 +810,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 974D6F6B2BF78D2800F7211C /* FeedPresenter.swift in Sources */,
974D6F5D2BF4E3D000F7211C /* FeedRefreshViewController.swift in Sources */,
+ 9762D4972BF8A60D00FCCC7E /* FeedViewModel.swift in Sources */,
974D6F472BF4DB9900F7211C /* FeedImageDataLoader.swift in Sources */,
+ 9762D4952BF8A5D900FCCC7E /* FeedLoadingViewModel.swift in Sources */,
974D6F412BF3A07B00F7211C /* UIView+Shimmering.swift in Sources */,
+ 9762D4932BF8A4B200FCCC7E /* FeedImagePresenter.swift in Sources */,
9708B1C82BF28E9800D170EA /* FeedViewController.swift in Sources */,
974D6F3F2BF366A700F7211C /* FeedImageCell.swift in Sources */,
974D6F682BF6575400F7211C /* FeedImageViewModel.swift in Sources */,
974D6F602BF4EC9700F7211C /* FeedImageCellController.swift in Sources */,
- 974D6F662BF646BF00F7211C /* FeedViewModel.swift in Sources */,
974D6F622BF4EF5C00F7211C /* FeedUIComposer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/FeedAppDiagram.drawio b/FeedAppDiagram.drawio
index 04085a3..9ef6a84 100644
--- a/FeedAppDiagram.drawio
+++ b/FeedAppDiagram.drawio
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/FeedAppiOS/Feed Presentation/FeedImagePresenter.swift b/FeedAppiOS/Feed Presentation/FeedImagePresenter.swift
new file mode 100644
index 0000000..379b92c
--- /dev/null
+++ b/FeedAppiOS/Feed Presentation/FeedImagePresenter.swift
@@ -0,0 +1,58 @@
+//
+// FeedImagePresenter.swift
+// FeedAppiOS
+//
+// Created by Aram Ispiryan on 18.05.24.
+//
+
+import Foundation
+import FeedApp
+
+protocol FeedImageView {
+ associatedtype Image
+
+ func display(_ model: FeedImageViewModel)
+}
+
+final class FeedImagePresenter 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))
+ }
+}
diff --git a/FeedAppiOS/Feed Presentation/FeedImageViewModel.swift b/FeedAppiOS/Feed Presentation/FeedImageViewModel.swift
new file mode 100644
index 0000000..d4a6bc5
--- /dev/null
+++ b/FeedAppiOS/Feed Presentation/FeedImageViewModel.swift
@@ -0,0 +1,20 @@
+//
+// FeedImageViewModel.swift
+// FeedAppiOS
+//
+// Created by Aram Ispiryan on 16.05.24.
+//
+
+import Foundation
+
+struct FeedImageViewModel {
+ let description: String?
+ let location: String?
+ let image: Image?
+ let isLoading: Bool
+ let shouldRetry: Bool
+
+ var hasLocation: Bool {
+ return location != nil
+ }
+}
diff --git a/FeedAppiOS/Feed Presentation/FeedLoadingViewModel.swift b/FeedAppiOS/Feed Presentation/FeedLoadingViewModel.swift
new file mode 100644
index 0000000..f42d258
--- /dev/null
+++ b/FeedAppiOS/Feed Presentation/FeedLoadingViewModel.swift
@@ -0,0 +1,12 @@
+//
+// FeedLoadingViewModel.swift
+// FeedAppiOS
+//
+// Created by Aram Ispiryan on 18.05.24.
+//
+
+import Foundation
+
+struct FeedLoadingViewModel {
+ let isLoading: Bool
+}
diff --git a/FeedAppiOS/Feed Presentation/FeedPresenter.swift b/FeedAppiOS/Feed Presentation/FeedPresenter.swift
new file mode 100644
index 0000000..97c6036
--- /dev/null
+++ b/FeedAppiOS/Feed Presentation/FeedPresenter.swift
@@ -0,0 +1,39 @@
+//
+// FeedPresenter.swift
+// FeedAppiOS
+//
+// Created by Aram Ispiryan on 17.05.24.
+//
+
+import FeedApp
+
+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))
+ }
+}
diff --git a/FeedAppiOS/Feed Presentation/FeedViewModel.swift b/FeedAppiOS/Feed Presentation/FeedViewModel.swift
new file mode 100644
index 0000000..c137ddf
--- /dev/null
+++ b/FeedAppiOS/Feed Presentation/FeedViewModel.swift
@@ -0,0 +1,12 @@
+//
+// FeedViewModel.swift
+// FeedAppiOS
+//
+// Created by Aram Ispiryan on 18.05.24.
+//
+
+import FeedApp
+
+struct FeedViewModel {
+ let feed: [FeedImage]
+}
diff --git a/FeedAppiOS/Feed UI/Composers/FeedUIComposer.swift b/FeedAppiOS/Feed UI/Composers/FeedUIComposer.swift
index 5716e50..1ed9d83 100644
--- a/FeedAppiOS/Feed UI/Composers/FeedUIComposer.swift
+++ b/FeedAppiOS/Feed UI/Composers/FeedUIComposer.swift
@@ -12,20 +12,112 @@ 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 {
+ 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) {
+ 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, UIImage>(model: model, imageLoader: imageLoader)
+ let view = FeedImageCellController(delegate: adapter)
+ adapter.presenter = FeedImagePresenter(
+ view: WeakRefVirtualProxy(view),
+ imageTransformer: UIImage.init)
- 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))
+ return view
+ }
+ }
+}
+
+private final class FeedLoaderPresentationAdapter: FeedRefreshViewControllerDelegate {
+ 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)
}
}
}
}
+
+private final class FeedImageDataLoaderPresentationAdapter: FeedImageCellControllerDelegate where View.Image == Image {
+ private let model: FeedImage
+ private let imageLoader: FeedImageDataLoader
+ private var task: FeedImageDataLoaderTask?
+
+ var presenter: FeedImagePresenter?
+
+ 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()
+ }
+}
diff --git a/FeedAppiOS/Feed UI/Controllers/FeedImageCellController.swift b/FeedAppiOS/Feed UI/Controllers/FeedImageCellController.swift
index a494a96..c898b40 100644
--- a/FeedAppiOS/Feed UI/Controllers/FeedImageCellController.swift
+++ b/FeedAppiOS/Feed UI/Controllers/FeedImageCellController.swift
@@ -7,45 +7,39 @@
import UIKit
-final class FeedImageCellController {
- private let viewModel: FeedImageViewModel
+protocol FeedImageCellControllerDelegate {
+ func didRequestImage()
+ func didCancelImageRequest()
+}
+
+final class FeedImageCellController: FeedImageView {
+ private let delegate: FeedImageCellControllerDelegate
+ private lazy var cell = FeedImageCell()
- init(viewModel: FeedImageViewModel) {
- self.viewModel = viewModel
+ init(delegate: FeedImageCellControllerDelegate) {
+ self.delegate = delegate
}
func view() -> UITableViewCell {
- let cell = binded(FeedImageCell())
- viewModel.loadImageData()
+ delegate.didRequestImage()
return cell
}
func preload() {
- viewModel.loadImageData()
+ delegate.didRequestImage()
}
func cancelLoad() {
- viewModel.cancelImageDataLoad()
+ delegate.didCancelImageRequest()
}
- private func binded(_ cell: FeedImageCell) -> FeedImageCell {
+ func display(_ viewModel: FeedImageViewModel) {
cell.locationContainer.isHidden = !viewModel.hasLocation
cell.locationLabel.text = viewModel.location
cell.descriptionLabel.text = viewModel.description
- cell.onRetry = viewModel.loadImageData
-
- viewModel.onImageLoad = { [weak cell] image in
- cell?.feedImageView.image = image
- }
-
- viewModel.onImageLoadingStateChange = { [weak cell] isLoading in
- cell?.feedImageContainer.isShimmering = isLoading
- }
-
- viewModel.onShouldRetryImageLoadStateChange = { [weak cell] shouldRetry in
- cell?.feedImageRetryButton.isHidden = !shouldRetry
- }
-
- return cell
+ cell.feedImageView.image = viewModel.image
+ cell.feedImageContainer.isShimmering = viewModel.isLoading
+ cell.feedImageRetryButton.isHidden = !viewModel.shouldRetry
+ cell.onRetry = delegate.didRequestImage
}
}
diff --git a/FeedAppiOS/Feed UI/Controllers/FeedRefreshViewController.swift b/FeedAppiOS/Feed UI/Controllers/FeedRefreshViewController.swift
index 00f9f22..2ab222e 100644
--- a/FeedAppiOS/Feed UI/Controllers/FeedRefreshViewController.swift
+++ b/FeedAppiOS/Feed UI/Controllers/FeedRefreshViewController.swift
@@ -7,28 +7,33 @@
import UIKit
-final class FeedRefreshViewController: NSObject {
- private(set) lazy var view = binded(UIRefreshControl())
- private let viewModel: FeedViewModel
+protocol FeedRefreshViewControllerDelegate {
+ func didRequestFeedRefresh()
+}
+
+final class FeedRefreshViewController: NSObject, FeedLoadingView {
+ private(set) lazy var view = loadView()
+ private let delegate: FeedRefreshViewControllerDelegate
- init(viewModel: FeedViewModel) {
- self.viewModel = viewModel
+ init(delegate: FeedRefreshViewControllerDelegate) {
+ self.delegate = delegate
}
@objc func refresh() {
- viewModel.loadFeed()
+ delegate.didRequestFeedRefresh()
}
-
- private func binded(_ view: UIRefreshControl) -> UIRefreshControl {
- viewModel.onLoadingStateChange = { [weak view] isLoading in
- if isLoading {
- view?.beginRefreshing()
- } else {
- view?.endRefreshing()
- }
+
+ func display(_ viewModel: FeedLoadingViewModel) {
+ if viewModel.isLoading {
+ view.beginRefreshing()
+ } else {
+ view.endRefreshing()
}
+ }
+
+ private func loadView() -> UIRefreshControl {
+ let view = UIRefreshControl()
view.addTarget(self, action: #selector(refresh), for: .valueChanged)
return view
}
}
-
diff --git a/FeedAppiOS/Feed UI/Models/FeedImageViewModel.swift b/FeedAppiOS/Feed UI/Models/FeedImageViewModel.swift
deleted file mode 100644
index 6d4b588..0000000
--- a/FeedAppiOS/Feed UI/Models/FeedImageViewModel.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-//
-// FeedImageViewModel.swift
-// FeedAppiOS
-//
-// Created by Aram Ispiryan on 16.05.24.
-//
-
-import Foundation
-import FeedApp
-
-final class FeedImageViewModel {
- typealias Observer = (T) -> Void
-
- private var task: FeedImageDataLoaderTask?
- private let model: FeedImage
- private let imageLoader: FeedImageDataLoader
- private let imageTransformer: (Data) -> Image?
-
- init(model: FeedImage, imageLoader: FeedImageDataLoader, imageTransformer: @escaping (Data) -> Image?) {
- self.model = model
- self.imageLoader = imageLoader
- self.imageTransformer = imageTransformer
- }
-
- var description: String? {
- return model.description
- }
-
- var location: String? {
- return model.location
- }
-
- var hasLocation: Bool {
- return location != nil
- }
-
- var onImageLoad: Observer?
- var onImageLoadingStateChange: Observer?
- var onShouldRetryImageLoadStateChange: Observer?
-
- func loadImageData() {
- onImageLoadingStateChange?(true)
- onShouldRetryImageLoadStateChange?(false)
- task = imageLoader.loadImageData(from: model.url) { [weak self] result in
- self?.handle(result)
- }
- }
-
- private func handle(_ result: FeedImageDataLoader.Result) {
- if let image = (try? result.get()).flatMap(imageTransformer) {
- onImageLoad?(image)
- } else {
- onShouldRetryImageLoadStateChange?(true)
- }
- onImageLoadingStateChange?(false)
- }
-
- func cancelImageDataLoad() {
- task?.cancel()
- task = nil
- }
-}
diff --git a/FeedAppiOS/Feed UI/Models/FeedViewModel.swift b/FeedAppiOS/Feed UI/Models/FeedViewModel.swift
deleted file mode 100644
index dc77fef..0000000
--- a/FeedAppiOS/Feed UI/Models/FeedViewModel.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// FeedViewModel.swift
-// FeedAppiOS
-//
-// Created by Aram Ispiryan on 16.05.24.
-//
-
-import Foundation
-import FeedApp
-
-final class FeedViewModel {
- private let feedLoader: FeedLoader
- typealias Observer = (T) -> Void
-
- init(feedLoader: FeedLoader) {
- self.feedLoader = feedLoader
- }
-
- var onLoadingStateChange: Observer?
- var onFeedLoad: Observer<[FeedImage]>?
-
- func loadFeed() {
- onLoadingStateChange?(true)
- feedLoader.load { [weak self] result in
- if let feed = try? result.get() {
- self?.onFeedLoad?(feed)
- }
- self?.onLoadingStateChange?(false)
- }
- }
-}