ios-concurrency — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-concurrency (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
async let or TaskGroup over spawning loose Task {} blocks.@MainActor-isolated. UIKit code that touches UI must run on @MainActor.AsyncSequence.Task {} with explicit actor isolation instead. Detached tasks lose actor context and priority inheritance.Task.isCancelled or call try Task.checkCancellation() at appropriate points.| Scenario | Solution |
|---|---|
| Single async operation | async func / Task {} |
| Multiple independent operations | TaskGroup / async let |
| Sequential dependent operations | await one after another |
| Shared mutable state | actor |
| UI updates from background | @MainActor / MainActor.run {} |
| Bridge callback API | withCheckedContinuation / withCheckedThrowingContinuation |
| Bridge delegate pattern | AsyncStream with continuation |
| Streaming data | AsyncSequence / AsyncStream |
| Debounce user input | Task cancellation pattern |
| Non-Sendable legacy type | @preconcurrency import / @unchecked Sendable (last resort) |
| Combine publisher to async | .values property on publisher |
| Timer / periodic work | AsyncTimerSequence (from swift-async-algorithms) or AsyncStream |
| Parallel with limit | TaskGroup with semaphore-like counter |
// Basic async function
func fetchUser(id: String) async throws -> User {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
// Calling from SwiftUI
.task {
do {
user = try await fetchUser(id: "123")
} catch {
errorMessage = error.localizedDescription
}
}
// Parallel execution with async let
async let profile = fetchProfile(id: userId)
async let posts = fetchPosts(userId: userId)
async let followers = fetchFollowers(userId: userId)
let result = try await (profile, posts, followers)
// TaskGroup for dynamic parallelism
func fetchAllUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await self.fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}// Actor for shared state
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
cache[url]
}
func store(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
// Usage — await is required to cross isolation boundary
let cache = ImageCache()
await cache.store(image, for: url)
let cached = await cache.image(for: url)
// @MainActor for UI
@MainActor
final class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
items = try await api.fetchItems()
} catch {
// handle error
}
}
}// Value types — automatically Sendable if all members are
struct UserDTO: Sendable {
let id: String
let name: String
}
// Reference types — must be final with immutable stored properties
final class Configuration: Sendable {
let apiKey: String
let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// @unchecked Sendable — escape hatch (use with caution)
final class LegacyManager: @unchecked Sendable {
private let lock = NSLock()
private var _state: State = .idle
var state: State {
lock.withLock { _state }
}
}// Bridge completion handler to async/await
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyAPI.fetch { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// Bridge delegate to AsyncStream
func locationUpdates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
let delegate = LocationDelegate { location in
continuation.yield(location)
}
continuation.onTermination = { _ in
delegate.stop()
}
delegate.start()
}
}// Cooperative cancellation
func processItems(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation() // throws CancellationError
await process(item)
}
}
// Manual check
func fetchWithFallback() async -> Data {
if Task.isCancelled { return Data() }
// ... continue work
}
// Debounce pattern
@MainActor
final class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [Result] = []
private var searchTask: Task<Void, Never>?
func search() {
searchTask?.cancel()
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = await api.search(query)
}
}
}// Enable in Package.swift
.target(
name: "MyTarget",
swiftSettings: [.swiftLanguageMode(.v6)]
)
// Or in Xcode: Build Settings → Swift Language Version → 6
// Common fixes:
// 1. Non-Sendable type crossing isolation boundary
// → Make type Sendable or use @unchecked Sendable
// 2. Mutable capture in @Sendable closure
// → Use actor or move state inside Task
// 3. Global variable not concurrency-safe
// → Use actor, nonisolated(unsafe), or make it let
// 4. Legacy framework types not Sendable
// → @preconcurrency import FrameworkName// BAD: Using Task.detached without good reason
Task.detached {
await self.doWork() // loses actor isolation and priority
}
// GOOD: Use Task {} — inherits actor context
Task {
await doWork()
}
// BAD: Blocking an actor with synchronous work
actor DataProcessor {
func process(_ data: Data) -> Result {
heavySyncComputation(data) // blocks the actor's executor
}
}
// GOOD: Move heavy sync work off the actor
actor DataProcessor {
func process(_ data: Data) async -> Result {
await Task.detached(priority: .utility) {
heavySyncComputation(data) // runs on cooperative pool
}.value
}
}
// BAD: Ignoring cancellation
Task {
for item in hugeList {
await process(item) // never checks cancellation
}
}
// GOOD: Cooperative cancellation
Task {
for item in hugeList {
try Task.checkCancellation()
await process(item)
}
}
// BAD: Resuming continuation multiple times (CRASH)
withCheckedContinuation { continuation in
api.fetch { data in
continuation.resume(returning: data)
}
api.fetch { data in // second resume — CRASH
continuation.resume(returning: data)
}
}
// BAD: Never resuming continuation (LEAK — task hangs forever)
withCheckedContinuation { continuation in
api.fetch { data in
if let data = data {
continuation.resume(returning: data)
}
// if data is nil, continuation is never resumed!
}
}@MainActor. Offload to a non-isolated async function or Task.detached.async let when the number of operations is dynamic.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.