ZamzamKit 4.0.1

ZamzamKit 4.0.1

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Jan 2019
SPMSupports SPM

Maintained by Zamzam Inc..



ZamzamKit 4.0.1

ZamzamKit

Build Status Platform Swift Xcode Carthage compatible Version MIT

ZamzamKit a Swift framework for rapid development using a collection of small utility extensions for Standard Library, Foundation and UIKit classes and protocols.

Installation

Carthage

Add github "ZamzamInc/ZamzamKit" to your Cartfile.

CocoaPods

Add pod "ZamzamKit" to your Podfile.

Framework
  1. Download the latest release of ZamzamKit and extract the zip.
  2. Go to your Xcode project’s "General" settings. Drag ZamzamKit.framework and ZamzamKit.framework from the appropriate Swift-versioned directory for your project in ios/, tvos/ or watchos/ directory to the "Embedded Binaries" section. Make sure "Copy items if needed" is selected (except if using on multiple platforms in your project) and click Finish.
  3. In your unit test target's "Build Settings", add the parent path to ZamzamKit.framework in the "Framework Search Paths" section.

Usage

Standard Library

Array

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
Collection

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

// 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
Dictionary

Remove all values equal to nil:

var value: [String: Any?] = [
    "abc": 123,
    "efd": "xyz",
    "ghi": nil,
    "lmm": true,
    "qrs": nil,
    "tuv": 987
]

value.removeAllNils()

value.count -> 4
value.keys.contains("abc") -> true
value.keys.contains("ghi") -> false
value.keys.contains("qrs") -> false
Equatable

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

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

let status: OrderStatus = .cancelled
status.within([.requeseted, .accepted, .inProgress]) -> false
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:

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".separate(every: 2, with: "-") -> "12-34-56-78-90"

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:

"<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

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

Convert sequences and dictionaries to a JSON string:

// Before
guard let data = self as? [[String: Any]],
    let stringData = try? JSONSerialization.data(withJSONObject: data, options: []) else {
        return nil
}
    
let json = String(data: stringData, encoding: .utf8) as? String
// After
let json = mySequence.jsonString
let json = myDictionary.jsonString

Foundation

Bundle

Get the contents of a file within any bundle:

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

Get the contents of a property list file within any bundle:

let values = 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))
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 €"
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 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

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"

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 using UTC and POSIX:

let calendar: Calendar = .posix
let locale: Locale = .posix
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.
}
Infix

Assign a value if not nil:

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

test ?= value
// test == 123

value = 456
test ?= value
// test == 456
NotificationCenter

Shorthand to post and observer functions:

let notificationCenter: NotificationCenter = .default

// Before
notificationCenter.post(name: .MyCustomNotificationKey, object: nil, userInfo: nil)
notificationCenter.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
notificationCenter.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)

// After
notificationCenter.post(name: .MyCustomNotificationKey)
notificationCenter.addObserver(for: UIApplication.willEnterForegroundNotification, selector: #selector(willEnterForeground), from: self)
notificationCenter.removeObserver(for: UIApplication.willEnterForegroundNotification, from: self)
NSAttributedString

Easily get the attributed string version of a string:

"Abc".attributed
"Lmn".mutableAttributed
"Xyz".mutableAttributed([
    .font: UIFont.italicSystemFont(ofSize: .systemFontSize),
    .foregroundColor, value: UIColor.green
])

Add attributed strings together:

label.attributedText = "Abc".attributed + " def " +
    "ghi".mutableAttributed([
        .underlineStyle: NSUnderlineStyle.single.rawValue
    ])
Object

Set properties with closures just after initializing:

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

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

Append or remove query string parameters:

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"

url?.appendingQueryItems([
    "def": "456",
    "jkl": "777",
    "abc": "333",
    "lmn": nil
]) -> "https://example.com?xyz=987&def=456&abc=333&jkl=777"

url?.removeQueryItem("xyz") -> "https://example.com?abc=123&lmn=tuv"

iOS

Application

Split up AppDelegate into pluggable modules:

// Subclass to pass lifecycle events to loaded modules
@UIApplicationMain
class AppDelegate: ApplicationModuleDelegate {

    override func modules() -> [ApplicationModule] {
        return [
            LoggerApplicationModule(),
            NotificationApplicationModule()
        ]
    }
}
// Each application module has access to the AppDelegate lifecycle events
final class LoggerApplicationModule: ApplicationModule {
    private let log = Logger()
 
    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        log.config(for: application)
        return true
    }
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
        log.info("App did finish launching.")
        return true
    }
    
    func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
        log.warn("App did receive memory warning.")
    }
    
    func applicationWillTerminate(_ application: UIApplication) {
        log.warn("App will terminate.")
    }
}

