ZamzamInc / Zamzamkit
Programming Languages
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:
// 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:
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:
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:
"<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(for: calendar)
date.isWeekday(for: calendar)
date.isCurrentMonth(for: calendar)
date.isToday(for: calendar)
date.startOfDay(for: calendar)
date.startOfMonth(for: 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.
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
andURLRequest
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:
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):
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:
@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 toLogManager
(console andos_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:
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
UIKit
BadgeBarButtonItem
A bar button item with a badge value:
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)
).apply {
$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":
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)
}
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()
}
}
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(name: "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:
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
}
}
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()
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)
}
}
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)")
}
)
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
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()
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: {})
]
)
Author
- Zamzam, https://zamzam.io
- Basem Emara, https://basememara.com
License
ZamzamKit
is available under the MIT license. See the LICENSE file for more info.