Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions EssentialApp/EssentialApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -532,6 +534,8 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -556,7 +560,9 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EssentialApp.app/EssentialApp";
Expand All @@ -582,6 +588,8 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EssentialApp.app/EssentialApp";
Expand Down
14 changes: 10 additions & 4 deletions EssentialApp/EssentialApp/CombineHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ public extension HTTPClient {

return Deferred {
Future { completion in
task = self.get(from: url, completion: completion)
nonisolated(unsafe) let uncheckedCompletion = completion
task = self.get(from: url, completion: {
uncheckedCompletion($0)
})
}
}
.handleEvents(receiveCancel: { task?.cancel() })
Expand Down Expand Up @@ -229,7 +232,8 @@ extension AnyDispatchQueueScheduler {
if store.contextQueue == .main, Thread.isMainThread {
action()
} else {
store.perform(action)
nonisolated(unsafe) let uncheckedAction = action
store.perform { uncheckedAction() }
}
return AnyCancellable {}
}
Expand All @@ -238,15 +242,17 @@ extension AnyDispatchQueueScheduler {
if store.contextQueue == .main, Thread.isMainThread {
action()
} else {
store.perform(action)
nonisolated(unsafe) let uncheckedAction = action
store.perform { uncheckedAction() }
}
}

func schedule(options: DispatchQueue.SchedulerOptions?, _ action: @escaping () -> Void) {
if store.contextQueue == .main, Thread.isMainThread {
action()
} else {
store.perform(action)
nonisolated(unsafe) let uncheckedAction = action
store.perform { uncheckedAction() }
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions EssentialApp/EssentialApp/CommentsUIComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Combine
import EssentialFeed
import EssentialFeediOS

@MainActor
public final class CommentsUIComposer {
private init() {}

Expand Down Expand Up @@ -38,6 +39,7 @@ public final class CommentsUIComposer {
}
}

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

Expand Down
7 changes: 4 additions & 3 deletions EssentialApp/EssentialApp/FeedUIComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import Combine
import EssentialFeed
import EssentialFeediOS

@MainActor
public final class FeedUIComposer {
private init() {}

private typealias FeedPresentationAdapter = LoadResourcePresentationAdapter<Paginated<FeedImage>, FeedViewAdapter>

public static func feedComposedWith(
feedLoader: @escaping () -> AnyPublisher<Paginated<FeedImage>, Error>,
imageLoader: @escaping (URL) -> FeedImageDataLoader.Publisher,
selection: @escaping (FeedImage) -> Void = { _ in }
feedLoader: @MainActor @escaping () -> AnyPublisher<Paginated<FeedImage>, Error>,
imageLoader: @MainActor @escaping (URL) -> FeedImageDataLoader.Publisher,
selection: @MainActor @escaping (FeedImage) -> Void = { _ in }
) -> ListViewController {
let presentationAdapter = FeedPresentationAdapter(loader: feedLoader)

Expand Down
1 change: 1 addition & 0 deletions EssentialApp/EssentialApp/FeedViewAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UIKit
import EssentialFeed
import EssentialFeediOS

@MainActor
final class FeedViewAdapter: ResourceView {
private weak var controller: ListViewController?
private let imageLoader: (URL) -> FeedImageDataLoader.Publisher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Combine
import EssentialFeed
import EssentialFeediOS

@MainActor
final class LoadResourcePresentationAdapter<Resource, View: ResourceView> {
private let loader: () -> AnyPublisher<Resource, Error>
private var cancellable: Cancellable?
Expand Down
13 changes: 1 addition & 12 deletions EssentialApp/EssentialAppTests/CommentsUIIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EssentialApp
import EssentialFeed
import EssentialFeediOS

@MainActor
class CommentsUIIntegrationTests: XCTestCase {

func test_commentsView_hasTitle() {
Expand Down Expand Up @@ -107,18 +108,6 @@ class CommentsUIIntegrationTests: XCTestCase {
assertThat(sut, isRendering: [comment])
}

func test_loadCommentsCompletion_dispatchesFromBackgroundToMainThread() {
let (sut, loader) = makeSUT()
sut.simulateAppearance()

let exp = expectation(description: "Wait for background queue")
DispatchQueue.global().async {
loader.completeCommentsLoading(at: 0)
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
}

func test_loadCommentsCompletion_rendersErrorMessageOnErrorUntilNextReload() {
let (sut, loader) = makeSUT()

Expand Down
2 changes: 2 additions & 0 deletions EssentialApp/EssentialAppTests/FeedAcceptanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EssentialFeed
import EssentialFeediOS
@testable import EssentialApp

@MainActor
class FeedAcceptanceTests: XCTestCase {

func test_onLaunch_displaysRemoteFeedWhenCustomerHasConnectivity() throws {
Expand Down Expand Up @@ -183,6 +184,7 @@ class FeedAcceptanceTests: XCTestCase {

}

@MainActor
extension CoreDataFeedStore {
static var empty: CoreDataFeedStore {
get throws {
Expand Down
44 changes: 2 additions & 42 deletions EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import EssentialApp
import EssentialFeed
import EssentialFeediOS

@MainActor
class FeedUIIntegrationTests: XCTestCase {

func test_feedView_hasTitle() {
Expand Down Expand Up @@ -137,18 +138,6 @@ class FeedUIIntegrationTests: XCTestCase {
assertThat(sut, isRendering: [image0])
}

func test_loadFeedCompletion_dispatchesFromBackgroundToMainThread() {
let (sut, loader) = makeSUT()
sut.simulateAppearance()

let exp = expectation(description: "Wait for background queue")
DispatchQueue.global().async {
loader.completeFeedLoading(at: 0)
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
}

func test_loadFeedCompletion_rendersErrorMessageOnErrorUntilNextReload() {
let (sut, loader) = makeSUT()

Expand Down Expand Up @@ -225,20 +214,6 @@ class FeedUIIntegrationTests: XCTestCase {
XCTAssertFalse(sut.isShowingLoadMoreFeedIndicator, "Expected no loading indicator once user initiated loading completes with error")
}

func test_loadMoreCompletion_dispatchesFromBackgroundToMainThread() {
let (sut, loader) = makeSUT()
sut.simulateAppearance()
loader.completeFeedLoading(at: 0)
sut.simulateLoadMoreFeedAction()

let exp = expectation(description: "Wait for background queue")
DispatchQueue.global().async {
loader.completeLoadMore()
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
}

func test_loadMoreCompletion_rendersErrorMessageOnError() {
let (sut, loader) = makeSUT()
sut.simulateAppearance()
Expand Down Expand Up @@ -545,21 +520,6 @@ class FeedUIIntegrationTests: XCTestCase {
XCTAssertNil(view?.renderedImage, "Expected no rendered image when an image load finishes after the view is not visible anymore")
}

func test_loadImageDataCompletion_dispatchesFromBackgroundToMainThread() {
let (sut, loader) = makeSUT()

sut.simulateAppearance()
loader.completeFeedLoading(with: [makeImage()])
_ = sut.simulateFeedImageViewVisible(at: 0)

let exp = expectation(description: "Wait for background queue")
DispatchQueue.global().async {
loader.completeImageLoading(with: self.anyImageData(), at: 0)
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
}

func test_feedImageView_doesNotLoadImageAgainUntilPreviousRequestCompletes() {
let image = makeImage(url: URL(string: "http://url-0.com")!)
let (sut, loader) = makeSUT()
Expand Down Expand Up @@ -589,7 +549,7 @@ class FeedUIIntegrationTests: XCTestCase {
// MARK: - Helpers

private func makeSUT(
selection: @escaping (FeedImage) -> Void = { _ in },
selection: @MainActor @escaping (FeedImage) -> Void = { _ in },
file: StaticString = #filePath,
line: UInt = #line
) -> (sut: ListViewController, loader: LoaderSpy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Combine

extension FeedUIIntegrationTests {

@MainActor
class LoaderSpy {

// MARK: - FeedLoader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import XCTest

extension XCTestCase {
@MainActor
func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
addTeardownBlock { [weak instance] in
XCTAssertNil(instance, "Instance should have been deallocated. Potential memory leak.", file: file, line: line)
Expand Down
1 change: 1 addition & 0 deletions EssentialApp/EssentialAppTests/SceneDelegateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import XCTest
import EssentialFeediOS
@testable import EssentialApp

@MainActor
class SceneDelegateTests: XCTestCase {

func test_configureWindow_setsWindowAsKeyAndVisible() throws {
Expand Down
26 changes: 26 additions & 0 deletions EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,9 @@
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -1577,6 +1579,8 @@
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1600,7 +1604,9 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1624,6 +1630,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1646,6 +1654,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1668,6 +1678,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1690,6 +1702,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1712,6 +1726,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -1745,7 +1761,10 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand Down Expand Up @@ -1779,6 +1798,9 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
Expand All @@ -1805,7 +1827,9 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
Expand All @@ -1831,6 +1855,8 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
Expand Down
Loading