Hello everyone. I encountered a problem while trying to reproduce a real scenario in a simulated project using Combine. The idea is the following: I have a first request to a repository which performs some async task and returns the result via a Future publisher. Based on the results of this request I want to:
open a specific number of streams(publishers),
merge all the streams into one
monitor the stream and transform the values before the final publisher is returned
return the merged stream.
Here is the code:
First the View Model which will make a request to the interactor. The request will return a publisher which will publish a list of results, which will be reflected in the view using the published property(the view will call openCombinedStream via a button press).
class FakeViewModel: ObservableObject {
let interactor = FakeInteractor()
@Published var values: [SearchResult] = []
private var subscriber: AnyCancellable?
func openCombinedStream() {
self.subscriber = self.interactor.provideResults(for: "")
.sink(receiveCompletion: { completion in
}, receiveValue: { newValues in
debugPrint("-----------------------------")
self.values = newValues
debugPrint("-----------------------------")
})
}
}
The view is something like this:
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = FakeViewModel()
var body: some View {
VStack {
Button(action: {
viewModel.openCombinedStream()
}, label: {
Text("Search")
.padding()
})
HStack {
VStack {
Text("Item id")
ForEach(vm.values, id: \.id) { item in
Text("\(item.id)")
}
}
VStack {
Text("Item value")
ForEach(vm.values, id: \.id) { item in
Text("\(item.value)")
}
}
}
}
.padding()
}
}
The interactor has the following implementation:
typealias SearchResult = (value: String, id: Int)
typealias ResultsPublisher = AnyPublisher<[SearchResult], DomainError>
typealias SearchRepoResult = Result<[SearchResult], DomainError>
class FakeInteractor {
let fakeStreamingRepo = FakeStreamingRepo()
let fakeSearchRepo = FakeSearchRepo()
var subscribers: [AnyCancellable] = []
func provideResults(for query: String) -> ResultsPublisher {
let subject = PassthroughSubject<[SearchResult], DomainError>()
queue.async {
let future = self.fakeSearchRepo.provideResultsPublisher(for: "")
let futureSubscriber = future
.sink(receiveCompletion: { _ in }, receiveValue: { newList in
var results = newList
let mergedSubscriber = Publishers.MergeMany(results.map{ item -> AnyPublisher<SearchResult, DomainError> in
return self.fakeStreamingRepo.subscribe(to: item.id)
})
.sink(receiveCompletion: { _ in }, receiveValue: { newValue in
if let index = results.firstIndex(where: { $0.id == newValue.id }) {
results[index] = newValue
subject.send(results)
}
})
self.subscribers.append(mergedSubscriber)
})
self.subscribers.append(futureSubscriber)
}
return subject
.eraseToAnyPublisher()
}
}
enum DomainError: Error { }
let queue = DispatchQueue(label: "")
The Search Repo:
typealias SearchRequest = String
class FakeSearchRepo {
func provideSearchResult(for request: SearchRequest) -> SearchRepoResult {
return .success([(value: "10", id: 1),
(value: "20", id: 2),
(value: "30", id: 3),
(value: "40", id: 4),
(value: "50", id: 5)])
}
func provideResultsPublisher(for request: SearchRequest) -> AnyPublisher<[SearchResult], DomainError> {
Future<[SearchResult], DomainError>({ promise in
let result: SearchRepoResult = self.provideSearchResult(for: request)
switch result {
case .success(let response):
sleep(3)
promise(.success(response))
case .failure(let error):
promise(.failure(error))
}
})
.eraseToAnyPublisher()
}
}
And the streaming repo:
typealias TopicRequest = Int
class FakeStreamingRepo {
func subscribe(to topic: Int) -> AnyPublisher<SearchResult, DomainError> {
let subject = PassthroughSubject<SearchResult, DomainError>()
_ = Timer.scheduledTimer(withTimeInterval: Double.random(in: 0...5), repeats: true) { _ in
switch topic {
case _ where topic == 1:
let value = String(format: "%.2f", Double.random(in: 0...10))
debugPrint("Sending: value for category 1: \((value))")
subject.send((value: value, id: topic))
case _ where topic == 2:
let value = String(format: "%.2f", Double.random(in: 10...20))
debugPrint("Sending: value for category 2: \(value)")
subject.send((value: value, id: topic))
case _ where topic == 3:
let value = String(format: "%.2f", Double.random(in: 20...30))
debugPrint("Sending: value for category 3: \(value)")
subject.send((value: value, id: topic))
case _ where topic == 4:
let value = String(format: "%.2f", Double.random(in: 30...40))
debugPrint("Sending: value for category 4: \(value)")
subject.send((value: value, id: topic))
default:
let value = String(format: "%.2f", Double.random(in: 40...50))
debugPrint("Sending: value for category 5: \(value)")
subject.send((value: value, id: topic))
}
}
return subject.eraseToAnyPublisher()
}
}
The problem is that the publisher returned from the method openCombinedStream does not work unless I specify the runloop to be the main one with .receive(on: RunLoop.main) inside the method before the flatMap. What would be the best approach to this scenario?
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hello everyone. I have encountered a strange bug when working with iOS version checking in SwiftUI. If I use the iOS version checking for a view and the respective view makes direct contact with a safe area border, the background color of the zone outside of that border will be the background color of that view.
This appears to happen only on iOS 15 devices(I experimented on an iPhone 11 simulator).
The following code is an example of such behaviour:
struct ContentView: View {
var body: some View {
if #available(iOS 15.0, *) {
Color.red
.background(Color.blue)
} else {
Color.red
.background(Color.blue)
}
}
}
In this case normally the entire screen should be red and the color of the zone outside of the safe area should be the default one. However if I run this code on an iOS 15 device, the background color of the zone outside the safe area will be blue.
If I would change the code with something like this:
struct ContentView: View {
var body: some View {
VStack {
Text("SwiftUI")
if #available(iOS 15.0, *) {
Color.red
} else {
Color.red
}
Text("Combine")
}
.background(Color.blue)
}
}
the background color of the zone outside the safe area would be the normal one. Also if I remove the version check in the first case, the app would run normally as well. A solution I found is to incorporate the view inside a container and add a padding of 1. Is there a better solution for such cases?