A Swift package for rapid development using a collection of micro utility extensions for Standard Library

ZamzamKit

ZamzamKit is a Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and other native frameworks.

Installation

Swift Package Manager

.package(url: "[email protected]:ZamzamInc/ZamzamKit.git", .upToNextMajor(from: "5.1.0"))

The ZamzamKit package contains four different products you can import. Add any combination of these to your target’s dependencies within your Package.swift manifest:

.target(
    name: "MyAppExample",
    dependencies: [
        .product(name: "ZamzamCore", package: "ZamzamKit"),
        .product(name: "ZamzamLocation", package: "ZamzamKit"),
        .product(name: "ZamzamNotification", package: "ZamzamKit"),
        .product(name: "ZamzamUI", package: "ZamzamKit"),
    ]
)

Note: This library is highly volatile and changes often to stay ahead of cutting-edge technologies. It is recommended to copy over code that you want into your own libraries or fork it.

ZamzamCore

Standard+

Collection

Get distinct elements from an array:

[1, 1, 3, 3, 5, 5, 7, 9, 9].distinct // [1, 3, 5, 7, 9]

Remove an element from an array by the value:

var array = ["a", "b", "c", "d", "e"]
array.remove("c")
array // ["a", "b", "d", "e"]

Easily get the array version of an array slice:

["a", "b", "c", "d", "e"].prefix(3).array

Safely retrieve an element at the given index if it exists:

4 {
items[3].selectedImage = UIImage("my-image")
}
“>

// Before
if let items = tabBarController.tabBar.items, items.count > 4 {
    items[3].selectedImage = UIImage("my-image")
}

// After
tabBarController.tabBar.items?[safe: 3]?.selectedImage = UIImage("my-image")

[1, 3, 5, 7, 9][safe: 1] // Optional(3)
[1, 3, 5, 7, 9][safe: 12] // nil

Determine if a value is contained within the array of equatable values:

"b".within(["a", "b", "c"]) // true

let status: OrderStatus = .cancelled
status.within([.requested, .accepted, .inProgress]) // false
Dictionary

Convert to JSON string or data:

// Before
guard let data = try? JSONSerialization.data(withJSONObject: merged, options: []),
    let log = String(data: data, encoding: .utf8) else {
        return
}

// After
guard let log = merged.jsonString else {
    return
}
Number

Round doubles, floats, or any floating-point type:

123.12312421.rounded(toPlaces: 3) // 123.123
Double.pi.rounded(toPlaces: 2) // 3.14
String

Create a new random string of given length:

String(random: 10) // "zXWG4hSgL9"
String(random: 4, prefix: "PIN-") // "PIN-uSjm"

Safely use subscript indexes and ranges on strings:

