I'm trying to subscribe to a Publisher created from a PassthroughSubject twice and only one of them is executed when PassthroughSubject fires a value.
Here is what I tried:
class Worker {
let stringGeneratorResultSubject: PassthroughSubject<String, Error>
init(stringGeneratorResultSubject: PassthroughSubject<String, Error>) {
self.stringGeneratorResultSubject = stringGeneratorResultSubject
}
func generateString() {
stringGeneratorResultSubject.send("someValue")
}
}
class A {
let workerObj: Worker
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObj: Worker,
workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObj = workerObj
self.workerObjPublisher = workerObjPublisher
super.init()
getString()
}
func getString() {
workerObjPublisher.sink { result in
// do something with result for
}.store(in: &cancellable)
workerObj.generateString()
}
}
class B {
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObjPublisher = workerObjPublisher
super.init()
loadString()
}
func loadString() {
workerObjPublisher.sink { result in
// do something with result
}.store(in: &cancellable)
}
}
class Parent {
lazy var stringGeneratorResultSubject: PassthroughSubject<String, Swift.Error> = .init()
lazy var workerObj: Worker = .init(stringGeneratorResultSubject: stringGeneratorResultSubject)
lazy var aObj: A = .init(workerObj: workerObj,
workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
lazy var bObj: B = .init(workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
_ = bObj
aObj.getString()
}
Only class A's subscription block in getString() is called. class B's subscription block inside loadString() is not executed. Am I missing something?
Related
I am very new in iOS development unit testing.
I have a view model as below
class PostsViewViewModel {
private let serviceRequest: NetworkRequestProtocol
public private(set) var requestOutput: PassthroughSubject<RequestOutput, Never> = .init()
private var cancellables = Set<AnyCancellable>()
init(
request: NetworkRequestProtocol,
user: LoginUserModel,
codeDataManager: CoreDataManagerProtocol) {
serviceRequest = request
loadPostsFromServerFor(user: user)
}
private func loadPostsFromServerFor(user: LoginUserModel) {
Task {
do {
let postsRecived = try await serviceRequest.callService(
with: ServiceEndPoint.fetchPostsForUser(id: user.userid),
model: [PostModel].self,
serviceMethod: .get
)
if postsRecived.isEmpty {
requestOutput.send(.fetchPostsDidSucceedWithEmptyList)
} else {
recievedRawPostsModel = postsRecived
createPostModelsFromPostRecieved(postsRecived)
requestOutput.send(.fetchPostsDidSucceed)
}
} catch {
requestOutput.send(.fetchPostsDidFail)
}
}
}
}
extension PostsViewViewModel {
enum RequestOutput {
case fetchPostsDidFail
case fetchPostsDidSucceed
case fetchPostsDidSucceedWithEmptyList
case reloadPost
}
}
Now I created a test class of ViewModel as below
final class PostViewViewModelTest: XCTestCase {
let userInput: PassthroughSubject<PostsViewViewModel.UserInput, Never> = .init()
private var cancellable = Set<AnyCancellable>()
private let mockUser = LoginUserModel(userid: 1)
private let coreDataManager = CoreDataStackInMemory()
private var sutPostViewModel: PostsViewViewModel!
override func setUp() {
sutPostViewModel = PostsViewViewModel(
request: MockNetworkRequestPostSuccess(),
user: mockUser, codeDataManager: coreDataManager
)
}
override func tearDown() {
sutPostViewModel = nil
}
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
//.receive(on: DispatchQueue.main)
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
}
.store(in: &cancellable)
}
func testPostsViewViewModel_WhenPostModelLoaded_GetPostAtGivenIndexPathMustHaveEqualPostID() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
.receive(on: DispatchQueue.main)
.sink { [weak self] output in
print(output == .reloadPost)
let post = self?.sutPostViewModel.getPost(at: IndexPath(row: 0, section: 0))
let postModel: [PostModel] = JSONLoader.load("Posts.json")
XCTAssertTrue(post.postID == postModel[0].id)
}
.store(in: &cancellable)
}
}
But test cases get crashed while access sutPostViewModel. I am unable to understand what am I doing wrong here.
While debugging I found tearDown() is being called before sink and test crash.
I think you might need to use an expectation.
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
let expectation = expectation(description: "Sink Executed") // 1.
self.sutPostViewModel.requestOutput
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
expectation.fulfill() //2. Fulfill to stop waiting
}
.store(in: &cancellable)
wait(for: [expectation], timeout: 5) // 3. Wait for 5 seconds before timeout and failure
}
You have asynchronous code so sink is executed after your test method runs. At that point tearDown has been called and your sut set to nil
I'm trying to implement a thread-safe array component in the most efficient and safe way, backed by unit tests.
So far, I would prefer a struct array, to keep a value type and not a reference type.
But when I run the test below, I still have random crashes that I don't explain :
Here's my ThreadSafe array class :
public struct SafeArray<T>: RangeReplaceableCollection {
public typealias Element = T
public typealias Index = Int
public typealias SubSequence = SafeArray<T>
public typealias Indices = Range<Int>
private var array: [T]
private var locker = NSLock()
private func lock() { locker.lock() }
private func unlock() { locker.unlock() }
// MARK: - Public methods
// MARK: - Initializers
public init<S>(_ elements: S) where S: Sequence, SafeArray.Element == S.Element {
array = [S.Element](elements)
}
public init() { self.init([]) }
public init(repeating repeatedValue: SafeArray.Element, count: Int) {
let array = Array(repeating: repeatedValue, count: count)
self.init(array)
}
}
extension SafeArray {
// Single action
public func get() -> [T] {
lock(); defer { unlock() }
return Array(array)
}
public mutating func set(_ array: [T]) {
lock(); defer { unlock() }
self.array = Array(array)
}
}
And here's my XCUnitTest code :
final class ConcurrencyTests: XCTestCase {
private let concurrentQueue1 = DispatchQueue.init(label: "concurrentQueue1",
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
private let concurrentQueue2 = DispatchQueue.init(label: "concurrentQueue2",
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
private var safeArray = SafeArray(["test"])
func wait(for expectations: XCTestExpectation, timeout seconds: TimeInterval) {
wait(for: [expectations], timeout: seconds)
}
func waitForMainRunLoop() {
let mainRunLoopExpectation = expectation(description: "mainRunLoopExpectation")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { mainRunLoopExpectation.fulfill() }
wait(for: mainRunLoopExpectation, timeout: 0.5)
}
func waitFor(_ timeout: TimeInterval) {
let mainRunLoopExpectation = expectation(description: "timeoutExpectation")
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { mainRunLoopExpectation.fulfill() }
wait(for: mainRunLoopExpectation, timeout: timeout + 0.5)
}
override func setUpWithError() throws {
try super.setUpWithError()
safeArray = SafeArray(["test"])
}
func testSafeArrayGet() {
var thread1: Thread!
var thread2: Thread!
concurrentQueue1.async {
thread1 = Thread.current
let startTime = Date()
for i in 0...1_000_000 {
self.safeArray.set(["modification"])
print("modification \(i)")
}
print("time modification: \(Date().timeIntervalSince(startTime))")
}
concurrentQueue2.async {
thread2 = Thread.current
let startTime = Date()
for i in 0...1_000_000 {
let _ = self.safeArray.get()
print("read \(i)")
}
print("time read: \(Date().timeIntervalSince(startTime))")
}
waitFor(10)
XCTAssert(!thread1.isMainThread && !thread2.isMainThread)
XCTAssert(thread1 != thread2)
}
}
Edit: Event with a class and a simple approach to make it thread safe, I get a crash. Here's a very simple test that crashes :
class TestClass {
var test = ["test"]
let nsLock = NSLock()
func safeSet(_ string: String) {
nsLock.lock()
test[0] = string // crash
nsLock.unlock()
}
}
func testStructThreadSafety() {
let testClass = TestClass()
DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
testClass.safeSet("modification \(i)")
let _ = testClass.test[0]
}
XCTAssert(true)
}
Why is it crashing? What am I doing wrong?
Note that if I make it a class I don't get crashes, but I would prefer to keep it a struct.
I'm trying to get Publisher which vends Observables to its clients Consumer, to determine when one of its consumers has disposed of its Observable.
Annoyingly. my code was working fine, until I removed an RxSwift .debug from within the Consumer code.
Is there some alternative way I might get this working?
private class Subscriber {
var ids: [Int]
// This property exists so I can watch whether the observable has
// gone nil (which I though would happen when its been disposed, but it
// seems to happen immediately)
weak var observable: Observable<[Updates]>?
}
class Publisher {
private let relay: BehaviorRelay<[Int: Updates]>
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.asObservable()
let subscriber = Subscriber(ids: ids, observable: observable)
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
let updates: [Updates] = ....
// I need to be able to determine at this point whether the subscriber's
// observable has been disposed of, so I can remove it from the list of
// subscribers. However `subscriber.observable` is always nil here.
// PS: I am happy for this to happen before the `repeatTimer` func fires
subscribers.remove(where: { subscriber in
return subscriber.observable == nil
})
relay.accept(updates)
}
}
class Client {
private var disposeBag: DisposeBag?
private let publisher = Publisher()
func startWatching() {
let disposeBag = DisposeBag()
self.disposeBag = disposeBag
publisher
// with the `.debug` below things work OK, without it the
///`Publisher.Subscriber.observable` immediately becomes nil
//.debug("Consumer")
.updatesStream(for: [1, 2, 3])
.subscribe(onNext: { values in
print(values)
})
.disposed(by: disposeBag)
}
func stopWatching() {
disposeBag = nil
}
}
I think this is a very bad idea, but it solves the requested problem... If I had to put this code in one of my projects, I would be very worried about race conditions...
struct Subscriber {
let ids: [Int]
var subscribeCount: Int = 0
let lock = NSRecursiveLock()
}
class Publisher {
private let relay = BehaviorRelay<[Int: Updates]>(value: [:])
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
var subscriber = Subscriber(ids: ids)
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.do(
onSubscribe: {
subscriber.lock.lock()
subscriber.subscribeCount += 1
subscriber.lock.unlock()
},
onDispose: {
subscriber.lock.lock()
subscriber.subscribeCount -= 1
subscriber.lock.unlock()
})
.asObservable()
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
subscribers.removeAll(where: { subscriber in
subscriber.subscribeCount == 0
})
}
}
My expectation is to add observables on-the-fly (eg: images upload), let them start, and, when I finished dynamically enqueueing everything, wait for all observable to be finished.
Here is my class :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
observables.append(observable)
let disposable = observable
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
But when I subscribe to the observable sent by waitForAllObservablesToBeFinished() , all of them are re-executed (which is logic, regarding how Rx works).
How could I warranty that each are executed once, whatever the number of subscription is ?
While writing the question, I got the answer !
By altering the observable through shareReplay(1), and enqueuing and subscribing to this altered observable.. It works !
Here is the updated code :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
let shared = observable.shareReplay(1)
observables.append(shared)
let disposable = shared
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
Is there a way with RxSwift to subscribe to a method which returns a completion block?
Example, let's have this object:
struct Service {
private var otherService = ...
private var initSucceeded = PublishSubject<Bool>()
var initSucceededObservale: Observable<Bool> {
return initSucceeded.asObservable()
}
func init() {
otherService.init {(success) in
self.initSucceeded.onNext( success)
}
}
}
And in a different place have a way to be notified when the service has been initialised:
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
service.init()
Would be there a simpler solution?
I like to use Variables for this sort of thing. Also, I'd recommend using class here because you're tracking unique states and not just concerning yourself with values.
class Service {
private let bag = DisposeBag()
public var otherService: Service?
private var isInitializedVariable = Variable<Bool>(false)
public var isInitialized: Observable<Bool> {
return isInitializedVariable.asObservable()
}
public init(andRelyOn service: Service? = nil) {
otherService = service
otherService?.isInitialized
.subscribe(onNext: { [unowned self] value in
self.isInitializedVariable.value = value
})
.addDisposableTo(bag)
}
public func initialize() {
isInitializedVariable.value = true
}
}
var otherService = Service()
var dependentService = Service(andRelyOn: otherService)
dependentService.isInitialized
.debug()
.subscribe({
print($0)
})
otherService.initialize() // "Initializes" the first service, causing the second to set it's variable to true.
You could use a lazy property:
lazy let initSucceededObservale: Observable<Bool> = {
return Observable.create { observer in
self.otherService.init {(success) in
observer.on(.next(success))
observer.on(.completed)
}
return Disposables.create()
}
}()
and then you can use:
service.init()
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
Let me know in the comments if you have problems before downvoting, thanks.