The pluggable module technique also works for UIViewController:

// Subclass to pass lifecycle events to loaded modules
class ViewController: ControllerModuleDelegate {

    override func modules() -> [ControllerModule] {
        return [
            ChatControllerModule(),
            OrderControllerService()
        ]
    }
}
// Each controller module has access to the UIViewController lifecycle events
final class ChatControllerModule: ControllerModule {
    private let chatWorker = ChatWorker()

    func viewDidLoad(_ controller: UIViewController) {
        chatWorker.config()
    }
}

extension ChatControllerService {

    func viewWillAppear(_ controller: UIViewController) {
        chatWorker.subscribe()
    }

    func viewWillDisappear(_ controller: UIViewController) {
        chatWorker.unsubscribe()
    }
}
Background

Easily execute a long-running background task:

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

A bar button item with a badge value:

Image of BadgeBarButtonItem

navigationItem.rightBarButtonItems = [
    BadgeBarButtonItem(
        button: UIButton(type: .contactAdd),
        badgeText: "123",
        target: self,
        action: #selector(test)
    )
]

navigationItem.leftBarButtonItems = [
    BadgeBarButtonItem(
        button: UIButton(type: .detailDisclosure),
        badgeText: SCNetworkReachability.isOnline ? "On" : "Off",
        target: self,
        action: #selector(test)
    ).with {
        $0.badgeFontColor = SCNetworkReachability.isOnline ? .black : .white
        $0.badgeBackgroundColor = SCNetworkReachability.isOnline ? .green : .red
    }
]
GradientView

A UIView with gradient effects:

@IBOutlet weak var gradientView: GradientView! {
    didSet {
        gradientView.firstColor = .blue
        gradientView.secondColor = .red
    }
}

Interface Builder compatible via "User Defined Runtime Attributes":

Image of GradientView

KeyboardScrollView

The automaticallyAdjustsInsetsForKeyboard property extends the scroll view insets when the keyboard is shown:

Image of KeyboardScrollView

MailComposer

Compose an email with optional subject, body, or attachment:

// Before
extension MyViewController: MFMailComposeViewControllerDelegate {

    func sendEmail() {
        guard MFMailComposeViewController.canSendMail() else {
            return present(alert: "Could Not Send Email", message: "Your device could not send e-mail.")
        }

        let mail = MFMailComposeViewController()
        mail.mailComposeDelegate = self
        mail.setToRecipients(["[email protected]"])

        present(mail, animated: true)
    }

    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        controller.dismiss(animated: true)
    }
}
// After
class MyViewController: UIViewController {
    private let mailComposer = MailComposer()

    func sendEmail() {
        guard let controller = mailComposer.makeViewController(email: "[email protected]") else {
            return present(alert: "Could Not Send Email", message: "Your device could not send e-mail.")
        }

        present(controller, animated: true)
    }
NextResponderTextField

An extended UITextView that wires the "Return Key" to another UIResponder:

Image of NextResponderTextField

Image of NextResponderTextField2

RoundedView

A UIView, UIImage, and UIButton subclasses with circular masking:

Image of RoundedView

Routing

Conforming types to Routable provides extensions in UIViewController for strongly-typed storyboard routing (read more):

class ViewController: UIViewController {

    @IBAction func moreTapped() {
        present(storyboard: .more) { (controller: MoreViewController) in
            controller.someProperty = "\(Date())"
        }
    }
}

extension ViewController: Routable {

    enum StoryboardIdentifier: String {
        case more = "More"
        case login = "Login"
    }
}

Conforming types to Router can specify a weak UIViewController instance:

struct HomeRouter: Router {
    weak var viewController: UIViewController?

    init(viewController: UIViewController?) {
        self.viewController = viewController
    }

    func listPosts(for fetchType: FetchType) {
        show(storyboard: .listPosts) { (controller: ListPostsViewController) in
            controller.fetchType = fetchType
        }
    }
}
StatusBarable

Manages the status bar view:

class ViewController: UIViewController, StatusBarable {

    let application = UIApplication.shared
    var statusBar: UIView?

    override func viewDidLoad() {
        showStatusBar()

        NotificationCenter.default.addObserver(
            for: UIDevice.orientationDidChangeNotification,
            selector: #selector(deviceOrientationDidChange),
            from: self
        )
    }
}

private extension ViewController {

    @objc func deviceOrientationDidChange() {
        removeStatusBar()
        showStatusBar()
    }
}

Image of StatusBarable

UICollectionView

Register cells in strongly-typed manner:

collectionView.register(nib: TransactionViewCell.self)

Get reusable cells through subscript:

// Before
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? TransactionViewCell 
// After
let cell: TransactionViewCell = collectionView[indexPath]
UIImage

Save an image to disk as .png:

imageView.image.pngToDisk() -> "/.../Library/Caches/img_ezoPU8.png"

Convert a color to an image:

let image = UIImage(from: .lightGray)
button.setBackgroundImage(image, for: .selected)
UILabel

Enable data detectors like in UITextView:

// Before
let label = UITextView()
label.isEditable = false
label.isScrollEnabled = false
label.textContainer.lineFragmentPadding = 0
label.textContainerInset = .zero
label.backgroundColor = .clear
label.dataDetectorTypes = [.phoneNumber, .link, .address, .calendarEvent]
// After
let label = UILabelView(
    dataDetectorTypes: [.phoneNumber, .link, .address, .calendarEvent]
)
UIStackView

Add a view with animation:

stackView.addArrangedSubview(view1, animated: true)

Add a list of views:

stackView.addArrangedSubviews([view1, view2, view3])
stackView.addArrangedSubviews([view1, view3], animated: true)

Remove and deinitialize all views:

stackView
    .deleteArrangedSubviews()
    .addArrangedSubviews([view2, view3]) // Chain commands
UIStoryboard

Instantiate a view controller using convention of storyboard identifier matching class name:

let storyboard = UIStoryboard("Main")
let controller: MyViewController = storyboard.instantiateViewController()
UITableView

Register cells in strongly-typed manner:

tableView.register(nib: TransactionViewCell.self)

Get reusable cells through subscript:

// Before
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? TransactionViewCell 
// After
let cell: TransactionViewCell = tableView[indexPath]

Scroll to top or bottom:

tableView.scrollToTop()
tableView.scrollToBottom()

Set selection color of cell:

// Before
let backgroundView = UIView()
backgroundView.backgroundColor = .lightGray
cell.selectedBackgroundView = backgroundView
// After
cell.selectionColor = .lightGray

Strongly-typed cell identifiers for static tables:

class ViewController: UITableViewController {
    
}

extension ViewController: CellIdentifiable {
    
    // Each table view cell must have an identifier that matches a case
    enum CellIdentifier: String {
        case about
        case subscribe
        case feedback
        case tutorial
    }
}

extension ViewController {
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        guard let cell = tableView.cellForRow(at: indexPath),
            let identifier = CellIdentifier(from: cell) else {
                return
        }
        
        // Easily reference the associated cell
        switch identifier {
        case .about:
            router.showAbout()
        case .subscribe:
            router.showSubscribe()
        case .feedback:
            router.sendFeedback(
                subject: .localizedFormat(.emailFeedbackSubject, constants.appDisplayName!)
            )
        case .tutorial:
            router.startTutorial()
        }
    }
}
UITextView

A placeholder like in UITextField:

let textView = PlaceholderTextView()
textView.placeholder = "Enter message..."

Interface Builder compatible via Attributes inspector:

Image of GradientView

UIToolbar

Create a toolbar that toggles to next field or dismisses keyboard:

class ViewController: UIViewController {

    private lazy var inputDoneToolbar: UIToolbar = .makeInputDoneToolbar(
        target: self,
        action: #selector(endEditing)
    )
}

extension ViewController: UITextViewDelegate {

    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        textView.inputAccessoryView = inputDoneToolbar
        return true
    }
}

Image of UIToolbar

UIView

Sometimes isHidden can be unintuitive:

myView.isVisible = isAuthorized && role.within[.admin, .author]

Adjust border, corners, and shadows conveniently:

myView.borderColor = .red
myView.borderWidth = 1
myView.cornerRadius = 3
myView.addShadow()

Image of UIView-Shadow

Animate visibility:

myView.fadeIn()
myView.fadeOut()

Add activity indicator to center of view:

let activityIndicator = myView.makeActivityIndicator()
activityIndicator.startAnimating()

Create instance from XIB:

let control = MyView.loadNIB()
control.isAwesome = true
addSubview(control)

Present a view modally:

class ModalView: UIView, PresentableView {

    @IBOutlet weak var contentView: UIView!

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Dismiss self when tapped on background
        dismiss()
    }

    @IBAction func closeButtonTapped() {
        dismiss()
    }
}

class ViewController: UIViewController {

    @IBAction func modalButtonTapped() {
        let modalView = ModalView.loadNIB()
        present(control: modalView)
    }
}

Image of PresentableView

UIViewController

Display an alert to the user:

// Before
let alertController = UIAlertController(title: "My Title", message: "This is my message.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default) { alert in
    print("OK tapped")
}
present(alertController, animated: true, completion: nil)
// After
present(alertController: "My Title", message: "This is my message.") {
    print("OK tapped")
}

Display a Safari web page to the user:

// Before
let safariController = SFSafariViewController(URL: URL(string: "https://apple.com")!)
safariController.modalPresentationStyle = .overFullScreen
present(safariController, animated: true, completion: nil)
// After
present(safari: "https://apple.com")
show(safari: "https://apple.com")

Display an action sheet to the user:

present(
    actionSheet: "Test Action Sheet",
    message: "Choose your action",
    popoverFrom: sender,
    additionalActions: [
        UIAlertAction(title: "Action 1") { },
        UIAlertAction(title: "Action 2") { },
        UIAlertAction(title: "Action 3") { }
    ],
    includeCancelAction: true
)

Display a prompt to the user:

// Before
let alertController = UIAlertController(
    title: "Test Prompt",
    message: "Enter user input.",
    preferredStyle: .alert
)

alertController.addAction(
    UIAlertAction(title: "Cancel", style: .cancel) { _ in }
)

alertController.addTextField {
    $0.placeholder = "Your placeholder here"
    $0.keyboardType = .phonePad
    $0.textContentType = .telephoneNumber
}

alertController.addAction(
    UIAlertAction(title: "Ok", style: .default) { _ in
        guard let text = alertController.textFields?.first?.text else {
            return
        }

        print("User response: \($0)")
    }
)

present(alertController, animated: animated, completion: nil)
// After
present(
    prompt: "Test Prompt",
    message: "Enter user input.",
    placeholder: "Your placeholder here",
    configure: {
        $0.keyboardType = .phonePad
        $0.textContentType = .telephoneNumber
    },
    response: {
        print("User response: \($0)")
    }
)

Image of UIViewController Prompt

Display a share activity with Safari added:

let safariActivity = UIActivity.make(
    title: .localized(.openInSafari),
    imageName: "safari-share",
    imageBundle: .zamzamKit,
    handler: {
        guard SCNetworkReachability.isOnline else {
            return self.present(alert: "Device must be online to view within the browser.")
        }
        
        UIApplication.shared.open(link)
    }
)

present(
    activities: ["Test Title", link],
    popoverFrom: sender,
    applicationActivities: [safariActivity]
)
UIWindow

Get the top view controller for the window:

window?.topViewController

watchOS

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().reloadTimelineForComplication()

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()
WatchSession

Communicate conveniently between iOS and watchOS:

// iOS
class WatchViewController: UIViewController {
    
    @IBOutlet weak var receivedContextLabel: UILabel!
    @IBOutlet weak var sentContextLabel: UILabel!
    @IBOutlet weak var receivedUserInfoLabel: UILabel!
    @IBOutlet weak var sentUserInfoLabel: UILabel!
    @IBOutlet weak var receivedMessageLabel: UILabel!
    @IBOutlet weak var sentMessageLabel: UILabel!
    
    var watchSession: WatchSession {
        return AppDelegate.watchSession
    }
    
    /// Another way to add observer
    var userInfoObserver: WatchSession.ReceiveUserInfoObserver {
        return Observer { [weak self] result in
            DispatchQueue.main.async {
                self?.receivedUserInfoLabel.text = result["value"] as? String ?? ""
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /// One way to add observer
        watchSession.addObserver(forApplicationContext: Observer { [weak self] result in
            DispatchQueue.main.async {
                self?.receivedContextLabel.text = result["value"] as? String ?? ""
            }
        })
        
        watchSession.addObserver(forUserInfo: userInfoObserver)
        watchSession.addObserver(forMessage: messageObserver)
    }
    
    @IBAction func sendContextTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(context: value)
        sentContextLabel.text = value["value"] ?? ""
    }
    
    @IBAction func sendUserInfoTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(userInfo: value)
        sentUserInfoLabel.text = value["value"] ?? ""
    }
    