<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="let value = "Abcdef123456"
value[3] // "d"
value[3..

let value = "Abcdef123456"
value[3] // "d"
value[3..<6] // "def"
value[3...6] // "def1"
value[3...] // "def123456"
value[3...99] // nil
value[99] // nil

Validate string against common formats:

"[email protected]".isEmail // true
"123456789".isNumber // true
"zamzam".isAlpha // true
"zamzam123".isAlphaNumeric // true

Remove spaces or new lines from both ends:

" Abcdef123456 \n\r  ".trimmed // "Abcdef123456"

Truncate to a given number of characters:

"Abcdef123456".truncated(3) // "Abc..."
"Abcdef123456".truncated(6, trailing: "***") // "Abcdef***"

Determine if a given value is contained:

"1234567890".contains("567") // true
"abc123xyz".contains("ghi") // false

Injects a separator every nth characters:

"1234567890".separated(every: 2, with: "-") // "12-34-56-78-90"

Remove the characters contained in a given set:

let string = """
    { 0         1
    2                  34
    56       7             8
    9
    }
    """

string.strippingCharacters(in: .whitespacesAndNewlines) // {0123456789}

Replace the characters contained in a given character set with another string:

<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="let set = CharacterSet.alphanumerics
.insert(charactersIn: "_")
.inverted

let string = """
_abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0{1 24@5#6`7~8?9,0

1
"""

string.replacingCharacters(in: set, with: "_") //_abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0_1_2_3_4_5_6_7_8_9_0__1
“>

let set = CharacterSet.alphanumerics
    .insert(charactersIn: "_")
    .inverted

let string = """
    _abcdefghijklmnopqrstuvwxyz
    ABCDEFGHIJKLMNOPQRSTUVWXYZ
    0{1 2<3>[email protected]#6`7~8?9,0

    1
    """

string.replacingCharacters(in: set, with: "_") //_abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0_1_2_3_4_5_6_7_8_9_0__1

Match using a regular expression pattern:

"1234567890".match(regex: "^[0-9]+?$") // true
"abc123xyz".match(regex: "^[A-Za-z]+$") // false

Replace occurrences of a regular expression pattern:

"aa1bb22cc3d888d4ee5".replacing(regex: "\\d", with: "*") // "aa*bb**cc*d***d*ee*"

Remove HTML for plain text:

<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content=""

This is web content with a link.

".htmlStripped // "This is web content with a link."
“>

"<p>This is <em>web</em> content with a <a href=\"http://example.com\">link</a>.</p>".htmlStripped // "This is web content with a link."

Encoders and decoders:

value.urlEncoded()
value.urlDecoded()
value.htmlDecoded()
value.base64Encoded()
value.base64Decoded()
value.base64URLEncoded()

Get an encrypted version of the string in hex format:

"[email protected]".sha256() // 973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b

Easily get the string version of substring:

"hello world".prefix(5).string

Determine if an optional string is nil or has no characters

var value: String? = "test 123"
value.isNilOrEmpty

Foundation+

Bundle

Get the string from a file within any bundle:

Bundle.main.string(file: "Test.txt") // "This is a test. Abc 123.\n"

Get a generic array from a property list file within any bundle:

let values: [String] = Bundle.main.array(plist: "Array.plist")

values[0] // "Abc"
values[1] // "Def"
values[2] // "Ghi"

let values: [[String: Any]] = Bundle.main.array(plist: "Things.plist")

values[0]["id"] as? Int // 1
values[0]["name"] as? String // "Test 1"
values[0]["description"] as? String // "This is a test for 1.")

values[1]["id"] as? Int // 2)
values[1]["name"] as? String // "Test 2")
values[1]["description"] as? String // "This is a test for 2.")

values[2]["id"] as? Int // 3)
values[2]["name"] as? String // "Test 3")
values[2]["description"] as? String // "This is a test for 3.")

Get a dictionary from a property list file within any bundle:

let values: [String: Any] = Bundle.main.contents(plist: "Settings.plist")

values["MyString1"] as? String // "My string value 1."
values["MyNumber1"] as? Int // 123
values["MyBool1"] as? Bool // false
values["MyDate1"] as? Date // 2018-11-21 15:40:03 +0000

Color

Additional color initializers:

UIColor(hex: 0x990000)
UIColor(hex: 0x4286F4)
UIColor(rgb: (66, 134, 244))
UIColor.random
Currency

A formatter that converts between numeric values and their textual currency representations:

let formatter = CurrencyFormatter()
formatter.string(fromAmount: 123456789.987) // "$123,456,789.99"

let formatter2 = CurrencyFormatter(for: Locale(identifier: "fr-FR"))
formatter2.string(fromCents: 123456789) // "1 234 567,89 €"
Data

Get a hex string representation of the data:

Data()?.hexString() // 68626a4a424a6a68626a68616420663773376474663720737567796f3837545e49542a69797567

Get an encrypted version of the data:

Data()?.sha256()
Date

Determine if a date is in the past or future:

Date(timeIntervalSinceNow: -100).isPast // true
Date(timeIntervalSinceNow: 100).isPast // false

Date(timeIntervalSinceNow: 100).isFuture // true
Date(timeIntervalSinceNow: -100).isFuture // false

Determine if a date is today, yesterday, or tomorrow:

Date().isToday // true
Date(timeIntervalSinceNow: -90_000).isYesterday // true
Date(timeIntervalSinceNow: 90_000).isTomorrow // true

Determine if a date is within a weekday or weekend period:

Date().isWeekday // false
Date().isWeekend // true

Get the beginning or end of the day:

Date().startOfDay // "2018/11/21 00:00:00"
Date().endOfDay // "2018/11/21 23:59:59"

Get the beginning or end of the month:

Date().startOfMonth // "2018/11/01 00:00:00"
Date().endOfMonth // "2018/11/30 23:59:59"

Determine if a date is current:

let date = Date(fromString: "2018/03/22 09:40")
date.isCurrentWeek
date.isCurrentMonth
date.isCurrentYear

Determine if a date is between two other dates:

let date = Date()
let date1 = Date(timeIntervalSinceNow: 1000)
let date2 = Date(timeIntervalSinceNow: -1000)

date.isBetween(date1, date2) // true

Determine if a date is beyond a specified time window:

let date = Date(fromString: "2018/03/22 09:40")
let fromDate = Date(fromString: "2018/03/22 09:30")

date.isBeyond(fromDate, bySeconds: 300) // true
date.isBeyond(fromDate, bySeconds: 1200) // false

Use specific calendar for data manipulations:

let date = Date(fromString: "2018/03/22 09:40")
let calendar = Calendar(identifier: .chinese)

date.isToday(using: calendar)
date.isWeekday(using: calendar)
date.isCurrentMonth(using: calendar)
date.isToday(using: calendar)
date.startOfDay(using: calendar)
date.startOfMonth(using: calendar)

Determine if a date is beyond a specified time window:

let date = Date(fromString: "2018/03/22 09:40")
let fromDate = Date(fromString: "2018/03/22 09:30")

date.isBeyond(fromDate, bySeconds: 300) // true
date.isBeyond(fromDate, bySeconds: 1200) // false

Create a date from a string:

Date(fromString: "2018/11/01 18:15")
Date(fromString: "1440/03/01 18:31", calendar: Calendar(identifier: .islamic))

Format a date to a string:

Date().string(format: "MMM d, h:mm a") // "Jan 3, 8:43 PM"
Date().string(style: .full, calendar: Calendar(identifier: .hebrew)) // "Friday, 1 Kislev 5779"
Date().string(formatter: .MM_dd_yyyy_HH_mm)

Format a time interval to display as a timer.

let date = Date(fromString: "2016/03/22 09:45")
let fromDate = Date(fromString: "2016/03/22 09:40")

date.timerString(from: fromDate)

// Prints "00:05:00"

Get the decimal representation of the time:

Date(fromString: "2018/10/23 18:15").timeToDecimal // 18.25

Increment years, months, days, hours, or minutes:

let date = Date()
date + .years(1)
date + .months(2)
date - .days(4)
date - .hours(6)
date + .minutes(12)
date + .days(5, Calendar(identifier: .chinese))

Convert between time interval units:

let diff = date.timeIntervalSince(date2) // 172,800 seconds
diff.minutes // 2,800 minutes
diff.hours // 48 hours
diff.days // 2 days

Time zone context and offset:

let timeZone = TimeZone(identifier: "Europe/Paris")
timeZone?.isCurrent // false
timeZone?.offsetFromCurrent // -21600

Normalize date calculations and data storage:

let timeZone: TimeZone = .posix // GMT
let locale: Locale = .posix // en_US_POSIX
Decodable

Get a value of the type you specify, decoded from a JSON string.

let jsonString = "{\"test1\":29,\"test2\":62,\"test3\":33,\"test4\":24,\"test5\":14,\"test6\":72}"
let jsonObject: [String: Int] = jsonString.decode()

// Result
[
    "test1": 29,
    "test2": 62,
    "test3": 33,
    "test4": 24,
    "test5": 14,
    "test6": 72
]

Get a type-erased Decodable value:

let json = """
{
    "boolean": true,
    "integer": 1,
    "double": 3.14159265358979323846,
    "string": "Abc123",
    "date": "2018-12-05T15:28:25+00:00",
    "array": [1, 2, 3],
    "nested": {
        "a": "alpha",
        "b": "bravo",
        "c": "charlie"
    }
}
""".data(using: .utf8)

let decoder = JSONDecoder()
let dictionary = try decoder.decode([String: AnyDecodable].self, from: json)

dictionary["boolean"].value // true
dictionary["integer"].value // 1
dictionary["string"].value // Abc123

Skip failed elements during decoding instead exiting collection completely; lossy array decoding.

<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.authors = try container.decode(FailableCodableArray.self, forKey: .author)
}
“>

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    
    self.authors = try container.decode(FailableCodableArray<Author>.self, forKey: .author)
}
DispatchQueue

