AsyncStream spams view, where AsyncPublisher does not - swift

I'm running into a behavior with AsyncStream I don't quite understand.
When I have an actor with a published variable, I can "subscribe" to it via an AsyncPublisher and it behaves as expected, updating only when there is a change in value. If I create an AsyncStream with a synchronous context (but with a potential task retention problem) it also behaves as expected.
The weirdness happens when I try to wrap that publisher in an AsyncStream with an asyncronous context. It starts spamming the view with an update per loop it seems, NOT only when there is a change.
What am I missing about the AsyncStream.init(unfolding:oncancel:) which is causing this behavior?
https://developer.apple.com/documentation/swift/asyncstream/init(unfolding:oncancel:)?
import Foundation
import SwiftUI
actor TestService {
static let shared = TestService()
#MainActor #Published var counter:Int = 0
#MainActor public func updateCounter(by delta:Int) async {
counter = counter + delta
}
public func asyncStream() -> AsyncStream<Int> {
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
for await n in $counter.values {
//print("\(location)")
return n
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm counter got canceled")
}
}
public func syncStream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
TestActorButton()
HStack {
//TestActorViewA() //<-- uncomment at your own risk.
TestActorViewB()
TestActorViewC()
}
}
.padding()
}
}
struct TestActorButton:View {
var counter = TestService.shared
var body: some View {
Button("increment counter") {
Task { await counter.updateCounter(by: 2) }
}
}
}
struct TestActorViewA:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Fires constantly.
for await value in await counter.asyncStream() {
print("View A Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewB:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Behaves like one would expect. Fires once per change.
for await value in await counter.$counter.values {
print("View B Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewC:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Also only fires on update
for await value in await counter.syncStream() {
print("View C Value: \(value)")
counterVal = value
}
}
}
}

The real solution to wrapping a publisher appears to be to stick to the synchronous context initializer and have it cancel it's own task:
public func stream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
//do hard work to transform n
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
From what I can tell the "unfolding" style initializer for AsyncStream is simply not a fit for wrapping an AsyncPublisher. The "unfolding" function will "pull" at the published value from within the stream, so the stream will just keep pushing values from that infinite well.
It seems like the "unfolding" style initializer is best used when processing a finite (but potentially very large) list of items, or when generating ones values from scratch... something like:
struct NumberQueuer {
let numbers:[Int]
public func queueStream() -> AsyncStream<Int> {
var iterator = AsyncArray(values: numbers).makeAsyncIterator()
print("Queue called")
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
do {
if let item = try await iterator.next() {
return item
}
} catch let error {
print(error.localizedDescription)
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm NumberQueue got canceled")
}
}
}
public struct AsyncArray<Element>: AsyncSequence, AsyncIteratorProtocol {
let values:[Element]
let delay:TimeInterval
var currentIndex = -1
public init(values: [Element], delay:TimeInterval = 1) {
self.values = values
self.delay = delay
}
public mutating func next() async throws -> Element? {
currentIndex += 1
guard currentIndex < values.count else {
return nil
}
try await Task.sleep(nanoseconds: UInt64(delay * 1E09))
return values[currentIndex]
}
public func makeAsyncIterator() -> AsyncArray {
self
}
}
One can force the unfolding type to work with an #Published by creating a buffer array that is checked repeatedly. The variable wouldn't actually need to be #Published anymore. This approach has a lot of problems but it can be made to work. If interested, I put it in a repo with a bunch of other AsyncStream examples. https://github.com/carlynorama/StreamPublisherTests
This article was very helpful to sorting this out: https://www.raywenderlich.com/34044359-asyncsequence-asyncstream-tutorial-for-ios
As was this video: https://www.youtube.com/watch?v=UwwKJLrg_0U

Related

SwiftUI unwrapping optionals inside View (pyramid of doom)

I have a sample program that does three things
Generate a random integer from -10...10 (regular function)
Generate 10 million random numbers from -10...10 (asynchronous function)
Calculate the average of #2 (throwing asynchronous function)
Below is the full working code. It works without errors, but the view has a horrible readability with three nested if/let loops. What's the best way/convention to get rid of the pyramid of doom in this scenario?
Result screenshot (how it should work)
Working code (methods)
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
func calculateAverageNumber(for numbers: [Double]) async throws {
guard !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
}
Working code (view)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
if let integer = numberManager.integer {
if let numbers = numberManager.numbers {
if let average = numberManager.average {
Text("Integer is \(integer)")
Text("First number is: \(numbers[0])")
Text("Average is: \(average)")
} else {
LoadingView(loadingType: "Calculating average")
.task {
do {
try await numberManager.calculateAverageNumber(for: numbers)
} catch {
print("empty numbers array")
}
}
}
} else {
LoadingView(loadingType: "Generating numbers")
.task {
await numberManager.generateNumbers()
}
}
} else {
LoadingView(loadingType: "Generating int")
.task {
numberManager.generateInt()
}
}
}
}
What I tried so far...
I tried building helper functions to build views as below, and called those functions that returns views inside my ContentView. When I run it, the integer and the number array gets generated and shows, but the last task that calculates the average does not get called again at all.
Result screenshot(with issues)
Code (Runs without errors. But the last task that calculates average doesn't get executed)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
intergerView()
.task {
print("Generating Int")
numberManager.generateInt()
}
numbersView()
.task {
print("Generating Numbers")
await numberManager.generateNumbers()
}
averageView()
.task {
do {
print("Calculating Average")
try await numberManager.calculateAverageNumber(for: numberManager.numbers ?? [])
} catch {
print("error")
}
}
}
}
private func intergerView() -> some View {
guard let integer = numberManager.integer else {
return AnyView(LoadingView(loadingType: "Generating int"))
}
return AnyView(Text("Integer is \(integer)"))
}
private func numbersView() -> some View {
guard let numbers = numberManager.numbers else {
return AnyView(LoadingView(loadingType: "Generating numbers"))
}
return AnyView(Text("First number is: \(numbers[0])"))
}
private func averageView() -> some View {
guard let average = numberManager.average else {
return AnyView(LoadingView(loadingType: "Calculating average"))
}
return AnyView(Text("Average is: \(average)"))
}
EDIT: In my app, I have a view that does all different functions in one view (it's like a dashboard). Some require others to run first (like calculating the average), whereas some can run on its own (Like generating one random integer). I want to display whatever that's loaded first, while displaying a loadingview placeholder for parts that aren't loaded yet.
Several issues here:
generateNumbers and calculateAverageNumber depend on each other. So they need to await each other.
your "working code" does not match the description of your code. You say you want to show what ever finishes first but your if/else statements introduce dependencies between all 3 functions/views
you donĀ“t need 3 different views. One that can be customized should be enough.
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
// No need for arguments here
func calculateAverageNumber() async throws {
guard let numbers = numbers, !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
//This function will handle the dependenies of generating the values and calculating the avarage
func calculateNumbersAndAvarage() async throws{
await generateNumbers()
try await calculateAverageNumber()
}
}
The View:
struct ContentView: View{
#StateObject private var numberManager = NumberManager()
var body: some View{
//Show the different detail views.
VStack{
Spacer()
Spacer()
DetailView(text: numberManager.integer != nil ? "Integer is \(numberManager.integer!)" : nil)
.onAppear {
numberManager.generateInt()
}
Spacer()
Group{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
Spacer()
DetailView(text: numberManager.average != nil ? "Average is: \(numberManager.average!)" : nil)
}.onAppear {
Task{
do{
try await numberManager.calculateNumbersAndAvarage()
}
catch{
print("error")
}
}
}
Spacer()
Spacer()
}
}
//Just to make it more readable
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
}
}
and the DetailView:
struct DetailView: View{
let text: String?
var body: some View{
// If no text to show, show `ProgressView`, or `LoadingView` in your case. You can inject the view directly or use a property for the String argument.
if let text = text {
Text(text)
.font(.headline)
.padding()
} else{
ProgressView()
}
}
}
The code should speak for itself. If you have any further question regarding this code please feel free to do so, but please read and try to understand how this works first.
Edit:
This does not wait for calculateAverageNumber to finish before displaying numbers[0]. The reason for it showing at the same time is that it takes almost no time to calculat the avarage. Try adding this between the 2 functions in calculateNumbersAndAvarage.
try await Task.sleep(nanoseconds: 4_000_000_000)
and you will see that it shows as it should.
You are almost done. Use #ViewBuilder , remove AnyView wrapper and dont use guard
#ViewBuilder
var intergerView: some View {
if let integer = numberManager.integer {
LoadingView(loadingType: "Generating int")
} else {
Text("Integer is \(integer)")
}
}

