UtiliKit

CI Status Version Carthage compatible License Platform codecov codebeat badge

目的

该库为 iOS 应用程序提供了一些有用的、常见的补充功能。 这些扩展、协议和结构旨在简化样板代码,并消除常见的“字符串类型”用例。

关键概念

该库分为 7 个部分,可通过 CocoaPods 子规范获得。

用法

实例化

可重用视图

注册和出列单元格、集合视图补充视图、表视图标题和页脚以及注释,就像在其呈现视图上调用注册一样简单,并在 collectionView(_:, cellForItemAt:) -> UICollectionViewCell 或等效函数中出列它们。

class ViewController: UIViewController {
    @IBOutlet var collectionView: UICollectionView!
    let dataA: [Int] = [0, 1, 2]
    let dataB: [Int] = [0, 1, 2]
    let dataC: [Int] = [0, 1, 2]
    var data: [[Int]] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.register(ProgrammaticCell.self)
        collectionView.registerHeaderFooter(ProgrammaticHeaderFooterView.self)
        collectionView.delegate = self
        collectionView.dataSource = self
    }
}

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {

    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data[section].count
    }

    public func numberOfSections(in collectionView: UICollectionView) -> Int {
        return data.count
    }

    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // Dequeue a "ProgrammaticCell" from the collection view using only the cell type
        let cell: ProgrammaticCell = collectionView.dequeueReusableCell(for: indexPath)
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

        // You need only provide the desired type and SupplementaryElementKind to receive a typed UICollectionReusableView
        switch kind {
        case UICollectionElementKindSectionHeader:
            let header: ProgrammaticHeaderFooterView = collectionView.dequeueReusableSupplementaryView(of: .sectionHeader, for: indexPath)
            return header
        default:
            let footer: ProgrammaticHeaderFooterView = collectionView.dequeueReusableSupplementaryView(of: .sectionFooter, for: indexPath)
            footer.kind = .sectionFooter
            return footer
        }
    }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: collectionView.bounds.width, height: 100)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        return CGSize(width: collectionView.bounds.width, height: 50)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        return CGSize(width: collectionView.bounds.width, height: 25)
    }
}

视图控制器

要从故事板实例化视图控制器,您只需为故事板创建一个 Storyboard.Identifier 并定义返回类型。 一个简单的实现可能如下所示

extension UIStoryboard.Identifier {

    static let myStoryboard = UIStoryboard.Identifier(name: "MyStoryboard")
}

class ViewController: UIViewController {

    func presentMyViewController() {
        let vc: MyViewController = UIStoryboard(identifier: .myStoryboard).instantiateViewController()
        present(vc, animated: true)
    }
}

通用

FileManager 扩展

作为 FileManager 的扩展,提供了几种便捷方法

let documentsDirectory = FileManager.default.documentsDirectory
let cachesDirectory = FileManager.default.cachesDirectory
let appSupportDirectory = FileManager.default.applicationSupportDirectory
let sharedContainerURL = FileManager.default.sharedContainerURL(forSecurityApplicationGroupIdentifier: "com.app.group")

UIView 扩展

作为 UIView 的扩展,提供了几种便捷方法,主要用于轻松地将子视图约束到其父视图

let myView = UIView(frame: .zero)
view.addSubview(myView, constrainedToSuperview: true)

let anotherView = UIView(frame: .zero)
anotherView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(anotherView)

let insets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
anotherView.constrainEdgesToSuperview(with: insets)

版本号

将版本号导入到面向用户的字符串中只需要一个函数调用。 *注意:如果提供的版本配置包含无效的密钥,此函数将抛出错误。 一个简单的实现可能如下所示

func printVersions() {
    do {
        let customVersionString = try Bundle.main.versionString(for: MyVersionConfig(), isShortVersion: false)
        let verboseVersionString = try Bundle.main.verboseVersionString()
        let versionString = try Bundle.main.versionString()

        print(customVersionString)
        print(verboseVersionString)
        print(versionString)
    } catch {
        print(error)
    }
}

Timeless 日期

Timeless Date 是一个简单的抽象,它从 Date 中删除时间,并使用 Calendar 进行计算。 这对于日历和旅行用例特别有用,因为查看某件事情还有多少天通常比它们之间的小时数 / 24 重要。

func numberOfDaysBetween(start: TimelessDate, finish: TimelessDate) -> DateInterval {
    return start.dateIntervalSince(finish)
}

func isOneWeekFrom(checkout: TimelessDate) -> Bool {
    return checkout.dateIntervalSince(TimelessDate()) <= 7
}

此结构还消除了将天、小时、分钟和秒添加到日期的不精确计算,并用 Calendar 计算替换它们。

func addOneHourTo(date: Date) -> Date {
    return date.adding(hours: 1)
}

ContainerViewController

ContainerViewController 是一种用于管理多个子视图控制器的解决方案,它管理子控制器的生命周期。 这使您可以专注于视图的导航结构以及它们之间的转换。

containerViewController.managedChildren = [Child(identifier: "A", viewController: controllerA), Child(identifier: "B", viewController: controllerB)]

containerViewController.willMove(toParent: self)

addChild(containerViewController)
containerView.addSubview(containerViewController.view)
containerViewController.view.frame = containerView.bounds