Provides configured queues for executing commonly related work items:

DispatchQueue.database.async {
    // Database work here
}

DispatchQueue.transform.async {
    // Parse or decode work here
}

DispatchQueue.logger.async {
    // Logging work here
}
FileManager

Get URL or file system path for a file:

FileManager.default.url(of: fileName, from: .documentDirectory)
FileManager.default.path(of: fileName, from: .cachesDirectory)

Get URL or file system paths of files within a directory:

FileManager.default.urls(from: .documentDirectory)
FileManager.default.paths(from: .downloadsDirectory)

Retrieve a file remotely and persist to local disk:

FileManager.default.download(from: "http://example.com/test.pdf") { url, response, error in
    // The `url` parameter represents location on local disk where remote file was downloaded.
}
Location

Get the location details for coordinates:

CLLocation(latitude: 43.6532, longitude: -79.3832).geocoder { meta in
    print(meta.locality)
    print(meta.country)
    print(meta.countryCode)
    print(meta.timezone)
    print(meta.administrativeArea)
}

Get the closest or farthest location from a list of coordinates:

let coordinates = [
    CLLocationCoordinate2D(latitude: 43.6532, longitude: -79.3832),
    CLLocationCoordinate2D(latitude: 59.9094, longitude: 10.7349),
    CLLocationCoordinate2D(latitude: 35.7750, longitude: -78.6336),
    CLLocationCoordinate2D(latitude: 33.720817, longitude: 73.090032)
]

