I want to keep firing a function 5 seconds after it completes.
Previously I would use this at the end of the function:
Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { self.function() }
But I am wanting to use Swift 5.5's async/await.
If I use something like this:
func loadInfo() async {
async let info = someOtherAsyncFunc()
self.info = try? await info
await Task.sleep(5_000_000_000)
await loadInfo()
}
I get a warning that the Function call causes an infinite recursion and it's not really cancellable.
This compiles fine:
func loadInfo() async {
Task {
async let info = someOtherAsyncFunc()
self.info = try? await info
await Task.sleep(5_000_000_000)
if Task.isCancelled {
print("Cancelled")
}
else
{
print("Not cancelled")
await loadInfo()
}
}
}
and although it does fire every 5 seconds, it keeps running when my SwiftUI view is dismissed.
I start it using:
.onAppear {
loadInfo()
}
As it's all running on the same Task and not detached should it not all cancel when the view is removed?
What is the modern way to achieve this with async/await?
You can save the task in a #State variable, and then cancel it when the view disappears with onDisappear(perform:).
Working example:
struct ContentView: View {
#State private var info: String?
#State private var currentTask: Task<Void, Never>?
var body: some View {
NavigationView {
VStack {
Text(info ?? "None (yet)")
.onAppear(perform: loadInfo)
.onDisappear(perform: cancelTask)
NavigationLink("Other view") {
Text("Some other view")
}
}
.navigationTitle("Task Test")
}
.navigationViewStyle(.stack)
}
private func loadInfo() {
currentTask = Task {
async let info = someOtherAsyncFunc()
self.info = try? await info
await Task.sleep(5_000_000_000)
guard !Task.isCancelled else { return }
loadInfo()
}
}
private func cancelTask() {
print("Disappear")
currentTask?.cancel()
}
private func someOtherAsyncFunc() async throws -> String {
print("someOtherAsyncFunc ran")
return "Random number: \(Int.random(in: 1 ... 100))"
}
}
The point of async await is to let you write asynchronous code in a synchronous way. So you could remove the recursive function and simply write:
.task {
repeat {
// code you want to repeat
print("Tick")
try? await Task.sleep(for: .seconds(5)) // exception thrown when cancelled by SwiftUI when this view disappears.
} while (!Task.isCancelled)
print("Cancelled")
}
I noticed the print tick is always on main thread however if I move it out to its own async func then it correctly runs on different threads.
Related
In a view, I want to wait for a series of async calls to finish loading, then redirect to another screen. Unfortunately, I see the code running in the back (The JSON data gets loaded) but once it completes it does not redirect to the new view.
Here is my view:
struct loadingView: View {
#ObservedObject var dataLoader: DataLoader = DataLoader()
#State var isLoaded: Bool = false
var body: some View {
VStack {
Text("Loading \(isLoaded)")
}
}
.task {
await self.dataloader.loadJSONData(isLoaded: $isLoaded)
MainScreen()
}
}
...and the DataLoader class:
#MainActor DataLoader: NSObject, ObservableObject {
func loadJSONData(isLoaded: Binding<Bool>) {
await doLoadData()
isLoaded.wrappedValue = True
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
"Redirecting" here doesn't really make sense. Do you really want the user to be able to navigate back to the loading screen? Perhaps you're thinking of this like a web page, but SwiftUI is nothing like that. What you really want to do is display one thing when loading, and a different thing when loaded. That's just if, not "redirection."
Instead, consider the following pattern. Create this kind of LoadingView (extracted from some personal code of mine):
struct LoadingView<Content: View, Model>: View {
enum LoadState {
case loading
case loaded(Model)
case error(Error)
}
#ViewBuilder let content: (Model) -> Content
let loader: () async throws -> Model
#State var loadState = LoadState.loading
var body: some View {
ZStack {
Color.white
switch loadState {
case .loading: Text("Loading")
case .loaded(let model): content(model)
case .error(let error): Text(verbatim: "Error: \(error)")
}
}
.task {
do {
loadState = .loaded(try await loader())
} catch {
loadState = .error(error)
}
}
}
}
It require no redirection. It just displays different things when in different states (obviously the Text view can be replaced by something more interesting).
Then to use this, embed it in another View. In my personal code, that includes a view like this:
struct DailyView: View {
var body: some View {
LoadingView() { model in
LoadedDailyView(model: model)
} loader: {
try await DailyModel()
}
}
}
Then LoadedDailyView is the "real" view. It is handled a fully populated model that is created by DailyModel.init (a throwing, async init).
You could try this approach, using NavigationStack and NavigationPath to Redirecting after task w/ Await completes.
Here is the code I use to test my answer:
struct ContentView: View {
var body: some View {
loadingView()
}
}
#MainActor
class DataLoader: NSObject, ObservableObject {
func loadJSONData() async {
await doLoadData()
// for testing, wait for 1 second
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var navPath = NavigationPath()
var body: some View {
NavigationStack(path: $navPath) {
VStack (spacing: 44) {
Text("Loading....")
}
.navigationDestination(for: Bool.self) { _ in
MainScreen()
}
}
.task {
await dataLoader.loadJSONData()
navPath.append(true)
}
}
}
struct MainScreen: View {
var body: some View {
Text("---> MainScreen here <---")
}
}
If you need ios 15 or earlier, then use NavigationView:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State var isLoaded: Bool?
var body: some View {
NavigationView {
VStack {
Text(isLoaded == nil ? "Loading..." : "Finished loading")
NavigationLink("", destination: MainScreen(), tag: true, selection: $isLoaded)
}
}.navigationViewStyle(.stack)
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
If your loadingView has the only purpose of showing the "loading" message, then
display the MainScreen after the data is loaded, you could use the following approach using a simple swicth:
struct loadingView: View {
#StateObject var dataLoader = DataLoader()
#State private var isLoaded = false
var body: some View {
VStack {
if isLoaded {
MainScreen()
} else {
ProgressView("Loading")
}
}
.task {
await dataLoader.loadJSONData()
isLoaded = true
}
}
}
Use #StateObject instead of #ObservedObject. Use #Published instead of trying to pass a binding to the object (that is a mistake because a binding is just a pair of get and set closures that will expire if LoadingView is re-init), use Group with an if to conditionally show a View e.g.
struct LoadingView: View {
#StateObject var dataLoader: DataLoader = DataLoader()
var body: some View {
Group {
if dataLoader.isLoaded {
LoadedView(data: dataLoader.data)
} else {
Text("Loading...")
}
}
.task {
await dataloader.loadJSONData()
}
}
The DataLoader should not be #MainActor because you want it to run on a background thread. Use #MainActor instead on a sub-task once the async work has finished e.g.
class DataLoader: ObservableObject {
#Published var isLoaded = false
#Published var data: [Data] = []
func loadJSONData async {
let d = await doLoadData()
Task { #MainActor in
isLoaded = true
data = d
}
}
func doLoadData() async {
/* do data load */
/* This code works */
}
}
This pattern is shown in Apple's tutorial here, PandaCollectionFetcher.swift copied below:
import SwiftUI
class PandaCollectionFetcher: ObservableObject {
#Published var imageData = PandaCollection(sample: [Panda.defaultPanda])
#Published var currentPanda = Panda.defaultPanda
let urlString = "http://playgrounds-cdn.apple.com/assets/pandaData.json"
enum FetchError: Error {
case badRequest
case badJSON
}
func fetchData() async
throws {
guard let url = URL(string: urlString) else { return }
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
Task { #MainActor in
imageData = try JSONDecoder().decode(PandaCollection.self, from: data)
}
}
}
I'm having trouble making async functions run in background threads (to prevent blocking the main thread).
Below is a method that takes about 5 seconds to run.
From what I've learned, it seemed like making the function async and marking it with await on function call would be enough. But it doesn't work as intended and still freezes up the UI.
EDIT
Since it's stated that Swift 5.5 concurrency can replace DispatchQueue, I am trying to find a way to do this with only Async/Await.
EDIT_2
I did try removing the #MainActor wrapper, but it still seem to run on the main thread.
NumberManager.swift
#MainActor class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
// takes about 5 seconds to run...
} }
ContentView
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View{
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
.onAppear() {
Task {
// Runs in the main thread, freezing up the UI until it completes.
await numberManager.generateNumbers()
}
}
}
.tabItem {
Label("One", systemImage: "list.dash")
}
Text("Hello")
.tabItem {
Label("Two", systemImage: "square.and.pencil")
}
}
}
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
} }
What I've tried...
I've tried a few things, but the only way that made it run in the background was changing the function as below. But I know that using Task.detached should be avoided unless it's absolutely necessary, and I didn't think this is the correct use-case.
func generateNumbers() async {
Task.detached {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
await MainActor.run { [numbers] in
self.numbers = numbers
}
}
Writing async on a function doesn’t make it leave the thread. You need a continuation and you need to actually leave the thread somehow.
Some ways you can leave the thread using DispatchQueue.global(qos: .background).async { or use Task.detached.
But the most important part is returning to the main thread or even more specific to the Actor's thread.
DispatchQueue.main.async is the "old" way of returning to the main thread it shouldn't be used with async await. Apple as provided CheckedContinuation and UncheckedContinuation for this purpose.
Meet async/await can elaborate some more.
import SwiftUI
struct ConcurrentSampleView: View {
//Solution
#StateObject var vm: AsyncNumberManager = .init()
//Just to create a project that can show both scenarios.
//#StateObject var vm: NumberManager = .init()
#State var isLoading: Bool = false
var body: some View {
HStack{
//Just to visualize the thread being released
//If you use NumberManager the ProgressView won't appear
//If you use AsyncNumberManager the ProgressView WILL appear
if isLoading{
ProgressView()
}
Text(vm.numbers == nil ? "nil" : "\(vm.numbers?.count.description ?? "")")
}
//.task is better for iOS 15+
.onAppear() {
Task{
isLoading = true
await vm.generateNumbers()
isLoading = false
}
}
}
}
struct ConcurrentSampleView_Previews: PreviewProvider {
static var previews: some View {
ConcurrentSampleView()
}
}
#MainActor
class AsyncNumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
numbers = await concurrentGenerateNumbers()
}
private func concurrentGenerateNumbers() async -> [Double] {
typealias Cont = CheckedContinuation<[Double], Never>
return await withCheckedContinuation { (cont: Cont) in
// This is the asynchronous part, have the operation leave the current actor's thread.
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
//This tells the function to return to the actor's thread
cont.resume(returning: numbers)
}
}
}
//Or something like this it just depends on the true scenario
private func concurrentGenerateNumbers2() async -> [Double] {
// This is the asynchronous part, have the operation leave the actor's thread
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
return await Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
return numbers
}.value
}
}
//Incorrect way of applying async/await. This doesn't actually leave the thread or mark when to return. Left here to highlight both scenarios in a reproducible example.
#MainActor
class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
}
}
You have answered your own question - You can use structured concurrency to solve your problem.
Your problem occurs because you have use the #MainActor decorator on your class. This means that it executes on the main queue.
You can either remove this decorator or, as you have found, use structured concurrency to explicitly create a detached task and the use a main queue task to provide your result.
Which approach you use depends on what else this class does. If it does a lot of other work that needs to be on the main queue then #MainActor is probably a good approach. If not then remove it.
The #MainActor property wrapper is not (just) what makes your observable object run on the main thread. #StateObject is what's doing it. Which is logical, since changes to the object will update the UI.
Removing the #MainActor wrapper is not the solution, because any changes to #Published properties will have to be done on the main thread (since they update the UI). You also don't want to run Task.detached, at least not if the task is going to change any #Published property, for the same reason.
By marking your generateNumbers function as async, a method from the main thread can call it with await - which allows the task to suspend and not block the main thread. That's what makes it concurrent.
extension NumberManager {
func loadNumbers() {
// This task will run on the main thread
Task {
// `await` tells the Swift executor that this method can run in the background,
// and the main thread can continue doing other things while it waits for its result
self.numbers = await self.generateNumbers()
}
}
func generateNumbers() async -> [Double] {
return (1...10_000_000).map { _ in Double.random(in: -10...10)
}
}
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
.onAppear { numberManager.loadNumbers() }
}
}
A more complete loadNumbers method could also store a reference to the task allowing you to cancel or restart a running task. However nowadays we have the excellent .task(priority:_:) to do all of that for you. It manages the task's lifecycle automatically, which means less boilerplate code.
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
// generate numbers
.task(.priority: .high) {
numberManager.numbers = await numberManager.generateNumbers
}
}
}
As far as I know this is the most succinct way to do expensive calculations on a background thread in Swift 5.7.
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
I have a list with data from the search.
to get data I want to call to await func (swift 5.5) but I get this error:
"Cannot pass function of type '() async -> ()' to parameter expecting
synchronous function type"
this is my code:
struct ContentView: View {
#ObservedObject var twitterAPI: TwitterAPI = TwitterAPI()
#State private var searchText = "TheEllenShow" // TheEllenShow
var body: some View {
NavigationView {
VStack{
if twitterAPI.twitterSearchResults?.resultDataVM != nil{
List {
ForEach((twitterAPI.twitterSearchResults?.resultDataVM)!) { item in
Text(item.text)
}
}
.refreshable {
await twitterAPI.executeQuery(userName: searchText)
}
}else{
Text("Loading")
}
Spacer()
}
.searchable(text: $searchText)
.onSubmit(of: .search) {
await twitterAPI.executeQuery(userName: searchText)
}
.navigationTitle("Twitter")
}
.task {
await twitterAPI.executeQuery(userName: searchText)
}
} }
To call asynchronous code from a synchronous code block, you can create a Task object:
.onSubmit(of: .search) {
Task {
await twitterAPI.executeQuery(userName: searchText)
}
}
You could bounce it over to .task like this:
#State var submittedSearch = ""
#State var results = []
.onSubmit(of: .search) {
submittedSearch = searchText
}
.task(id: submittedSearch) {
if submittedSearch.isEmpty {
return
}
results = await twitterAPI.executeQuery(userName: submittedSearch)
}
Has the advantage it will be cancelled and restarted if the search changes and also when the underlying UIView disappears.
I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.