    @IBAction func sendMessageTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(message: value)
        sentMessageLabel.text = value["value"] ?? ""
    }
    
    deinit {
        watchSession.removeObservers()
    }
}

extension WatchViewController {
    
    /// Another way to add observer
    var messageObserver: WatchSession.ReceiveMessageObserver {
        return Observer { [weak self] message, replyHandler in
            DispatchQueue.main.async {
                self?.receivedMessageLabel.text = message["value"] as? String ?? ""
            }
        }
    }
}
// watchOS
class ExtensionDelegate: NSObject, WKExtensionDelegate {
    static let watchSession = WatchSession()
}

class InterfaceController: WKInterfaceController {

    @IBOutlet var receivedContextLabel: WKInterfaceLabel!
    @IBOutlet var sentContextLabel: WKInterfaceLabel!
    @IBOutlet var receivedUserInfoLabel: WKInterfaceLabel!
    @IBOutlet var sentUserInfoLabel: WKInterfaceLabel!
    @IBOutlet var receivedMessageLabel: WKInterfaceLabel!
    @IBOutlet var sentMessageLabel: WKInterfaceLabel!
    
    var watchSession: WatchSession {
        return ExtensionDelegate.watchSession
    }
    
    override func awake(withContext: Any?) {
        super.awake(withContext: withContext)
        
        watchSession.addObserver(forApplicationContext: Observer { [weak self] result in
            DispatchQueue.main.async {
                self?.receivedContextLabel.setText(result["value"] as? String ?? "")
            }
        })
        
        watchSession.addObserver(forUserInfo: Observer { [weak self] result in
            DispatchQueue.main.async {
                self?.receivedUserInfoLabel.setText(result["value"] as? String ?? "")
            }
        })
        
        watchSession.addObserver(forMessage: Observer { [weak self] message, replyHandler in
            DispatchQueue.main.async {
                self?.receivedMessageLabel.setText(message["value"] as? String ?? "")
            }
        })
    }
    
    @IBAction func sendContextTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(context: value)
        sentContextLabel.setText(value["value"] ?? "")
    }
    
    @IBAction func sendUserInfoTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(userInfo: value)
        sentUserInfoLabel.setText(value["value"] ?? "")
    }
    
    @IBAction func sendMessageTapped() {
        let value = ["value": "\(Date())"]
        watchSession.transfer(message: value)
        sentMessageLabel.setText(value["value"] ?? "")
    }
}

Image of WatchSession

WKViewController

Display an alert to the user:

present(alert: "Test Alert")

Display an action sheet to the user:

present(
    actionSheet: "Test",
    message: "This is the message.",
    additionalActions: [
        WKAlertAction(title: "Action 1", handler: {}),
        WKAlertAction(title: "Action 2", handler: {}),
        WKAlertAction(title: "Action 3", style: .destructive, handler: {})
    ],
    includeCancelAction: true
)

Display an side-by-side alert to the user:

present(
    sideBySideAlert: "Test",
    message: "This is the message.",
    additionalActions: [
        WKAlertAction(title: "Action 1", handler: {}),
        WKAlertAction(title: "Action 2", style: .destructive, handler: {}),
        WKAlertAction(title: "Action 3", handler: {})
    ]
)

Helpers

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
CoreLocation

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

CLLocationManager.isAuthorized -> bool

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)

Approximate comparison of coordinates rounded to 3 decimal places (about 100 meters):

let coordinate1 = CLLocationCoordinate2D(latitude: 43.6532, longitude: -79.3832)
let coordinate2 = CLLocationCoordinate2D(latitude: 43.6531, longitude: -79.3834)
let coordinate3 = CLLocationCoordinate2D(latitude: 43.6522, longitude: -79.3822)

coordinate1 ~~ coordinate2 -> true
coordinate1 ~~ coordinate3 -> false

Location worker that offers easy authorization and observable closures (read more):

class LocationViewController: UIViewController {

    @IBOutlet weak var outputLabel: UILabel!
    
