diff --git a/Documentation/loader.md b/Documentation/loader.md index 5b32f1f..e0a5347 100644 --- a/Documentation/loader.md +++ b/Documentation/loader.md @@ -129,6 +129,34 @@ final class ProfileViewController: UIViewController { } ``` +## Observing loading state via Combine + +To keep track of the loader state via Combine use `statePublisher`. + +```swift +final class ProfileViewController: UIViewController { + private var bag = Set() + ... + override func viewDidLoad() { + super.viewDidLoad() + userProfileLoader.statePublisher.sink { [weak self] newState in + guard let self = self else { return } + + switch newState { + case .initial: + // + case .loading(let cache): + // + case .success(let content): + // + case .failure(let error, let cache): + // + } + }.store(in: &bag) + } +} +``` + ## Use cases ApexyLoader used in the following scenarios: diff --git a/Documentation/loader_ru.md b/Documentation/loader_ru.md index 9987037..2bbb7bc 100644 --- a/Documentation/loader_ru.md +++ b/Documentation/loader_ru.md @@ -128,6 +128,34 @@ final class ProfileViewController: UIViewController { } ``` +## Отслеживание состояния загрузки через Combine + +Чтобы следить за состоянием загрузчика с помощью Combine используйте паблишер `statePublisher`. + +```swift +final class ProfileViewController: UIViewController { + private var bag = Set() + ... + override func viewDidLoad() { + super.viewDidLoad() + userProfileLoader.statePublisher.sink { [weak self] newState in + guard let self = self else { return } + + switch newState { + case .initial: + // + case .loading(let cache): + // + case .success(let content): + // + case .failure(let error, let cache): + // + } + }.store(in: &bag) + } +} +``` + ## Сценарии использования ApexyLoader применяется когда: diff --git a/Sources/ApexyLoader/ContentLoader.swift b/Sources/ApexyLoader/ContentLoader.swift index c875f63..921ad98 100644 --- a/Sources/ApexyLoader/ContentLoader.swift +++ b/Sources/ApexyLoader/ContentLoader.swift @@ -1,3 +1,6 @@ +#if canImport(Combine) +import Combine +#endif import Foundation private final class StateChangeHandler { @@ -28,11 +31,24 @@ open class ContentLoader: ObservableLoader { public var state: LoadingState = .initial { didSet { stateHandlers.forEach { $0.notify() } + + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + stateSubject.send(state) + } } } + @available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) + private lazy var stateSubject = CurrentValueSubject, Never>(.initial) + + /// Content loading status. The default value is `.initial`. + /// + /// - Remark: To change state use `update(_:)`. + @available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) + public lazy var statePublisher: AnyPublisher, Never> = stateSubject.eraseToAnyPublisher() + public init() {} - + // MARK: - ObservableLoader /// Starts state observing. diff --git a/Tests/ApexyLoaderTests/ContentLoaderTests.swift b/Tests/ApexyLoaderTests/ContentLoaderTests.swift index ce5e94e..7be865a 100644 --- a/Tests/ApexyLoaderTests/ContentLoaderTests.swift +++ b/Tests/ApexyLoaderTests/ContentLoaderTests.swift @@ -1,4 +1,5 @@ @testable import ApexyLoader +import Combine import XCTest final class ContentLoaderTests: XCTestCase { @@ -6,7 +7,10 @@ final class ContentLoaderTests: XCTestCase { private var contentLoader: ContentLoader! private var numberOfChanges = 0 private var observation: LoaderObservation! - + + private var bag = Set() + private var receivedValues = [LoadingState]() + override func setUp() { super.setUp() @@ -15,7 +19,13 @@ final class ContentLoaderTests: XCTestCase { observation = contentLoader.observe { [weak self] in self?.numberOfChanges += 1 } - + + receivedValues.removeAll() + + contentLoader.statePublisher.sink(receiveCompletion: { _ in }) { loadingState in + self.receivedValues.append(loadingState) + }.store(in: &bag) + XCTAssertTrue( contentLoader.observations.isEmpty, "No observation of other loaders") @@ -32,6 +42,15 @@ final class ContentLoaderTests: XCTestCase { numberOfChanges, 0, "The change handler didn‘t triggered because the observation was canceled") } + + func testCancelObservationCombine() { + bag.removeAll() + contentLoader.state = .success(content: 10) + XCTAssertEqual( + receivedValues, + [.initial], + "The change handler didn‘t triggered because the observation was canceled") + } func testStartLoading() { XCTAssertTrue( @@ -54,12 +73,29 @@ final class ContentLoaderTests: XCTestCase { numberOfChanges, 1, "The change handler did NOT triggered") } + + func testStartLoadingCombine() { + XCTAssertTrue( + contentLoader.startLoading(), + "Loading has begun") + XCTAssertEqual( + receivedValues, + [.initial, .loading(cache: nil)], + "State of the loader must be loading") + XCTAssertFalse( + contentLoader.startLoading(), + "The second loading didn‘t start before the end of the first one.") + XCTAssertEqual( + receivedValues, + [.initial, .loading(cache: nil)], + "The load status has NOT changed") + } func testFinishLoading() { contentLoader.finishLoading(.success(12)) XCTAssertTrue( contentLoader.state == .success(content: 12), - "Succesfull loading state") + "Successfully loading state") XCTAssertEqual( numberOfChanges, 1, "The change handler triggered") @@ -73,6 +109,24 @@ final class ContentLoaderTests: XCTestCase { numberOfChanges, 2, "The handler triggered") } + + func testFinishLoadingCombine() { + contentLoader.finishLoading(.success(12)) + XCTAssertEqual( + receivedValues, + [.initial, .success(content: 12)], + "Successfully loading state") + + receivedValues.removeAll() + + let error = URLError(.networkConnectionLost) + contentLoader.finishLoading(.failure(error)) + + XCTAssertEqual( + receivedValues, + [.failure(error: error, cache: 12)], + "The state must me failure with cache") + } func testUpdate() { contentLoader.update(.initial) @@ -90,4 +144,24 @@ final class ContentLoaderTests: XCTestCase { numberOfChanges, 1, "The state didn't changed and the handler didn't triggered") } + + func testUpdateCombine() { + contentLoader.update(.initial) + XCTAssertEqual( + receivedValues, + [.initial], + "The state didn't change and the handler didn't triggered") + + contentLoader.update(.success(content: 1)) + XCTAssertEqual( + receivedValues, + [.initial, .success(content: 1)], + "The state changed and the handler triggered") + + contentLoader.update(.success(content: 1)) + XCTAssertEqual( + receivedValues, + [.initial, .success(content: 1)], + "The state didn't changed and the handler didn't triggered") + } }