swift
performselector
unavailable
programming
duplicate

Swift performSelectorwithObjectafterDelay is unavailable

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If you have tried to call performSelector(withObject:afterDelay:) in Swift, you have seen the compiler error telling you it is unavailable. This is not a bug. Swift intentionally removed access to the performSelector family of methods because they are fundamentally incompatible with Swift's type safety model. Understanding why Swift made this decision helps you choose the right modern replacement for delayed execution.

Why Swift Removed performSelector

In Objective-C, performSelector:withObject:afterDelay: works by passing a selector (a method name as a string) to the runtime, which looks up and invokes the method dynamically at the specified time. This has several problems from Swift's perspective.

First, the compiler cannot verify that the selector you pass actually exists on the target object. A typo in the selector name compiles fine but crashes at runtime. Second, the method signature is limited to methods that take zero or one id parameter and return id or void. Passing value types like Int or Bool requires boxing them into NSNumber, which is error-prone. Third, memory management is ambiguous. The compiler cannot apply ARC (Automatic Reference Counting) correctly because it does not know the return type or parameter types at compile time.

Swift's design philosophy prioritizes catching errors at compile time rather than at runtime. Since performSelector bypasses the type system entirely, Swift removes it from the API surface.

Replacement 1 -- DispatchQueue.main.asyncAfter

The most direct replacement for delayed execution is DispatchQueue.main.asyncAfter. It schedules a closure to run after a specified time interval on any dispatch queue:

swift
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
    self.updateUI()
}

You can also use a background queue for non-UI work:

swift
1DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1.5) {
2    self.processData()
3    DispatchQueue.main.async {
4        self.refreshView()
5    }
6}

This approach is type-safe because the closure captures concrete types, and the compiler verifies every method call inside the closure at compile time. It is the recommended replacement for most use cases.

Cancellation with DispatchWorkItem

One advantage of the old performSelector approach was easy cancellation via NSObject.cancelPreviousPerformRequests. With GCD, you achieve cancellation using DispatchWorkItem:

swift
1class SearchController {
2    private var pendingSearch: DispatchWorkItem?
3
4    func userTyped(query: String) {
5        // Cancel the previous delayed search
6        pendingSearch?.cancel()
7
8        // Schedule a new one
9        let task = DispatchWorkItem { [weak self] in
10            self?.performSearch(query: query)
11        }
12        pendingSearch = task
13        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: task)
14    }
15
16    private func performSearch(query: String) {
17        print("Searching for: \(query)")
18    }
19}

Notice the [weak self] capture list. This prevents a retain cycle where the delayed work item keeps the controller alive after it should have been deallocated.

Replacement 2 -- Timer.scheduledTimer

When you need a repeating action or want the flexibility of invalidating a timer, Timer.scheduledTimer is a good choice:

swift
1// One-shot timer
2Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
3    self.showAlert()
4}

For repeating work:

swift
1class PollingService {
2    private var timer: Timer?
3
4    func startPolling() {
5        timer = Timer.scheduledTimer(
6            withTimeInterval: 5.0,
7            repeats: true
8        ) { [weak self] _ in
9            self?.fetchUpdates()
10        }
11    }
12
13    func stopPolling() {
14        timer?.invalidate()
15        timer = nil
16    }
17
18    private func fetchUpdates() {
19        print("Fetching updates...")
20    }
21}

Cancellation is straightforward. Call invalidate() on the timer and set it to nil. Always invalidate timers before the owning object is deallocated, typically in deinit or when the view disappears.

Replacement 3 -- Task.sleep in async/await

With Swift concurrency (Swift 5.5+), you can write delayed execution as sequential code using Task.sleep:

swift
1func loadDataAfterDelay() async {
2    do {
3        try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
4        await updateUI()
5    } catch {
6        // Task was cancelled
7        print("Delayed task cancelled")
8    }
9}

In iOS 16+ and macOS 13+, you can use Duration for clearer syntax:

swift
1func loadDataAfterDelay() async {
2    do {
3        try await Task.sleep(for: .seconds(2))
4        await updateUI()
5    } catch {
6        print("Task cancelled")
7    }
8}

Cancellation with Task

Structured concurrency gives you built-in cancellation support through Task:

swift
1class ViewModel: ObservableObject {
2    private var delayedTask: Task<Void, Never>?
3
4    func scheduleUpdate() {
5        // Cancel any previous delayed task
6        delayedTask?.cancel()
7
8        delayedTask = Task { [weak self] in
9            do {
10                try await Task.sleep(for: .seconds(1))
11                await self?.performUpdate()
12            } catch {
13                // Cancellation is expected, not an error
14            }
15        }
16    }
17
18    @MainActor
19    private func performUpdate() {
20        print("Updating...")
21    }
22}

When you cancel a Task, the Task.sleep call throws CancellationError, which cleanly exits the closure without running the delayed work. This is more explicit and safer than the silent cancellation behavior of performSelector.

Choosing the Right Replacement

The best replacement depends on your context. Use DispatchQueue.main.asyncAfter for simple one-shot delays in UIKit code. Use Timer.scheduledTimer when you need repeating execution or when your code already works with the run loop. Use Task.sleep in async/await contexts, especially in SwiftUI view models and modern Swift concurrency code. For new projects targeting iOS 15+, Task.sleep is generally the cleanest option.

Common Pitfalls

  • Creating strong reference cycles by not using [weak self] in delayed closures, causing memory leaks.
  • Forgetting to cancel pending work when the owning object is deallocated, leading to crashes when the closure executes on a freed object.
  • Using Timer without invalidating it, which keeps the timer and its target alive indefinitely.
  • Calling UI updates from a background queue with DispatchQueue.global().asyncAfter, which causes undefined behavior.
  • Using nanosecond values incorrectly with Task.sleep(nanoseconds:). One second is 1,000,000,000 nanoseconds, not 1,000,000.

Summary

  • Swift removes performSelector:withObject:afterDelay: because it bypasses the type system and cannot be verified at compile time.
  • DispatchQueue.main.asyncAfter is the most direct replacement for one-shot delayed execution.
  • DispatchWorkItem provides cancellation support similar to cancelPreviousPerformRequests.
  • Timer.scheduledTimer is best for repeating work and offers simple cancellation via invalidate().
  • Task.sleep with async/await provides the cleanest syntax and built-in cancellation through structured concurrency.
  • Always use [weak self] in delayed closures to prevent retain cycles and memory leaks.

Course illustration
Course illustration

All Rights Reserved.