Combine - subscribe to a publisher of PassthroughSubject twice - ios

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

Swift XC UnitTest of PassthrowSubject is getting crashed while access sut object

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

Swift: Struct thread safe array crashing with NSLock

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.

RxSwift - Determining whether an Observable has been disposed

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

How to wait for all observable within a mutable array to finish in RxSwift

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

RxSwift - subscribe to a method

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.

Resources