diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 8077629..8d07684 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -13,6 +13,11 @@ 231DA90C2D28B3AE00A50156 /* RemoteFeedLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DA90B2D28B3AB00A50156 /* RemoteFeedLoader.swift */; }; 2370B4092EEC68FE00737DAC /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370B4082EEC68FE00737DAC /* HTTPClient.swift */; }; 2370B40B2EEC6BE200737DAC /* FeedItemsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370B40A2EEC6BE200737DAC /* FeedItemsMapper.swift */; }; + 2377131E2F199AAF00D1122A /* FeedViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */; }; + 237713262F19D19000D1122A /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */; platformFilter = ios; }; + 237713272F19D19000D1122A /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 2377132B2F19D27200D1122A /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FBD0BA2EF0F595005E9351 /* XCTestCase+MemoryLeakTracking.swift */; }; + 2377132D2F1EC9D800D1122A /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2377132C2F1EC94D00D1122A /* FeedViewController.swift */; }; 23883FA52EFC50C100C80E74 /* LocalFeedLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA42EFC50C100C80E74 /* LocalFeedLoader.swift */; }; 23883FA72EFC51FC00C80E74 /* FeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA62EFC51F600C80E74 /* FeedStore.swift */; }; 23883FA92EFF12CE00C80E74 /* RemoteFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA82EFF12CE00C80E74 /* RemoteFeedItem.swift */; }; @@ -59,6 +64,13 @@ remoteGlobalIDString = 080EDEF021B6DA7E00813479; remoteInfo = EssentialFeed; }; + 237713282F19D19000D1122A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 080EDEE821B6DA7E00813479 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 080EDEF021B6DA7E00813479; + remoteInfo = EssentialFeed; + }; 23F5D6E42F182DE900AC7D9F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 080EDEE821B6DA7E00813479 /* Project object */; @@ -82,6 +94,20 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 2377132A2F19D19000D1122A /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 237713272F19D19000D1122A /* EssentialFeed.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EssentialFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 080EDEF521B6DA7E00813479 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -92,6 +118,8 @@ 231DA90B2D28B3AB00A50156 /* RemoteFeedLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedLoader.swift; sourceTree = ""; }; 2370B4082EEC68FE00737DAC /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 2370B40A2EEC6BE200737DAC /* FeedItemsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemsMapper.swift; sourceTree = ""; }; + 2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewControllerTests.swift; sourceTree = ""; }; + 2377132C2F1EC94D00D1122A /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 23883FA42EFC50C100C80E74 /* LocalFeedLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedLoader.swift; sourceTree = ""; }; 23883FA62EFC51F600C80E74 /* FeedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStore.swift; sourceTree = ""; }; 23883FA82EFF12CE00C80E74 /* RemoteFeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedItem.swift; sourceTree = ""; }; @@ -151,6 +179,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 237713262F19D19000D1122A /* EssentialFeed.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -190,6 +219,7 @@ 23FDEB372F089193004D2CD0 /* EssentialFeedCacheIntegrationTests */, 23F5D6F02F182E5600AC7D9F /* EssentialFeediOS */, 23F5D6EF2F182E5600AC7D9F /* EssentialFeediOSTests */, + 237713252F19D19000D1122A /* Frameworks */, 080EDEF221B6DA7E00813479 /* Products */, ); sourceTree = ""; @@ -251,6 +281,13 @@ path = "Feed API"; sourceTree = ""; }; + 237713252F19D19000D1122A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 23883FA32EFC50A400C80E74 /* Feed Cache */ = { isa = PBXGroup; children = ( @@ -267,6 +304,7 @@ isa = PBXGroup; children = ( 23F5D6F12F182FC900AC7D9F /* EssentialFeediOS.xctestplan */, + 2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */, ); path = EssentialFeediOSTests; sourceTree = ""; @@ -274,6 +312,7 @@ 23F5D6F02F182E5600AC7D9F /* EssentialFeediOS */ = { isa = PBXGroup; children = ( + 2377132C2F1EC94D00D1122A /* FeedViewController.swift */, 23F5D6FA2F18345C00AC7D9F /* CI_iOS.xctestplan */, ); path = EssentialFeediOS; @@ -433,10 +472,12 @@ 23F5D6D62F182DE800AC7D9F /* Sources */, 23F5D6D72F182DE800AC7D9F /* Frameworks */, 23F5D6D82F182DE800AC7D9F /* Resources */, + 2377132A2F19D19000D1122A /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 237713292F19D19000D1122A /* PBXTargetDependency */, ); name = EssentialFeediOS; packageProductDependencies = ( @@ -529,6 +570,7 @@ }; 23F5D6E12F182DE900AC7D9F = { CreatedOnToolsVersion = 26.0.1; + LastSwiftMigration = 2600; }; 23FBD0C52EF1BDE2005E9351 = { CreatedOnToolsVersion = 26.0.1; @@ -656,6 +698,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2377132D2F1EC9D800D1122A /* FeedViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -663,6 +706,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2377131E2F199AAF00D1122A /* FeedViewControllerTests.swift in Sources */, + 2377132B2F19D27200D1122A /* XCTestCase+MemoryLeakTracking.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -695,6 +740,12 @@ target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; targetProxy = 080EDEFC21B6DA7E00813479 /* PBXContainerItemProxy */; }; + 237713292F19D19000D1122A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; + targetProxy = 237713282F19D19000D1122A /* PBXContainerItemProxy */; + }; 23F5D6E52F182DE900AC7D9F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 23F5D6D92F182DE800AC7D9F /* EssentialFeediOS */; @@ -870,6 +921,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -901,6 +953,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -924,6 +977,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; @@ -947,6 +1001,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; @@ -957,7 +1012,7 @@ 23F5D6E92F182DE900AC7D9F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -970,7 +1025,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -999,7 +1054,7 @@ 23F5D6EA2F182DE900AC7D9F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -1012,7 +1067,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1042,12 +1097,13 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = YLLF7E9854; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.dapeter.EssentialFeediOSTests; @@ -1057,6 +1113,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1067,12 +1124,13 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = YLLF7E9854; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.dapeter.EssentialFeediOSTests; diff --git a/EssentialFeed/EssentialFeediOS/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/FeedViewController.swift new file mode 100644 index 0000000..d523c5c --- /dev/null +++ b/EssentialFeed/EssentialFeediOS/FeedViewController.swift @@ -0,0 +1,42 @@ +// +// FeedViewController.swift +// EssentialFeed +// +// Created by Andres Sanchez on 19/1/26. +// + +import UIKit +import EssentialFeed + +final public class FeedViewController: UITableViewController { + private var loader: FeedLoader? + private var onViewIsAppearing: ((FeedViewController) -> Void)? + + public convenience init(loader: FeedLoader) { + self.init() + self.loader = loader + } + + public override func viewDidLoad() { + super.viewDidLoad() + + refreshControl = UIRefreshControl() + refreshControl?.addTarget(self, action: #selector(load), for: .valueChanged) + onViewIsAppearing = { vc in + vc.onViewIsAppearing = nil + vc.load() + } + } + + public override func viewIsAppearing(_ animated: Bool) { + super.viewIsAppearing(animated) + onViewIsAppearing?(self) + } + + @objc private func load() { + refreshControl?.beginRefreshing() + loader?.load { [weak self] _ in + self?.refreshControl?.endRefreshing() + } + } +} diff --git a/EssentialFeed/EssentialFeediOSTests/FeedViewControllerTests.swift b/EssentialFeed/EssentialFeediOSTests/FeedViewControllerTests.swift new file mode 100644 index 0000000..2030e7a --- /dev/null +++ b/EssentialFeed/EssentialFeediOSTests/FeedViewControllerTests.swift @@ -0,0 +1,139 @@ +// +// FeedViewControllerTests.swift +// EssentialFeediOSTests +// +// Created by Andres Sanchez on 15/1/26. +// + +import XCTest +import UIKit +import EssentialFeed +import EssentialFeediOS + +final class FeedViewControllerTests: XCTestCase { + + func test_loadFeedActions_requestFeedFromLoader() { + let (sut, loader) = makeSUT() + XCTAssertEqual(loader.loadCallCount, 0, "Expected no loading requests before view appears") + + sut.simulateAppearance() + XCTAssertEqual(loader.loadCallCount, 1, "Expected a loading request once view appears") + + sut.simulateUserInitiatedFeedReload() + XCTAssertEqual(loader.loadCallCount, 2, "Expected another loading request once user initiates a reload") + + sut.simulateUserInitiatedFeedReload() + XCTAssertEqual(loader.loadCallCount, 3, "Expected yet another loading request once user initiates another reload") + } + + func test_loadFeedActions_runsAutomaticallyOnlyOnFirstAppearance() { + let (sut, loader) = makeSUT() + XCTAssertEqual(loader.loadCallCount, 0, "Expected no loading requests before view appears") + + sut.simulateAppearance() + XCTAssertEqual(loader.loadCallCount, 1, "Expected a loading request once view appears") + + sut.simulateAppearance() + XCTAssertEqual(loader.loadCallCount, 1, "Expected no loading request the second time view appears") + + } + + func test_loadingFeedIndicator_isVisibleWhileLoadingFeed() { + let (sut, loader) = makeSUT() + + sut.simulateAppearance() + XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once view appears") + + loader.completeFeedLoading(at: 0) + XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once loading is completed") + + sut.simulateUserInitiatedFeedReload() + XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once user initiates a reload") + + loader.completeFeedLoading(at: 1) + XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once user initiated loading is completed") + } + + // MARK: - Helpers + + private func makeSUT(file: StaticString = #file, line: UInt = #line) -> (sut: FeedViewController, loader: LoaderSpy) { + let loader = LoaderSpy() + let sut = FeedViewController(loader: loader) + trackForMemoryLeaks(loader, file: file, line: line) + trackForMemoryLeaks(sut, file: file, line: line) + return (sut, loader) + } + + class LoaderSpy: FeedLoader { + private var completions = [(FeedLoader.Result) -> Void]() + + var loadCallCount: Int { + return completions.count + } + + func load(completion: @escaping (FeedLoader.Result) -> Void) { + completions.append(completion) + } + + func completeFeedLoading(at index: Int) { + completions[index](.success([])) + } + } + +} + +private extension FeedViewController { + func simulateAppearance() { + if !isViewLoaded { + loadViewIfNeeded() + replaceRefreshControlWithFakeForiOS17PlusSupport() + } + + beginAppearanceTransition(true, animated: false) + endAppearanceTransition() + } + + func replaceRefreshControlWithFakeForiOS17PlusSupport() { + let fakeRefreshControl = FakeUIRefreshControl() + + refreshControl?.allTargets.forEach { target in + refreshControl?.actions(forTarget: target, forControlEvent: .valueChanged)?.forEach { action in + fakeRefreshControl.addTarget(target, action: Selector(action), for: .valueChanged) + } + } + + refreshControl = fakeRefreshControl + } + + private class FakeUIRefreshControl: UIRefreshControl { + private var _isRefreshing = false + + override var isRefreshing: Bool { _isRefreshing } + + override func beginRefreshing() { + _isRefreshing = true + } + + override func endRefreshing() { + _isRefreshing = false + } + } + + func simulateUserInitiatedFeedReload() { + refreshControl?.simulatePullToRefresh() + } + + var isShowingLoadingIndicator: Bool { + return refreshControl?.isRefreshing == true + } +} + +private extension UIRefreshControl { + func simulatePullToRefresh() { + allTargets.forEach { target in + actions(forTarget: target, forControlEvent: .valueChanged)?.forEach { + (target as NSObject).perform(Selector($0)) + } + } + } +} diff --git a/Prototype/Prototype/Base.lproj/Main.storyboard b/Prototype/Prototype/Base.lproj/Main.storyboard index c7bf2ae..30a4b50 100644 --- a/Prototype/Prototype/Base.lproj/Main.storyboard +++ b/Prototype/Prototype/Base.lproj/Main.storyboard @@ -110,6 +110,7 @@ Location + @@ -122,6 +123,12 @@ Location + + + + + + diff --git a/Prototype/Prototype/FeedImageCell.swift b/Prototype/Prototype/FeedImageCell.swift index 6bb48be..b33adfb 100644 --- a/Prototype/Prototype/FeedImageCell.swift +++ b/Prototype/Prototype/FeedImageCell.swift @@ -10,6 +10,7 @@ import UIKit final class FeedImageCell: UITableViewCell { @IBOutlet private(set) var locationContainer: UIView! @IBOutlet private(set) var locationLabel: UILabel! + @IBOutlet private(set) var feedImageContainer: UIView! @IBOutlet private(set) var feedImageView: UIImageView! @IBOutlet private(set) var descriptionLabel: UILabel! @@ -17,23 +18,62 @@ final class FeedImageCell: UITableViewCell { super.awakeFromNib() feedImageView.alpha = 0 + feedImageContainer.startShimmering() } override func prepareForReuse() { super.prepareForReuse() feedImageView.alpha = 0 + feedImageContainer.startShimmering() } func fadeIn(_ image: UIImage?) { feedImageView.image = image UIView.animate( - withDuration: 0.3, - delay: 0.3, + withDuration: 0.25, + delay: 1.25, options: [], animations: { self.feedImageView.alpha = 1 + }, completion: { completed in + if completed { + self.feedImageContainer.stopShimmering() + } }) } } + +private extension UIView { + private var shimmerAnimationKey: String { + return "shimmer" + } + + func startShimmering() { + let white = UIColor.white.cgColor + let alpha = UIColor.white.withAlphaComponent(0.7).cgColor + let width = bounds.width + let height = bounds.height + + let gradient = CAGradientLayer() + gradient.colors = [alpha, white, alpha] + gradient.startPoint = CGPoint(x: 0.0, y: 0.4) + gradient.endPoint = CGPoint(x: 1.0, y: 0.6) + gradient.locations = [0.4, 0.5, 0.6] + gradient.frame = CGRect(x: -width, y: 0, width: width*3, height: height) + layer.mask = gradient + + let animation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.locations)) + animation.fromValue = [0.0, 0.1, 0.2] + animation.toValue = [0.8, 0.9, 1.0] + animation.duration = 1 + animation.repeatCount = .infinity + gradient.add(animation, forKey: shimmerAnimationKey) + } + + func stopShimmering() { + layer.mask = nil + } +} + diff --git a/Prototype/Prototype/FeedViewController.swift b/Prototype/Prototype/FeedViewController.swift index f9651d0..07bd19f 100644 --- a/Prototype/Prototype/FeedViewController.swift +++ b/Prototype/Prototype/FeedViewController.swift @@ -14,7 +14,25 @@ struct FeedImageViewModel { } final class FeedViewController: UITableViewController { - private let feed = FeedImageViewModel.prototypeFeed + private var feed = [FeedImageViewModel]() + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + refresh() + tableView.setContentOffset(CGPoint(x: 0, y: -tableView.contentInset.top), animated: false) + } + + @IBAction func refresh() { + refreshControl?.beginRefreshing() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + if self.feed.isEmpty { + self.feed = FeedImageViewModel.prototypeFeed + self.tableView.reloadData() + } + self.refreshControl?.endRefreshing() + } + } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {