ZamzamKit 4.0.1

ZamzamKit 4.0.1

测试已测试
Lang语言 SwiftSwift
许可证 MIT
Released上次发布2019年1月
SPM支持SPM

Zamzam Inc.维护。



ZamzamKit 4.0.1

ZamzamKit

Build Status Platform Swift Xcode Carthage compatible Version MIT

ZamzamKit是一个Swift框架,使用一系列对标准库、Foundation和UIKit类和协议的小工具扩展,用于快速开发。

安装

Carthage

github "ZamzamInc/ZamzamKit"添加到您的Cartfile中。

CocoaPods

pod "ZamzamKit"添加到您的Podfile中。

框架
  1. 下载最新的ZamzamKit版本并解压缩。
  2. 前往您的Xcode项目的"常规"设置。将ZamzamKit.framework和ZamzamKit.framework从ios/tvos/watchos/目录中适当的Swift版本目录拖动到"已嵌入的二进制文件"部分。确保"如果需要则复制项"被选中(除非在您的项目中使用多个平台),然后单击完成。
  3. 在您的单元测试目标的"构建设置"中,将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

使用 UTCPOSIX 正规化日期计算和数据存储

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

带有徽章值的按钮

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

具有渐变效果的 UIView

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

通过“用户定义的运行时属性”与 Interface Builder 兼容

Image of GradientView

KeyboardScrollView

当显示键盘时,automaticallyAdjustsInsetsForKeyboard 属性将滚动视图的间距扩展

Image of KeyboardScrollView

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)
    }
NextResponderTextField

一个扩展 UITextView,将“返回键”连接到另一个 UIResponder

Image of NextResponderTextField

Image of NextResponderTextField2

RoundedView

带有圆形蒙版的 UIViewUIImageUIButton 子类

Image of RoundedView

路由

符合 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()
    }
}

Image of StatusBarable

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 兼容

Image of GradientView

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

Image of UIToolbar

UIView

有时 isHidden 可能是不可以为宜的

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

方便地调整边框、角和阴影

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

Image of UIView-Shadow

动画显示性

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)
    }
}

Image of PresentableView

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

Image of UIViewController Prompt

显示带 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

iOSwatchOS 之间方便地通信

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

向用户显示警告

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文件。