coordinates.closest(to: homeCoordinate)
coordinates.farthest(from: homeCoordinate)

Determine if location services is enabled and authorized for always or when in use:

CLLocationManager.isAuthorized // bool
URL

Append a query string parameter to a URL:

let url = URL(string: "https://example.com?abc=123&lmn=tuv&xyz=987")
url?.appendingQueryItem("def", value: "456") // "https://example.com?abc=123&lmn=tuv&xyz=987&def=456"
url?.appendingQueryItem("xyz", value: "999") // "https://example.com?abc=123&lmn=tuv&xyz=999"

Append a dictionary of query string parameters to a URL:

let url = URL(string: "https://example.com?abc=123&lmn=tuv&xyz=987")
url?.appendingQueryItems([
    "def": "456",
    "jkl": "777",
    "abc": "333",
    "lmn": nil
]) // "https://example.com?xyz=987&def=456&abc=333&jkl=777"

Remove a query string parameter to a URL:

let url = URL(string: "https://example.com?abc=123&lmn=tuv&xyz=987")
url?.removeQueryItem("xyz") // "https://example.com?abc=123&lmn=tuv"

Query a URL from a parameter name:

let url = URL(string: "https://example.com?abc=123&lmn=tuv&xyz=987")
url?.queryItem("aBc") // "123"
url?.queryItem("lmn") // "tuv"
url?.queryItem("yyy") // nil
URLSession

A thin wrapper around URLSession and URLRequest for simple network requests:

let request = URLRequest(
    url: URL(string: "https://httpbin.org/get")!,
    method: .get,
    parameters: [
        "abc": 123,
        "def": "test456",
        "xyz": true
    ],
    headers: [
        "Abc": "test123",
        "Def": "test456",
        "Xyz": "test789"
    ]
)
 
let networkManager = NetworkManager(
    service: NetworkFoundationService()
)

networkManager.send(with: request) { result in
    switch result {
    case let .success(response):
        response.data
        response.headers
        response.statusCode
    case let .failure(error):
        error.statusCode
    }
}

