41

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?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
Anthney
  • 735
  • 1
  • 7
  • 12

5 Answers5

88

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
        }
    }
}
George
  • 25,988
  • 10
  • 79
  • 133
  • 2
    This doesn't feel like the intended way to do this... especially since SwiftUI views are ephemeral and can be destroyed and re-initialized at any time. I'd expect they would intend some Combine publisher here instead – Ky - Oct 09 '20 at 15:21
21

.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
    }
 })
esilver
  • 27,713
  • 23
  • 122
  • 168
  • This is perfect. I display an occasional error message (like 'account unknown' or 'incorrect password') but I want it to disappear after several seconds. Two thumbs up! – Zonker.in.Geneva Dec 30 '21 at 21:44
9

// Add a delay of 1 sec

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
   // your function
}
Rahul Bansal
  • 1,382
  • 15
  • 22
1

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()
            }
        }
    }
}
John
  • 964
  • 8
  • 21
0

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
      }
    }
Hlung
  • 13,850
  • 6
  • 71
  • 90