I'm attempting to figure out how to display a message to my users when some asynchronous code takes some time to run. So far I've used a sample I found online to create a popup banner and tied the message together using an ObservedObject of the async method on my view and then Publish the values from my async method.
My sample code project is on a public GitHub repository here and I'll post the code at the bottom.
Right now I have an issue when setting the variables from the async method: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Solutions online seem to fix this issue by updating the value on the @mainActor thread but I want these methods to run asynchronously AND update the user on what's happening. What's the best way to update my variables from this location?
CODE
in the main app:
var body: some Scene {
WindowGroup {
ContentView(asyncmethod: myAsyncViewModel())
}
}
ContentView:
import SwiftUI
struct ContentView: View {
@State private var isLoaderPresented = false
@State private var isTopMessagePresented = false
@ObservedObject var asyncmethod: myAsyncViewModel
var body: some View {
VStack {
Spacer()
Button( action: {
Task {
isTopMessagePresented = true
let response = await asyncmethod.thisMethodTakesTime()
// Want to return a string or object so I know what happens.
print("Response Loader: \(response ?? "no response")")
isTopMessagePresented = false
}
},
label: { Text("Run Top Banner Code") }
)
Spacer()
}
.foregroundColor(.black)
.popup(isPresented: isTopMessagePresented, alignment: .top, direction: .top, content: {
Snackbar(showForm: $isTopMessagePresented, asyncmethod: asyncmethod)
})
}
}
struct Snackbar: View {
@Binding var showForm: Bool
@ObservedObject var asyncmethod: myAsyncViewModel
var body: some View {
VStack {
HStack() {
Image(systemName: asyncmethod.imageName)
.resizable()
.aspectRatio(contentMode: ContentMode.fill)
.frame(width: 40, height: 40)
Spacer()
VStack(alignment: .center, spacing: 4) {
Text(asyncmethod.title)
.foregroundColor(.black)
.font(.headline)
Text(asyncmethod.subTitle)
.font(.body)
.foregroundColor(.black)
.frame(maxWidth: .infinity)
}
}
.frame(minWidth: 200)
}
.padding(15)
.frame(maxWidth: .infinity, idealHeight: 100)
.background(Color.black.opacity(0.1))
}
}
My async sample method:
import Foundation
class myAsyncViewModel: ObservableObject {
@Published var imageName: String = "questionmark"
@Published var title: String = "title"
@Published var subTitle: String = "subtitle"
func thisMethodTakesTime() async -> String? {
print("In method: \(imageName), \(title), \(subTitle)")
title = "MY METHOD"
subTitle = "Starting out!"
print("In method. Starting \(title)")
subTitle = "This is the message"
print("Sleeping")
try? await Task.sleep(nanoseconds: 1_000_000_000)
subTitle = "Between"
try? await Task.sleep(nanoseconds: 1_000_000_000)
print("After sleep. Ending")
subTitle = "About to return. Success!"
print("In method: \(imageName), \(title), \(subTitle)")
return "RETURN RESULT"
}
}
And the supporting file for the popup:
import SwiftUI
struct Popup<T: View>: ViewModifier {
let popup: T
let isPresented: Bool
let alignment: Alignment
let direction: Direction
// 1.
init(isPresented: Bool, alignment: Alignment, direction: Direction, @ViewBuilder content: () -> T) {
self.isPresented = isPresented
self.alignment = alignment
self.direction = direction
popup = content()
}
// 2.
func body(content: Content) -> some View {
content
.overlay(popupContent())
}
// 3.
@ViewBuilder private func popupContent() -> some View {
GeometryReader { geometry in
if isPresented {
withAnimation {
popup
.transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
.frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
}
}
}
}
}
extension Popup {
enum Direction {
case top, bottom
func offset(popupFrame: CGRect) -> CGFloat {
switch self {
case .top:
let aboveScreenEdge = -popupFrame.maxY
return aboveScreenEdge
case .bottom:
let belowScreenEdge = UIScreen.main.bounds.height - popupFrame.minY
return belowScreenEdge
}
}
}
}
private extension GeometryProxy {
var belowScreenEdge: CGFloat {
UIScreen.main.bounds.height - frame(in: .global).minY
}
}
extension View {
func popup<T: View>(
isPresented: Bool,
alignment: Alignment = .center,
direction: Popup<T>.Direction = .bottom,
@ViewBuilder content: () -> T
) -> some View {
return modifier(Popup(isPresented: isPresented, alignment: alignment, direction: direction, content: content))
}
}
Again all this can be found in my GitHub page here.
CodePudding user response:
You can annotate the observable class or just the function with ‘@MainActor’ or use DispatchQueue.main.async when you assign to the published variables.