Or call multiple URL requests simultaneously:

let request1 = URLRequest(
    url: URL(string: "https://httpbin.org/get")!,
    method: .get
)

let request2 = URLRequest(
    url: URL(string: "https://httpbin.org/post")!,
    method: .post
)

let request3 = URLRequest(
    url: URL(string: "https://httpbin.org/delete")!,
    method: .delete
)

networkManager.send(requests: request1, request2, request3) { firstResult, anotherResult, otherResult in
    switch firstResult {
    case let .success(response):
        response.data
    case let .failure(error):
        error.statusCode
    }

    switch anotherResult {
    case let .success(response):
        response.data
    case let .failure(error):
        error.statusCode
    }
    
    switch otherResult {
    case let .success(response):
        response.data
    case let .failure(error):
        error.statusCode
    }
}

Use an adapter to intercept any URLRequest and modify for all network requests:

URLRequest {
var request = request
request.setValue("1", forHTTPHeaderField: "X-Test-1")
request.setValue("2", forHTTPHeaderField: "X-Test-2")
return request
}
}

let request = URLRequest(
url: URL(string: "https://httpbin.org/get")!,
method: .get
)

let networkManager = NetworkManager(
service: NetworkFoundationService(),
adapter: CustomURLRequestAdapter()
)

networkManager.send(with: request) { result in
guard case let .success(response) else { return }

request.value(forHTTPHeaderField: "X-Test-1") == nil // true
request.value(forHTTPHeaderField: "X-Test-2") == nil // true

response.request.value(forHTTPHeaderField: "X-Test-1") == "1" // true
response.request.value(forHTTPHeaderField: "X-Test-2") == "2" // true
}
“>

struct CustomURLRequestAdapter: URLRequestAdapter {
        
    func adapt(_ request: URLRequest) -> URLRequest {
        var request = request
        request.setValue("1", forHTTPHeaderField: "X-Test-1")
        request.setValue("2", forHTTPHeaderField: "X-Test-2")
        return request
    }
}

let request = URLRequest(
    url: URL(string: "https://httpbin.org/get")!,
    method: .get
)
 
let networkManager = NetworkManager(
    service: NetworkFoundationService(),
    adapter: CustomURLRequestAdapter()
)

networkManager.send(with: request) { result in
    guard case let .success(response) else { return }

    request.value(forHTTPHeaderField: "X-Test-1") == nil // true
    request.value(forHTTPHeaderField: "X-Test-2") == nil // true

    response.request.value(forHTTPHeaderField: "X-Test-1") == "1" // true
    response.request.value(forHTTPHeaderField: "X-Test-2") == "2" // true
}

Utilities

AppInfo

Get details of the current app:

struct SomeStruct: AppInfo {

}

let someStruct = SomeStruct()

someStruct.appDisplayName // "Zamzam App"
someStruct.appBundleID // "io.zamzam.app"
someStruct.appVersion // "1.0.0"
someStruct.appBuild // "23"
someStruct.isInTestFlight // false
someStruct.isRunningOnSimulator // false
Apply

Set properties with closures just after initializing:

let paragraph = NSMutableParagraphStyle().apply {
    $0.alignment = .center
    $0.lineSpacing = 8
}

let label = UILabel().apply {
    $0.textAlignment = .center
    $0.textColor = UIColor.black
    $0.text = "Hello, World!"
}

UITabBar.appearance().apply {
    $0.barStyle = .dark
    $0.tintColor = .blue
}
Atomic

A thread-safe value that handles concurrent reads and writes (read more):

<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="var temp = Atomic(0)

DispatchQueue.concurrentPerform(iterations: 1_000_000) { index in
temp.value { $0 += 1 }
}

XCTAssertEqual(temp.value, 1_000_000) // true
“>

var temp = Atomic<Int>(0)

DispatchQueue.concurrentPerform(iterations: 1_000_000) { index in
temp.value { $0 += 1 }
}

XCTAssertEqual(temp.value, 1_000_000) // true
AppMigration

Manages blocks of code that only need to run once on version updates in apps:

Bool {
migration
.performUpdate {
print("Migrate update occurred.")
}
.perform(forVersion: "1.0") {
print("Migrate to 1.0 occurred.")
}
.perform(forVersion: "1.7") {
print("Migrate to 1.7 occurred.")
}
.perform(forVersion: "2.4") {
print("Migrate to 2.4 occurred.")
}

return true
}
}
“>

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let migration = AppMigration()

    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        migration
            .performUpdate {
                print("Migrate update occurred.")
            }
            .perform(forVersion: "1.0") {
                print("Migrate to 1.0 occurred.")
            }
            .perform(forVersion: "1.7") {
                print("Migrate to 1.7 occurred.")
            }
            .perform(forVersion: "2.4") {
                print("Migrate to 2.4 occurred.")
            }
            
        return true
    }
}
BackgroundTask

Easily execute a long-running background task:

BackgroundTask.run(for: application) { task in
    // Perform finite-length task...
    task.end()
}
Keychain

A thin wrapper to manage Keychain, or other services that conform to KeychainService:

let keychain = KeychainManager(
    service: KeychainExternalService()
)

keychain.set("kjn989hi", forKey: .token)

keychain.get(.token) {
    print($0) // "kjn989hi"
}

// Define strongly-typed keys
extension KeychainAPI.Key {
    static let token = KeychainAPI.Key("token")
}
Logger

Create loggers that conform to LogService and add to LogManager (console and os_log are included):

let log = LogManager(
    services: [
        LogServiceConsole(minLevel: .debug),
        LogServiceOS(
            minLevel: .warning,
            subsystem: "io.zamzam.Basem-Emara",
            category: "Application"
        ),
        MyCustomLogger()
    ]
)

log.error("There was an error.")
SystemConfiguration

Determine if the device is connected to a network:

import SystemConfiguration

SCNetworkReachability.isOnline

Infixes

ConditionalAssignment ?=

Assign a value if not nil:

var test: Int? = 123
var value: Int? = nil

test ?= value
// test == 123

value = 456
test ?= value
// test == 456
NilOrEmptyAssignment ??+

Assign a value if not nil or empty:

var test: String
var value: String?

test = value ??+ "Abc"
// test == "Abc"

value = ""
test = value ??+ "Lmn"
// test == "Lmn"

value = "Xyz"
test = value ??+ "Rst"
// test == "Xyz"

ZamzamLocation

LocationManager

Location manager that offers Combine wrappers:

AnyPublisher in
self?.log.error("GPS location coordinate failed", error: error)
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
.first()
.sink { [weak self] in
self?.log.debug("Location coordinate: \($0)")
self?.locationManager.stopUpdatingLocation()
self?.log.debug("Location turned off GPS")
}
.store(in: &cancellable)
}
“>

func fetchLocation() {
    log.debug("Begin location authorization...")

    guard locationManager.isAuthorized else {
        locationManager.requestAuthorization()
            .handleEvents(receiveOutput: { [weak self] granted in
                guard granted else {
                    self?.log.error("Location authorization denied")
                    return
                }

                self?.log.debug("Location authorization granted")
            })
            .first { $0 }
            .sink { [weak self] _ in self?.fetchLocation() }
            .store(in: &cancellable)

        return
    }

    log.debug("Begin fetching location...")

    locationManager
        .startUpdatingLocation()
        .retry(3)
        .catch { [weak self] error -> AnyPublisher<CLLocation, Never> in
            self?.log.error("GPS location coordinate failed", error: error)
            return Empty(completeImmediately: true).eraseToAnyPublisher()
        }
        .first()
        .sink { [weak self] in
            self?.log.debug("Location coordinate: \($0)")
            self?.locationManager.stopUpdatingLocation()
            self?.log.debug("Location turned off GPS")
        }
        .store(in: &cancellable)
}

ZamzamNotification

UserNotification

Registers the local and remote notifications with the categories and actions it supports:

