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) - } - } -}