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:
You can also use a background queue for non-UI work:
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:
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:
For repeating work:
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:
In iOS 16+ and macOS 13+, you can use Duration for clearer syntax:
Cancellation with Task
Structured concurrency gives you built-in cancellation support through Task:
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
Timerwithout 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.asyncAfteris the most direct replacement for one-shot delayed execution.DispatchWorkItemprovides cancellation support similar tocancelPreviousPerformRequests.Timer.scheduledTimeris best for repeating work and offers simple cancellation viainvalidate().Task.sleepwith 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.