containerViewController.didMove(toParent: self)

此时,在容器的子项之间转换非常简单。

let child = ...
containerViewController.transitionToController(for: child)

容器还有几个委托回调,可以帮助自定义其行为。 其中,是一个返回 UIViewControllerAnimatedTransitioning 对象的函数。

func containerViewController(_ container: ContainerViewController, animationControllerForTransitionFrom source: UIViewController, to destination: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if useCustomAnimator, let sourceIndex = container.index(ofChild: source), let destinationIndex = container.index(ofChild: destination) {
        return WipeTransitionAnimator(withStartIndex: sourceIndex, endIndex: destinationIndex)
    }

    return nil
}

ActiveLabel

ActiveLabel 是一个 UILabel 子类,当其 text 属性设置为 nil 时,它会向您的标签添加水平活动指示器。 您可以在代码或 Interface Builder 中对该视图进行相当多的自定义,以满足您的特定需求。 此子类的目的是在将数据加载到标签中时,在标签级别具有可视指示。

默认配置

let label: ActiveLabel = ActiveLabel()

自定义配置

let label: ActiveLabel = ActiveLabel()
label.estimatedNumberOfLines = 3
label.finalLineTrailingInset = 100

使用便捷初始化程序的自定义配置。

var configuration = ActiveLabelConfiguration.default
configuration.estimatedNumberOfLines = 3
configuration.finalLineLength = 100
configuration.loadingView.animationDuration = 2.0
configuration.loadingView.animationDelay = 0
let label: ActiveLabel = ActiveLabel(frame: CGRect(x: 0, y: 0, width: 335, height: 21), configuration: configuration)

添加一些颜色,更改行高和间距。

let label: ActiveLabel = ActiveLabel()
label.estimatedNumberOfLines = 3
label.finalLineTrailingInset = 100
label.loadingView.color = UIColor(red: 233.0/255.0, green: 231.0/255.0, blue: 237.0/255.0, alpha: 1.0))
label.loadingView.lineHeight = 16
label.loadingView.lineVerticalSpacing = 8

在 Storyboard 或 Xib 中初始化 ActiveLabel 时,必须在代码中将标签的文本设置为 nil,因为 IB 使用空字符串值初始化标签。

ActiveLabel 用于快照测试时,可以通过在标签上调用 configureForSnapshotTest() 来使渐变居中。

ScrollingPageControl

ScrollingPageControl 是一个基于 Apple 的 UIPageControl 建模(但不是其子类)的视图。 此类的目的是允许在有限的空间内表示大量页面,并提供比 UIPageControl 更多的自定义。

默认配置,与 UIPageControl 相似

let pageControl: ScrollingPageControl = ScrollingPageControl()
pageControl.numberOfPages = 30                          // default is 0
pageControl.currentPage = 14                            // default is 0
pageControl.hidesForSinglePage = false                  // default
pageControl.pageIndicatorTintColor = .systemGray        // default
pageControl.currentPageIndicatorTintColor = .systemBlue // default

自定义点布局

pageControl.mainDotCount = 5                           // default is 3
pageControl.marginDotCount = 3                         // default is 2
pageControl.dotSize = CGSize(width: 5.0, height: 10.0) // default is 7.0 x 7.0
pageControl.dotSpacing = 14.0                          // default is 9.0
pageControl.minimumDotScale = 0.25                     // default is 0.4

响应 ScrollingPageControl 交互

pageControl.didSetCurrentPage = { [weak self] (index) in
    self?.scrollToPageAtIndex(index)
}

添加自定义页面点

pageControl.customPageDotAtIndex = { [weak self] (index) in
    guard self?.pageData[index].isFavorited else { return nil }
    return FavoriteIconView()
}

使用说明

混淆

通过使用 ObfuscatedKey,您可以构建一个人类可读的密钥,该密钥不会简单地通过对已编译的代码运行“strings”来显示,甚至不会作为字符串出现在您的源代码中。 只需创建一个 ObfuscatedKey 并使用构建器变量来编码您的密钥。

let key = ObfuscatedKey().T.h.i.s.underscore.I.s.underscore.O.b.f.u.s.c.a.t.e.d.value // This_Is_Obfuscated
let key = ObfuscatedKey().e.x.a.m.p.l.e.dash.n1.n2.n3.n4.n5.value // example-12345

示例

要运行示例项目,请克隆 repo,打开 UtiliKit.xcworkspace,然后运行“UtiliKit-iOSExample”项目。

要求

安装 - Swift Package Manager

dependencies: [
    .package(url: "https://github.com/BottleRocketStudios/iOS-UtiliKit.git", from: "1.6.0")
]

然后,您需要从可用的库中进行选择,以添加到您的项目中。 这些库应与通过 Cocoapods 提供的子规范相匹配。

安装 - CocoaPods

将以下内容添加到您的 Podfile

pod 'UtiliKit'

您还需要确保选择使用框架

use_frameworks!

然后使用 CocoaPods 0.36 或更高版本运行 pod install

安装 - Carthage

将以下内容添加到您的 Cartfile

github "BottleRocketStudios/iOS-UtiliKit"

运行 carthage update 并按照 Carthage 的 README 中描述的步骤进行操作。

贡献

请参阅 CONTRIBUTING 文档。 感谢各位贡献者