    var locationsWorker: LocationsWorkerType = LocationsWorker(
        desiredAccuracy: kCLLocationAccuracyThreeKilometers,
        distanceFilter: 1000
    )
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        locationsWorker.addObserver(locationObserver)
        locationsWorker.addObserver(headingObserver)
        
        locationsWorker.requestAuthorization(
            for: .whenInUse,
            startUpdatingLocation: true,
            completion: { granted in
                guard granted else { return }
                self.locationsWorker.startUpdatingHeading()
            }
        )
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        locationsWorker.removeObservers()
    }
    
    deinit {
        locationsWorker.removeObservers()
    }
}

extension LocationViewController {
    
    var locationObserver: Observer<LocationsWorker.LocationHandler> {
        return Observer { [weak self] in
            self?.outputLabel.text = $0.description
        }
    }
    
    var headingObserver: Observer<LocationsWorker.HeadingHandler> {
        return Observer {
            print($0.description)
        }
    }
}
Localization

Strongly-typed localizable keys that's also XLIFF export friendly (read more):

// First define localization keys
extension Localizable {
    static let ok = Localizable(NSLocalizedString("ok.dialog", comment: "OK text for dialogs"))
    static let cancel = Localizable(NSLocalizedString("cancel.dialog", comment: "Cancel text for dialogs"))
    static let next = Localizable(NSLocalizedString("next.dialog", comment: "Next text for dialogs"))
}

// Then use strongly-typed localization keys
myLabel1.text = .localized(.ok)
myLabel2.text = .localized(.cancel)
myLabel3.text = .localized(.next)
Migration

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

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let migration = Migration()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions 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.0", withBuild: "1") {
                print("Migrate to 1.0 (1) occurred.")
            }
            .perform(forVersion: "1.0", withBuild: "2") {
                print("Migrate to 1.0 (2) occurred.")
            }
            
        return true
    }
}
RateLimit

A throttler that will ignore work items until the time limit for the preceding call is over:

let limiter = Throttler(limit: 5)
var value = 0

limiter.execute {
    value += 1
}

limiter.execute {
    value += 1
}

limiter.execute {
    value += 1
}

sleep(5)

limiter.execute {
    value += 1
}

// value == 2

A debouncer that will delay work items until time limit for the preceding call is over:

let limiter = Debouncer(limit: 5)
var value = ""

func sendToServer() {
    limiter.execute {
        // Sends to server after no typing for 5 seconds
        // instead of once per character, so:
        value == "hello" -> true
    }
}

value.append("h")
sendToServer()

value.append("e")
sendToServer()

value.append("l")
sendToServer()

value.append("l")
sendToServer()

value.append("o")
sendToServer()
Result

Used to represent whether an asynchronous request was successful or encountered an error:

// Declare the function with a completion handler of `Result` type
func fetch(id: Int, completion: @escaping (Result<Author, ZamzamError>) -> Void) {
    guard id > 0 else {
        completion(.failure(.nonExistent))
        return
    }

    DispatchQueue.global().async {
        completion(.success(Author(...)))
    }
}

// Call the asynchronous function and determine the response
fetch(id: 123) {
    guard let value = $0.value, $0.isSuccess else {
        print("An error occurred: \($0.error ?? .general)")
        return
    }

    print(value)
}
SystemConfiguration

Determine if the device is connected to a network:

import SystemConfiguration

SCNetworkReachability.isOnline
SynchronizedArray

A thread-safe array that allows concurrent reads and exclusive writes (read more):

var array = SynchronizedArray<Int>()

DispatchQueue.concurrentPerform(iterations: 1000) { index in
    array.append(index)
}
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 $0.isSuccess, let attachment = $0.value else {
        return log(error: "Could not download the remote resource (\(urlString)): \($0.error.debugDescription).")
    }

    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()
UserDefaults

Strongly-typed UserDefault keys:

// First define keys
extension UserDefaults.Keys {
    static let testString = UserDefaults.Key<String?>("testString")
    static let testInt = UserDefaults.Key<Int?>("testInt")
    static let testBool = UserDefaults.Key<Bool?>("testBool")
    static let testArray = UserDefaults.Key<[Int]?>("testArray")
}

// Then use strongly-typed values
let testString: String? = UserDefaults.standard[.testString]
let testInt: Int? = UserDefaults.standard[.testInt]
let testBool: Bool? = UserDefaults.standard[.testBool]
let testArray: [Int]? = UserDefaults.standard[.testArray]

Author

Zamzam Inc., http://zamzam.io

License

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