GlobalActor directive doesn't guarantee a function will be called on that actor

Assuming I have defined a global actor:
#globalActor actor MyActor {
static let shared = MyActor()
}
And I have a class, in which a couple of methods need to act under this:
class MyClass {
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager?.registerUndo(withTarget: self) {
$0.reverseSomething(undoManager: UndoManager)
}
}
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true when called from undo stack
undoManager?.registerUndo(withTarget: self) {
$0.doSomething(undoManager: UndoManager)
}
}
}
Assume the code gets called from a SwiftUI view:
struct MyView: View {
#Environment(\.undoManager) private var undoManager: UndoManager?
let myObject: MyClass
var body: some View {
Button("Do something") { myObject.doSomething(undoManager: undoManager) }
}
}
Note that when the action is undone the 'reversing' func it is called on the MainThread. Is the correct way to prevent this to wrap the undo action in a task? As in:
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true
undoManager?.registerUndo(withTarget: self) {
Task { $0.doSomething(undoManager: UndoManager) }
}
}
I am surprised that the compiler does not generate a warning about calling a global actor 'MyActor'-isolated instance method in a synchronous nonisolated context (i.e. the closure). It would appear that the compiler is confused by the closure syntax within an actor isolated method.
Anyway, you can wrap it in a Task and it should run that on the appropriate actor:
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager.registerUndo(withTarget: self) { target in
Task { #MyActor in
target.reverseSomething(undoManager: undoManager)
}
}
}
That having been said, I have found erratic UndoManager behavior when using it from a background thread (i.e., not on the main actor).
So, especially because undo/redo is behavior generally initiated from the UI (on the main thread), I would keep it on the main thread, and only run the desired work on another actor. E.g.:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#globalActor actor MyGlobalActor {
static let shared = MyGlobalActor()
}
#MainActor
class ViewModel: ObservableObject {
#MyGlobalActor
var values: [String] = []
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.append(newValue)
print(#function, values)
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.removeLast()
print(#function, values)
}
}
}
Now, this is a somewhat contrived example (for something this simple, we wouldn't have a simply array on a global actor), but hopefully it illustrates the idea.
Or, you can use a non-global actor:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#MainActor
class ViewModel: ObservableObject {
var model = Model()
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task {
await model.append(newValue)
await print(#function, model.values())
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task {
await model.removeLast()
await print(#function, model.values())
}
}
}
actor Model {
private var strings: [String] = []
func append(_ string: String) {
strings.append(string)
}
func removeLast() {
strings.removeLast()
}
func values() -> [String] {
strings
}
}

Swift AsyncStream variable, use after finish()

I'm currently reading about AsyncStreams and as I understand it, they are best for tasks that produce some results over time, but have a lifespan - e.g. start and end. Here's an example I am playing with:
struct AHardWorkingStruct {
lazy var updates = AsyncStream<String> { continuation in
onProgress = { value in
continuation.yield(value)
}
onFinish = { value in
continuation.yield(value)
continuation.finish()
}
}
private var onProgress: (String) -> Void = { _ in () }
private var onFinish: (String) -> Void = { _ in () }
func doSomeWork() async {
let numbers = (0..<20).map { anInt in
anInt^2
}
for number in numbers {
onProgress("The number is \(number)")
if(number == 20) {
onFinish("I'm DONE!")
}
}
}
}
I then have AHardWorkingStruct as a property in my view controller and use it like so:
class MyViewController: UIViewController {
var myHardWorker = AHardWorkingStruct()
#IBAction func tapToRun(_ sender: UIButton) {
Task {
await myHardWorker.doSomeWork()
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
for await stream in myHardWorker.updates {
print(stream)
}
}
}
This works perfectly when I tap the button.
However, once I call finish() on the stream, I no longer get the updates from it - which I understand is expected behaviour.
My question is, how can I keep getting updates from a stream that is a variable on a long-lived object? Is there a "reset"? (other than getting rid of the lazy and nilling the variable)
I also have a 2nd part to this - what would be the best way to unit test an asyncstream? I've tried using expectations, but not sure if this is right.

How to reduce the memory footprint of a large list of images in SwiftUI

I'm playing with SwiftUI and I'm currently struggling with images. Basically, I want to display an list of images with an infinite scroll but I want to keep the memory usage reasonable.
I have the following (truncated) code:
struct HomeView: View {
#State private var wallpapers: Loadable<[Wallpaper]> = .notRequested
#State private var currentPage = 1
#Environment(\.container) private var container
var body: some View {
content
.onAppear { loadWallpapers() }
}
private var content: some View {
VStack {
wallpapersList(data: wallpapers.value ?? [])
// ...
}
}
private func wallpapersList(data: [Wallpaper]) -> some View {
ScrollView {
LazyVStack(spacing: 5) {
ForEach(data) { w in
networkImage(url: w.thumbs.original)
.onAppear { loadNextPage(current: w.id) }
}
}
}
}
private func networkImage(url: String) -> some View {
// I use https://github.com/onevcat/Kingfisher to handle image loading
KFImage(URL(string: url))
// ...
}
private func loadWallpapers() {
container.interactors.wallpapers.load(data: $wallpapers, page: currentPage)
}
private func loadNextPage(current: String) {
// ...
}
}
struct WallpapersInteractor: PWallpapersInteractor {
let state: Store<AppState>
let agent: PWallpapersAgent
func load(data: LoadableSubject<[Wallpaper]>, page: Int) {
let store = CancelBag()
data.wrappedValue.setLoading(store: store)
Just.withErrorType((), Error.self)
.flatMap { _ in
agent.loadWallpapers(page: page) // network call here
}
.map { response in
response.data
}
.sink { subCompletion in
if case let .failure(error) = subCompletion {
data.wrappedValue.setFailed(error: error)
}
} receiveValue: {
if var currentWallpapers = data.wrappedValue.value {
currentWallpapers.append(contentsOf: $0) // /!\
data.wrappedValue.setLoaded(value: currentWallpapers)
} else {
data.wrappedValue.setLoaded(value: $0)
}
}
.store(in: store)
}
}
Because I append the new data to my Binding every time I request a new batch of images, the memory consumption quickly becomes stupidly high.
I tried to remove data from the array using .removeFirst(pageSize) once I get to the third page so that my array contains at most 2 * pageSize elements (pageSize being 64 in this case). But doing so makes my list all jumpy because the content goes up, which creates more problems than it solves.
I tried searching for a solution but I surprisingly didn't find anything on this particular topic, am I missing something obvious ?

Swift computed property templates?

Is there a way to template computed properties to avoid repeating the same code over and over? For example, right now I have a class with a block of code that looks like this:
private var _state:State?
private var _maxs:State?
private var _state1s:State?
private var _state10s:State?
var state:State? {
get {
dispatch_semaphore_wait(statephore, DISPATCH_TIME_FOREVER)
let s=_state
dispatch_semaphore_signal(statephore)
return s
}
set {
dispatch_semaphore_wait(statephore, DISPATCH_TIME_FOREVER)
_state=newValue
dispatch_semaphore_signal(statephore)
if newValue != nil {statsTest(newValue!)}
}
}
var maxs:State? {
get {
dispatch_semaphore_wait(maxphore, DISPATCH_TIME_FOREVER)
let m=_maxs
dispatch_semaphore_signal(maxphore)
return m
}
set {
dispatch_semaphore_wait(maxphore, DISPATCH_TIME_FOREVER)
_maxs=newValue
dispatch_semaphore_signal(maxphore)
}
}
var state1s:State? {
get {
dispatch_semaphore_wait(state1sphore, DISPATCH_TIME_FOREVER)
let s=_state1s
dispatch_semaphore_signal(state1sphore)
return s
}
set {
dispatch_semaphore_wait(state1sphore, DISPATCH_TIME_FOREVER)
_state1s=newValue
dispatch_semaphore_signal(state1sphore)
}
}
var state10s:State? {
get {
dispatch_semaphore_wait(state10sphore, DISPATCH_TIME_FOREVER)
let s=_state10s
dispatch_semaphore_signal(state10sphore)
return s
}
set {
dispatch_semaphore_wait(state10sphore, DISPATCH_TIME_FOREVER)
_state10s=newValue
dispatch_semaphore_signal(state10sphore)
}
}
There's an obvious pattern here, and all the repeated code just obfuscates what's happening and has led to errors as I cut/paste/edit/fail. Is there a way I can capture this pattern, and then define my properties with something like:
var state=ProtectedValue(_state,statephore)
?
This looks like a job for generics and inout variables.
func setProtectedValue<T>(inout destination: T, newValue: T, semaphore: SemaphoreType) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
destination = newValue
dispatch_semaphore_signal(semaphore)
}
at the call site:
var state10s:State? {
get {
//...
}
set {
setProtectedValue(&_state10s, newValue, state10sphore)
}
}