ZamzamKit
ZamzamKit是一个Swift框架,使用一系列对标准库、Foundation和UIKit类和协议的小工具扩展,用于快速开发。
安装
Carthage
将github "ZamzamInc/ZamzamKit"
添加到您的Cartfile
中。
CocoaPods
将pod "ZamzamKit"
添加到您的Podfile
中。
框架
- 下载最新的
ZamzamKit
版本并解压缩。 - 前往您的Xcode项目的"常规"设置。将ZamzamKit.framework和ZamzamKit.framework从
ios/
、tvos/
或watchos/
目录中适当的Swift版本目录拖动到"已嵌入的二进制文件"部分。确保"如果需要则复制项"被选中(除非在您的项目中使用多个平台),然后单击完成。 - 在您的单元测试目标的"构建设置"中,将ZamzamKit.framework的父路径添加到"框架搜索路径"部分。
使用
标准库
数组
获取数组中的唯一元素
[1, 1, 3, 3, 5, 5, 7, 9, 9].distinct -> [1, 3, 5, 7, 9]
通过值从数组中删除元素
var array = ["a", "b", "c", "d", "e"]
array.remove("c")
array -> ["a", "b", "d", "e"]
轻松获取数组切片的数组版本
["a", "b", "c", "d", "e"].prefix(3).array
集合
如果存在,安全地检索给定索引处的元素
// 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
字典
移除所有等于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
可比较的
确定一个值是否包含在值数组中
"b".within(["a", "b", "c"]) -> true
let status: OrderStatus = .cancelled
status.within([.requeseted, .accepted, .inProgress]) -> false
数字
四舍五入双精度浮点数、单精度浮点数或任何浮点类型
123.12312421.rounded(toPlaces: 3) -> 123.123
Double.pi.rounded(toPlaces: 2) -> 3.14
字符串
创建给定长度的新的随机字符串
String(random: 10) -> "zXWG4hSgL9"
String(random: 4, prefix: "PIN-") -> "PIN-uSjm"
在字符串上安全地使用索引和范围
let value = "Abcdef123456"
value[3] -> "d"
value[3..<6] -> "def"
value[3...6] -> "def1"
value[3...] -> "def123456"
value[3...99] -> nil
value[99] -> nil
对字符串进行验证以匹配规范格式
"[email protected]".isEmail -> true
"123456789".isNumber -> true
"zamzam".isAlpha -> true
"zamzam123".isAlphaNumeric -> true
删除两端的空间或换行符
" Abcdef123456 \n\r ".trimmed -> "Abcdef123456"
截断到指定字符数
"Abcdef123456".truncated(3) -> "Abc..."
"Abcdef123456".truncated(6, trailing: "***") -> "Abcdef***"
确定给定的值是否包含
"1234567890".contains("567") -> true
"abc123xyz".contains("ghi") -> false
每隔第n个字符插入分隔符
"1234567890".separate(every: 2, with: "-") -> "12-34-56-78-90"
使用正则表达式模式进行匹配
"1234567890".match(regex: "^[0-9]+?$") -> true
"abc123xyz".match(regex: "^[A-Za-z]+$") -> false
替换正则表达式模式的出现
"aa1bb22cc3d888d4ee5".replacing(regex: "\\d", with: "*") -> "aa*bb**cc*d***d*ee*"
删除HTML以获得纯文本
"<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."
编码和解码器
value.urlEncoded
value.urlDecoded
value.htmlDecoded
value.base64Encoded
value.base64Decoded
value.base64URLEncoded
轻松获取子字符串的字符串版本
"hello world".prefix(5).string
确定可选字符串是否为
nil
或没有字符
var value: String? = "test 123"
value.isNilOrEmpty
将序列和字典转换为JSON字符串
// 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
基础库
包
获取任何包中文件的正文
Bundle.main.string(file: "Test.txt") -> "This is a test. Abc 123.\n"
获取任何包中属性列表文件的正文
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
颜色
额外的颜色初始化器
UIColor(hex: 0x990000)
UIColor(hex: 0x4286F4)
UIColor(rgb: (66, 134, 244))
货币
一种将数值与其文本货币表示形式之间进行转换的格式化程序
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(timeIntervalSinceNow: -100).isPast -> true
Date(timeIntervalSinceNow: 100).isPast -> false
Date(timeIntervalSinceNow: 100).isFuture -> true
Date(timeIntervalSinceNow: -100).isFuture -> false
确定日期是今天、昨天还是明天
Date().isToday -> true
Date(timeIntervalSinceNow: -90_000).isYesterday -> true
Date(timeIntervalSinceNow: 90_000).isTomorrow -> true
确定日期是否在周内或周末期间
Date().isWeekday -> false
Date().isWeekend -> true
获取一天的开始或结束
Date().startOfDay -> "2018/11/21 00:00:00"
Date().endOfDay -> "2018/11/21 23:59:59"
获取一个月的开始或结束
Date().startOfMonth -> "2018/11/01 00:00:00"
Date().endOfMonth -> "2018/11/30 23:59:59"
确定日期是否在两个日期之间
let date = Date()
let date1 = Date(timeIntervalSinceNow: 1000)
let date2 = Date(timeIntervalSinceNow: -1000)
date.isBetween(date1, date2) -> true
确定日期是否超出指定的时间窗口
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
从字符串创建日期
Date(fromString: "2018/11/01 18:15")
Date(fromString: "1440/03/01 18:31", calendar: Calendar(identifier: .islamic))
将日期格式化为字符串
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"
将时间间隔格式化为计时器显示
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"
获取时间的小数表示形式
Date(fromString: "2018/10/23 18:15").timeToDecimal -> 18.25
增加年、月、日、小时或分钟
let date = Date()
date + .years(1)
date + .months(2)
date - .days(4)
date - .hours(6)
date + .minutes(12)
date + .days(5, Calendar(identifier: .chinese))
在时间间隔单位之间进行转换
let diff = date.timeIntervalSince(date2) -> 172,800 seconds
diff.minutes -> 2,800 minutes
diff.hours -> 48 hours
diff.days -> 2 days
时区上下文和偏移量
let timeZone = TimeZone(identifier: "Europe/Paris")
timeZone?.isCurrent -> false
timeZone?.offsetFromCurrent -> -21600
使用
UTC
和POSIX
正规化日期计算和数据存储
let calendar: Calendar = .posix
let locale: Locale = .posix
文件管理器
获取文件或文件的URL或文件系统路径
FileManager.default.url(of: fileName, from: .documentDirectory)
FileManager.default.path(of: fileName, from: .cachesDirectory)
获取目录中文件的URL或文件系统路径
FileManager.default.urls(from: .documentDirectory)
FileManager.default.paths(from: .downloadsDirectory)
从远程检索文件并将其持久化到本地磁盘
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.
}
前缀
如果非nil则分配值
var test: Int? = 123
var value: Int? = nil
test ?= value
// test == 123
value = 456
test ?= value
// test == 456
通知中心
发布和观察函数的简写
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
轻松获取字符串的属性字符串版本
"Abc".attributed
"Lmn".mutableAttributed
"Xyz".mutableAttributed([
.font: UIFont.italicSystemFont(ofSize: .systemFontSize),
.foregroundColor, value: UIColor.green
])
将属性字符串相加
label.attributedText = "Abc".attributed + " def " +
"ghi".mutableAttributed([
.underlineStyle: NSUnderlineStyle.single.rawValue
])
对象
在初始化后立即使用闭包设置属性
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
添加或删除查询字符串参数
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
应用
将
AppDelegate
分割成可插入的模块
// 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.")
}
}
可插入模块技术也适用于 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()
}
}
背景
轻松执行 长时间运行的背景任务
BackgroundTask.run(for: application) { task in
// Perform finite-length task...
task.end()
}
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
具有渐变效果的
UIView
@IBOutlet weak var gradientView: GradientView! {
didSet {
gradientView.firstColor = .blue
gradientView.secondColor = .red
}
}
通过“用户定义的运行时属性”与 Interface Builder 兼容
MailComposer
使用可选的主题、正文或附件编写电子邮件
// 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)
}
路由
符合
Routable
的类型可在UIViewController
中提供强类型故事板路由的扩展(《了解更多》 http://basememara.com/protocol-oriented-router-in-swift/)
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"
}
}
符合
Router
的类型可以指定一个弱UIViewController
实例
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
管理状态栏视图
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
以强类型方式注册单元格
collectionView.register(nib: TransactionViewCell.self)
通过下标获取可重用的单元格
// Before
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? TransactionViewCell
// After
let cell: TransactionViewCell = collectionView[indexPath]
UIImage
将图像保存到磁盘作为 .png 文件
imageView.image.pngToDisk() -> "/.../Library/Caches/img_ezoPU8.png"
将颜色转换为图像
let image = UIImage(from: .lightGray)
button.setBackgroundImage(image, for: .selected)
UILabel
启用数据检测器,如
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
以动画方式添加视图
stackView.addArrangedSubview(view1, animated: true)
添加视图列表
stackView.addArrangedSubviews([view1, view2, view3])
stackView.addArrangedSubviews([view1, view3], animated: true)
移除并初始化所有视图
stackView
.deleteArrangedSubviews()
.addArrangedSubviews([view2, view3]) // Chain commands
UIStoryboard
使用故事板标识符匹配类名约定来实例化视图控制器
let storyboard = UIStoryboard("Main")
let controller: MyViewController = storyboard.instantiateViewController()
UITableView
以强类型方式注册单元格
tableView.register(nib: TransactionViewCell.self)
通过下标获取可重用的单元格
// Before
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? TransactionViewCell
// After
let cell: TransactionViewCell = tableView[indexPath]
滚动到顶部或底部
tableView.scrollToTop()
tableView.scrollToBottom()
设置单元格的选择颜色
// Before
let backgroundView = UIView()
backgroundView.backgroundColor = .lightGray
cell.selectedBackgroundView = backgroundView
// After
cell.selectionColor = .lightGray
静态表的强类型单元格标识符
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
类似于
UITextField
的提示占位符
let textView = PlaceholderTextView()
textView.placeholder = "Enter message..."
通过属性检查器通过 Interface Builder 兼容
UIToolbar
创建一个在下一个字段之间切换或关闭键盘的工具栏
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
有时
isHidden
可能是不可以为宜的
myView.isVisible = isAuthorized && role.within[.admin, .author]
方便地调整边框、角和阴影
myView.borderColor = .red
myView.borderWidth = 1
myView.cornerRadius = 3
myView.addShadow()
动画显示性
myView.fadeIn()
myView.fadeOut()
将活动指示器添加到视图中心
let activityIndicator = myView.makeActivityIndicator()
activityIndicator.startAnimating()
从
XIB
创建实例
let control = MyView.loadNIB()
control.isAwesome = true
addSubview(control)
以模态方式呈现视图
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
向用户显示警告
// 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")
}
向用户显示 Safari 网页
// 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")
向用户显示操作表
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
)
向用户显示提示
// 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)")
}
)
显示带 Safari 的共享活动
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
获取窗口的最顶层视图控制器
window?.topViewController
watchOS
CLKComplicationServer
使所有复分的所有时间线数据无效并重新加载
// Before
guard let complications = activeComplications, !complications.isEmpty else { return }
complications.forEach { reloadTimeline(for: $0) }
// After
CLKComplicationServer.sharedInstance().reloadTimelineForComplication()
扩展所有复分的所有时间线数据
// Before
guard let complications = activeComplications, !complications.isEmpty else { return }
complications.forEach { extendTimeline(for: $0) }
// After
CLKComplicationServer.sharedInstance().extendTimelineForComplications()
WatchSession
在
iOS
和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"] ?? "")
}
}
WKViewController
向用户显示警告
present(alert: "Test Alert")
向用户显示操作表
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
)
向用户显示并排的警告
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: {})
]
)
帮助器
AppInfo
获取当前应用的详细信息
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
确定位置服务是否启用且在始终或使用时被授权
CLLocationManager.isAuthorized -> bool
获取坐标的位置详细信息
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)
}
从坐标列表中获取最接近或最远的地理位置
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)
将坐标近似比较,四舍五入到3位小数(大约100米)
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
提供简单授权和可观察闭包的位置工作器(了解更多)
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)
}
}
}
本地化
强类型可本地化密钥,同时也友好地导出
XLIFF
(了解更多)
// 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)
迁移
管理只在应用版本更新时需要运行一次的代码块
@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
一个节流器,会忽略工作项直到先前调用的超时时间结束
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
一个防抖器,会将工作项延迟到先前调用的超时时间结束
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()
结果
用于表示异步请求是成功还是遇到了错误
// 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
确定设备是否连接到网络
import SystemConfiguration
SCNetworkReachability.isOnline
SynchronizedArray
一个线程安全的数组,允许并发读取和独占写入(了解更多)
var array = SynchronizedArray<Int>()
DispatchQueue.concurrentPerform(iterations: 1000) { index in
array.append(index)
}
UserNotification
注册本地和远程通知以及它支持的分类和动作
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.")
}
)
获取所有挂起或已发送的用户通知的列表
UNUserNotificationCenter.current().getNotificationRequests { notifications in
notifications.forEach {
print($0.identifier)
}
}
通过标识符找到挂起或已发送的通知请求
UNUserNotificationCenter.current().get(withIdentifier: "abc123") {
print($0?.identifier)
}
UNUserNotificationCenter.current().get(withIdentifiers: ["abc123", "xyz789"]) {
$0.forEach {
print($0.identifier)
}
}
确定挂起或已发送的通知请求是否存在
UNUserNotificationCenter.current().exists(withIdentifier: "abc123") {
print("Does notification exist: \($0)")
}
安排本地通知以供投递
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"
)
从网页获取远程图像并将其转换为用户通知附件
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]
)
}
通过标识符、类别或全部移除挂起或已发送的通知请求
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
强类型UserDefault密钥
// 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]
作者
Zamzam Inc.,http://zamzam.io
许可
ZamzamKit遵循MIT许可协议。更多详情请参阅LICENSE文件。