Anthney
Anthney

Reputation: 1043

How to trigger an action after x seconds in SwiftUI?

I have been trying to accomplish two main goals that I'm having a headache with. Sorry if it's a simple fix, I am a still bit new to Swift/SwiftUI.

  1. Trigger an action after a certain time has elapsed.
  2. Trigger an @State to change value based on how much time has passed.

I've searched through Stack Overflow and found answers suggesting to use a timer:

struct CurrentDateView : View {
    @State var now = Date()

    let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()

    var body: some View {
        Text("\(now)")
            .onReceive(timer) {
                self.now = Date()
            }
    }
}

But how would I incorporate this so that something like @State can be used change my value to false after 7.5 seconds has passed:

@State randomTF : Bool = true

Or a Text("Please Enter Above") to change to Text("Sorry Too Late") after 7.5 seconds has passed?

Upvotes: 69

Views: 91153

Answers (5)

Hlung
Hlung

Reputation: 14328

From on John's answer, that task is performed before the view appears. From apple's doc:

Adds an asynchronous task to perform before this view appears.

So this is not really "onAppear" and, I think, there's a chance it can be performed before view appears if delay is very short. Which may not be expected.

I suggest keep using .onAppear and just have a Task inside. No need to declare any extension functions. Like this:

    .onAppear {
      Task { @MainActor in
        try await Task.sleep(for: .seconds(0.1))
        // your code here
      }
    }

Upvotes: 16

John
John

Reputation: 1467

It can be as simple as:

Text("\(now)")
.onAppear(delay: 1) {
    self.now = Date()
}

Using these extensions, which are reusable across projects and address some related issues as well:

public extension TimeInterval {
    var nanoseconds: UInt64 {
        return UInt64((self * 1_000_000_000).rounded())
    }
}

@available(iOS 13.0, macOS 10.15, *)
public extension Task where Success == Never, Failure == Never {
    static func sleep(_ duration: TimeInterval) async throws {
        try await Task.sleep(nanoseconds: duration.nanoseconds)
    }
}

@available(iOS 15.0, macOS 12.0, *)
public extension View {
    func onAppear(delay: TimeInterval, action: @escaping () -> Void) -> some View {
        task {
            do {
                try await Task.sleep(delay)
            } catch { // Task canceled
                return
            }
            
            await MainActor.run {
                action()
            }
        }
    }
}

Upvotes: 2

Rahul Bansal
Rahul Bansal

Reputation: 1530

// Add a delay of 1 sec

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
   // your function
}

Upvotes: 22

George
George

Reputation: 30431

Create a delay, which then sets the @State property hasTimeElapsed to true when the time has elapsed, updating the view body.

With Swift 5.5 and the new concurrency updates (async & await), you can now use task(_:) like the following:

struct ContentView: View {
    @State private var hasTimeElapsed = false

    var body: some View {
        Text(hasTimeElapsed ? "Sorry, too late." : "Please enter above.")
            .task(delayText)
    }

    private func delayText() async {
        // Delay of 7.5 seconds (1 second = 1_000_000_000 nanoseconds)
        try? await Task.sleep(nanoseconds: 7_500_000_000)
        hasTimeElapsed = true
    }
}

See more info about Task.sleep(nanoseconds:) in this answer.


Older versions

Xcode 13.0+ now supports concurrency, backwards compatible! However, here is still the 'old' way to do it:

You can use DispatchQueue to delay something. Trigger it with onAppear(perform:) (which happens when the view first appears). You could also hook the delay up to a Button instead if wanted.

Example:

struct ContentView: View {
    @State private var hasTimeElapsed = false

    var body: some View {
        Text(hasTimeElapsed ? "Sorry, too late." : "Please enter above.")
            .onAppear(perform: delayText)
    }

    private func delayText() {
        // Delay of 7.5 seconds
        DispatchQueue.main.asyncAfter(deadline: .now() + 7.5) {
            hasTimeElapsed = true
        }
    }
}

Upvotes: 113

esilver
esilver

Reputation: 28473

.delay is built-in to Swift animations. I achieved my goal of launching an animation 0.5 seconds after a view appeared with the following code:

 .onAppear(perform: {
    withAnimation(Animation.spring().delay(0.5)) {
         self.scale = 1.0
    }
 })

Upvotes: 32

Related Questions