Skip to content

Instantly share code, notes, and snippets.

@IsaacXen
Last active December 5, 2022 19:14
Show Gist options
  • Save IsaacXen/5a69c5378fa9707ad632c2fca65d8316 to your computer and use it in GitHub Desktop.
Save IsaacXen/5a69c5378fa9707ad632c2fca65d8316 to your computer and use it in GitHub Desktop.
dispatch_once in Swift, and more.

Usage

To executes a block object only once for the lifetime of an application, use DispatchQueue.once(token:where:block):

DispatchQueue.once(token: "com.execute.once") {
  // block of code to execute once
}

You can also omit the token to let the function generates one for you:

DispatchQueue.once {
  // block of code to execute once
}

where allow you to perform a condition check to decide whether the block object should be executed:

DispatchQueue.once(where: /* condition */) {
  // block of code to execute once when the condition meet
}

A global convenience method is also available:

dispatchOnce {
  // block of code to execute
}

This is useful for initialization of global data (singletons) in an application.


To executes a block object omly once for the lifetime of an class instance, use DispatchQueue.once(withinLifetime):

DispatchQueue.once(whthinLifetimeOf: /* some object */) {
  // block of code to execute once within object's lifetime
}

This is useful, for example, in iOS, when a view controller are presented as page sheet, viewDidAppear(_:) may be called multiple times when the user attempt to dismiss the view controller by swiping but cancel.