UNUserNotificationCenter.current().register(
    delegate: self,
    categories: [
        "order": [
            UNNotificationAction(
                identifier: "confirmAction",
                title: "Confirm",
                options: [.foreground]
            )
        ],
        "chat": [
            UNTextInputNotificationAction(
                identifier: "replyAction",
                title: "Reply",
                options: [],
                textInputButtonTitle: "Send",
                textInputPlaceholder: "Type your message"
            )
        ],
        "offer": nil
    ],
    authorizations: [.alert, .badge, .sound],
    completion: { granted in
        granted
            ? log.debug("Authorization for notification succeeded.")
            : log.warn("Authorization for notification not given.")
    }
)

Get a list of all pending or delivered user notifications:

UNUserNotificationCenter.current().getNotificationRequests { notifications in
    notifications.forEach {
        print($0.identifier)
    }
}

Find the pending or delivered notification request by identifier:

UNUserNotificationCenter.current().get(withIdentifier: "abc123") {
    print($0?.identifier)
}

UNUserNotificationCenter.current().get(withIdentifiers: ["abc123", "xyz789"]) {
    $0.forEach {
        print($0.identifier)
    }
}

Determine if the pending or delivered notification request exists:

UNUserNotificationCenter.current().exists(withIdentifier: "abc123") {
    print("Does notification exist: \($0)")
}

Schedules local notifications for delivery:

UNUserNotificationCenter.current().add(
    body: "This is the body for time interval",
    timeInterval: 5
)

UNUserNotificationCenter.current().add(
    body: "This is the body for time interval",
    title: "This is the snooze title",
    timeInterval: 60,
    identifier: "abc123-main"
)

UNUserNotificationCenter.current().add(
    body: "This is the body for time interval",
    title: "This is the misc1 title",
    timeInterval: 60,
    identifier: "abc123-misc1",
    category: "misc1Category"
)

UNUserNotificationCenter.current().add(
    body: "This is the body for time interval",
    title: "This is the misc2 title",
    timeInterval: 60,
    identifier: "abc123-misc2",
    category: "misc2Category",
    userInfo: [
        "id": post.id,
        "link": post.link,
        "mediaURL": mediaURL
    ],
    completion: { error in
        guard error == nil else { return }
        // Added successfully
    }
)

UNUserNotificationCenter.current().add(
    date: Date(timeIntervalSinceNow: 5),
    body: "This is the body for date",
    repeats: .minute,
    identifier: "abc123-repeat"
)

Get a remote image from the web and convert to a user notification attachment:

UNNotificationAttachment.download(from: urlString) {
    guard case let .success(attachment) = $0 else {
        log.error("Could not download the remote resource (\(urlString)): \($0.error?.debugDescription).")
        return
    }

    UNUserNotificationCenter.current().add(
        body: "This is the body",
        attachments: [attachment]
    )
}

Remove pending or delivered notification requests by identifiers, categories, or all:

UNUserNotificationCenter.current().remove(withIdentifier: "abc123")
UNUserNotificationCenter.current().remove(withIdentifiers: ["abc123", "xyz789"])
UNUserNotificationCenter.current().remove(withCategory: "chat") { /* Done */ }
UNUserNotificationCenter.current().remove(withCategories: ["order", "chat"]) { /* Done */ }
UNUserNotificationCenter.current().removeAll()

ZamzamUI

SwiftUI

Documentation coming soon!

Extensions Modifiers Styles Views

WatchKit

CLKComplicationServer

Invalidates and reloads all timeline data for all complications:

// Before
guard let complications = activeComplications, !complications.isEmpty else { return }
complications.forEach { reloadTimeline(for: $0) }

// After
CLKComplicationServer.sharedInstance().reloadTimelineForComplications()

Extends all timeline data for all complications:

// Before
guard let complications = activeComplications, !complications.isEmpty else { return }
complications.forEach { extendTimeline(for: $0) }

// After
CLKComplicationServer.sharedInstance().extendTimelineForComplications()

Author

License

ZamzamKit is available under the MIT license. See the LICENSE file for more info.

GitHub

https://github.com/ZamzamInc/ZamzamKit