I am learning Combine on iOS. This is my code:
struct ContentView: View {
let aiRepsonse = "View the latest news and breaking news today for U.S., world, weather, entertainment, politics and health at abc.com."
@State private var cancellableSet = Set<AnyCancellable>()
@State private var content = ""
func test() {
content = ""
aiRepsonse.publisher
.flatMap{
return Just($0).delay(for: 0.01, scheduler: RunLoop.main)}
.sink { _ in
if content.count != aiRepsonse.count{
print("error! lost data! content.count is \(content.count), aiRepsonse.count is \( aiRepsonse.count)")
print("content is : \(content)")
print("aiRepsonse is : \(aiRepsonse)")
}
} receiveValue: { value in
content += String(value)
}
.store(in: &cancellableSet)
}
var body: some View {
VStack {
Text(content)
.frame(height: 200)
Button(action: {
test()
}, label: {
Text("Test")
})
}
}
}
If you press Test button several times in a short time, the error log will appear. For example,
error! lost data! content.count is 104, aiRepsonse.count is 117
content is : View the latest news and breaking news today for U., wol, weather eertainmet, politicsnd heathat ac.com.
aiRepsonse is : View the latest news and breaking news today for U.S., world, weather, entertainment, politics and health at abc.com.
I don't understand why some [Just($0).delay()] publishers do not emit character correctly. Is this a back-pressure problem? Thanks a lot.
Some data is lost because you are using a RunLoop
as the schedular. From this post,
RunLoop.main
runs callbacks only when the main run loop is running in the.default
mode, which is not the mode used when tracking touch and mouse events. If you useRunLoop.main
as a Scheduler, your events will not be delivered while the user is in the middle of a touch or drag.
If you use DispatchQueue.main
, no data will be lost.
Another problem is that test
might be called when the publisher created by the previous call to test
has not completed. This is not much of a problem when the delay is 0.01, but will become a problem when the delay is larger. You will end up having two sink
s appending to content
at the same time, and content
would be longer than expected. This can be solved by cancelling everything in cancellableSet
first, before starting a new publisher.
In practice, it might be more convenient to implement this with async-await instead of Combine. Presumably you are sending prompts to some AI and getting its responses. You can put that code in a task(id: prompt) { ... }
view modifier. Here is a sketch:
@State private var content = ""
@State private var prompt = ""
@State private var text = ""
var body: some View {
VStack {
Text(content)
TextField("Prompt", text: $text)
Button("Send") {
prompt = text
}
}
.task(id: prompt) {
guard !prompt.isEmpty else { return }
await showResponse(forPrompt: prompt)
}
}
func showResponse(forPrompt prompt: String) async {
content = ""
// assuming fetchAIResponse returns a Publisher
let values = fetchAIResponse(prompt: prompt)
.flatMap(maxPublishers: .max(1)) {
return Just($0).delay(for: 0.01, scheduler: DispatchQueue.main)
}.values
for await character in values {
content += String(character)
}
// if you can, I would recommend not using a Publisher at all, and have fetchAIResponse return a AsyncSequence directly.
}