import Foundation
public extension DispatchQueue {
private struct _once_Domain: Hashable {
/// The unique identifier for the class instance as domain.
var identifier: ObjectIdentifier
/// A weak reference to the class instance as domain.
weak var lifetimeObject: NSObject?
/// Create a `_once_Domain` struct with given object as domain.
/// - Parameter object: The object as domain.
init(lifetimeOfObject object: NSObject) {
self.identifier = ObjectIdentifier(object)
self.lifetimeObject = object
}
}
private class _once_DomainalTokens: Hashable {
/// The domain of tokens.
var domain: _once_Domain
/// An sequence of tracked tokens.
var tokens: Set<String>
init(domain: _once_Domain, tokens: Set<String> = []) {
self.domain = domain
self.tokens = tokens
}
static func == (lhs: DispatchQueue._once_DomainalTokens, rhs: DispatchQueue._once_DomainalTokens) -> Bool {
lhs.domain == rhs.domain
}
func hash(into hasher: inout Hasher) {
hasher.combine(domain)
}
}
// =============================================================================================================================================================================
// MARK: - Token Records
// =============================================================================================================================================================================
/// An sequence used for tracking tokens.
private static var _once_globalTokens: Set<String> = []
/// An sequence used for tracking tokens in domain.
private static var _once_domainalTokens: Set<_once_DomainalTokens> = []
// =============================================================================================================================================================================
// MARK: - Managing Token
// =============================================================================================================================================================================
/// Insert a token into domainal token list with specific domain, or global token list if `domain` is `nil`.
/// - Parameters:
/// - token: The token to insert.
/// - domain:The specific domain in domainal token list to insert. `nil` to insert into global token list.
private class func _once_insert(token: String, domain: _once_Domain? = nil) {
/// When `domain` is specified, we insert the token into domainal token list.
if let domain = domain {
/// If the given domain already exist in the domainal token list, then isnert the token directly.
if let domainalTokens = _once_domainalTokens.first(where: { $0.domain == domain }) {
domainalTokens.tokens.insert(token)
/// Otherwise, create one.
} else {
let domainalTokens = _once_DomainalTokens(domain: domain, tokens: [token])
_once_domainalTokens.insert(domainalTokens)
}
/// Otherwise, we insert the token into global token list.
} else {
_once_globalTokens.insert(token)
}
}
/// Remove any `nil` references' tokens in domainal token list. This doesn't affect global token list.
private class func _once_compact() {
/// For some reason, `Set` didn't support `removeAll(where:)` to quickly remove matching elements.
/// So we `filter(_:)` it, and assign the result to achieve the same.
_once_domainalTokens = _once_domainalTokens.filter { $0.domain.lifetimeObject != nil }
}
/// Returns a Boolean value that indicates whether the given token exists in the token list.
/// - Parameters:
/// - token: An token to look for in the token list.
/// - domain: The specific domain in domainal token list to look for. `nil` to look within global token list.
/// - Returns: `true` if the token list contains an token that satisfies predicate; otherwise, `false`.
private class func _once_contains(token: String, in domain: _once_Domain? = nil) -> Bool {
if let domain = domain {
return _once_domainalTokens.contains { $0.domain == domain && $0.tokens.contains(token) }
} else {
return _once_globalTokens.contains(token)
}
}
// =============================================================================================================================================================================
// MARK: - Token Records
// =============================================================================================================================================================================
/// Executes a block object only once for the lifetime of an application.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(token: String? = nil, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(token: token, where: true, block: block, later: { })
}
/// Executes a block object only once for the lifetime of an application.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. `true` by defualt.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(token: String? = nil, where condition: @autoclosure () throws -> Bool = true, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(token: token, where: condition(), block: block, later: { })
}
/// Executes a block object only once for the lifetime of an application.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(token: String? = nil, block: () throws -> Void, later: () throws -> Void = { }, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(token: token, where: true, block: block, later: later)
}
/// Executes a block object only once for the lifetime of an application.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. If the condition mismatch, nither `block` nor `later` will be executed. `true` by defualt.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(token: String? = nil, where condition: @autoclosure () throws -> Bool = true, block: () throws -> Void, later: () throws -> Void = { }, file: String = #file, function: String = #function, line: Int = #line) rethrows {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
_once_compact()
guard try condition() else { return }
let token = token ?? "\(file):\(function):\(line)"
guard !_once_contains(token: token) else {
try later()
return
}
_once_insert(token: token)
try block()
}
/// Executes a block object only once for the lifetime of an given object.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(withinLifetimeOf object: NSObject, token: String? = nil, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(withinLifetimeOf: object, token: token, where: true, block: block, later: { })
}
/// Executes a block object only once for the lifetime of an given object.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. `true` by defualt.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(withinLifetimeOf object: NSObject, token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(withinLifetimeOf: object, token: token, where: condition(), block: block, later: { })
}
/// Executes a block object only once for the lifetime of an given object.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(withinLifetimeOf object: NSObject, token: String? = nil, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try once(withinLifetimeOf: object, token: token, where: true, block: block, later: later)
}
/// Executes a block object only once for the lifetime of an given object.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. If the condition mismatch, nither `block` nor `later` will be executed. `true` by defualt.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
class func once(withinLifetimeOf object: NSObject, token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
_once_compact()
guard try condition() else { return }
let domain = _once_Domain(lifetimeOfObject: object)
let token = token ?? "\(file):\(function):\(line)"
guard !_once_contains(token: token, in: domain) else {
try later()
return
}
_once_insert(token: token, domain: domain)
try block()
}
}
// =================================================================================================================================================================================
// MARK: - Convenience Global Methods
// =================================================================================================================================================================================
/// Executes a block object only once for the lifetime of an application.
///
/// This is a global convenience method of the class method `once(token:block:)` in `DispatchQueue`.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(token: String? = nil, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(token: token, block: block)
}
/// Executes a block object only once for the lifetime of an application.
///
/// This is a global convenience method of the class method `once(token:block:later:)` in `DispatchQueue`.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - later: An block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(token: String? = nil, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(token: token, block: block, later: later)
}
/// Executes a block object only once for the lifetime of an application.
///
/// This is a global convenience method of the class method `once(token:where:block:)` in `DispatchQueue`.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. `true` by defualt.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(token: token, where: condition(), block: block)
}
/// Executes a block object only once for the lifetime of an application.
///
/// This is a global convenience method of the class method `once(token:where:block:later:)` in `DispatchQueue`.
///
/// - Parameters:
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. If the condition mismatch, nither `block` nor `later` will be executed. `true` by defualt.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(token: token, where: condition(), block: block, later: later)
}
/// Executes a block object only once for the lifetime of an given object.
///
/// This is a global convenience method of the class method `once(withinLifetimeOf:token:block:)` in `DispatchQueue`.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(withinLifetimeOf object: NSObject, token: String? = nil, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(withinLifetimeOf: object, token: token, block: block)
}
/// Executes a block object only once for the lifetime of an given object.
///
/// This is a global convenience method of the class method `once(withinLifetimeOf:token:where:block:)` in `DispatchQueue`.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. `true` by defualt.
/// - block: The block object to execute once.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(withinLifetimeOf object: NSObject, token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(withinLifetimeOf: object, token: token, where: condition(), block: block)
}
/// Executes a block object only once for the lifetime of an given object.
///
/// This is a global convenience method of the class method `once(withinLifetimeOf:token:block:later:)` in `DispatchQueue`.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(withinLifetimeOf object: NSObject, token: String? = nil, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(withinLifetimeOf: object, token: token, block: block, later: later)
}
/// Executes a block object only once for the lifetime of an given object.
///
/// This is a global convenience method of the class method `once(withinLifetimeOf:token:where:block:later:)` in `DispatchQueue`.
///
/// - Parameters:
/// - object: An object where its lifetime are used as the scope to run the block once.
/// - token: A token string that is used to test whether the block has completed or not. `nil` to let this function generates one for you.
/// - condition: The condition to meet to execute the block. If the condition mismatch, nither `block` nor `later` will be executed. `true` by defualt.
/// - block: The block object to execute once.
/// - later: An optional block object to execute instead if the `block` has completed.
/// - Throws: This function rethrows any error closure-parameters may throws.
public func dispatchOnce(withinLifetimeOf object: NSObject, token: String? = nil, where condition: @autoclosure () throws -> Bool, block: () throws -> Void, later: () throws -> Void, file: String = #file, function: String = #function, line: Int = #line) rethrows {
try DispatchQueue.once(withinLifetimeOf: object, token: token, where: condition(), block: block, later: later)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment