OwlKit 1.1.3

OwlKit 1.1.3

Daniele Margutti维护。



OwlKit 1.1.3

SwiftDate

Owl为在iOS中构建快速且灵活的列表提供了一种数据驱动的声明式方法。

它支持UICollectionViewUITableViewUIStackView也在开发中!

特性亮点
🕺 不再需要委托和dataSource。只需完全类型安全的声明式内容方法
🧩 更好的架构,可以重用组件并从UI中十倍数据
🌈 自动动画内容更改,无需reloadData/performBatchUpdates
🚀 基于DifferenceKit的闪电般快速的差异算法
🧬 它在其核心使用标准的UIKit组件。没有魔法!
💎 (即将推出)支持可滚动声明式/全可定制的StackView
🐦 完全由Swift开发,由Swift爱好者编写的

Owl由Daniele Margutti创建并维护 - 我的个人网站www.danielemargutti.com

Version License Platform CI Status Carthage compatible Twitter Follow

要求

  • Xcode 9.0+
  • iOS 10.0+
  • Swift 5+

安装

首选的安装方法是使用CocoaPods。将以下内容添加到您的Podfile

pod 'OwlKit'

您还可以使用Carthage安装Owl。将此内容添加到您的Cartfile

github "malcommac/Owl"

Owl也兼容SPM (Swift Package Manager) 用于Swift 5.x+

您将获得

这是如何使用Owl实现功能齐全的Contacts列表的示例。这是一个简单的示例,但您可以轻松创建具有异构模型的复杂布局!

director = TableDirector(table: table)

// An adapter encapsulate the logic to render a model (Contact) with a specific ui (ContactCell).
let contactAdapter = TableCellAdapter<Contact,ContactCell> { dr in

	// Each adapter exposes all the standard methods of a list, events
	// and properties. ctx (context) received from event is a type-safe
	// object both for model and cell!
	dr.events.dequeue = { ctx in
		// element is type-safe, it's the Contact instance!
		// cell is type-safe, it's the ContactCell instance
		ctx.cell?.item = ctx.element
	}
	dr.events.didSelect = { ctx in
		let vc = ContactDetailVC(people: ctx.element)
		navigationController.pushViewController(vc, animated: true)
	}
	dr.events.shouldHighlight = { ctx in
		return ctx.element.isBlacklisted == false
	}
}
// Since now our table can show Contact istances using ContactCell
// All events are received only in its adapter.
director?.registerCellAdapter(contactAdapter) 

/// Manage your content in a declarative way!
let friendsSection = TableSection(elements: [john,mark,anita])
director?.add(section: friendsSection)
director?.reload()

许可证

  • Owl使用Apache 2.0许可证发布。
  • DifferenceKit使用Apache 2.0许可证发布。
  • Owl图标由Freepik创作,来源www.flaticon.com,遵循CC 3.0 BY许可

完整许可证文件.

文档

1.0 - 介绍:导演 & 适配器

Owl的所有SDK都基于两个概念:导演(director)和适配器(adapter)。

导演

导演是管理列表内容的类,保持数据与UI同步,并提供所有方法和属性来管理它。当您需要添加、移动或删除一个节或单元格,更改头部或尾部时,您将在该类中找到所有方法和属性。一个导演实例只能与一个列表(表或集合)相关联;一旦导演分配给列表,它就成为该对象的数据源和委托。

以下导演可用

  • TableDirector用于管理UITableView实例
  • CollectionDirectorFlowCollectionDirector 用于管理带有自定义布局或 UICollectionViewFlowLayout 布局的 UICollectionView

适配器

为列表创建新的导演后,您需要声明列表可以接受哪些类型的模型。每个模型都分配给一个UI元素(表格使用UITableViewCell子类,集合使用UICollectionViewCell子类)。

适配器的范围是声明导演可以管理的模型和UI的一对。

整个框架都基于这个概念:一个模型可以由一个单个UI元素渲染,而选择哪个UI来为特定的模型实例则由Owl来决定。

适配器也是接收涉及特定模型实例的事件的中心点。例如:当用户触摸类型A的模型实例的行时,您将接收事件(以及相关信息:索引路径,涉及到的模型实例等),并在管理该模型的适配器中接收这些信息。

您将注册尽可能多的适配器,与您拥有的模型数量一样多。

2.0 - 入门

以下代码展示如何创建一个导演来管理 UITableView(对于 UICollectionView 使用了类似的方法)。

public class MyController: UIViewController {
	@IBOutlet public var table: UITableView!
	
	private var director: TableDirector?
	
	func viewDidLoad() {
		super.viewDidLoad()
		director = TableDirector(table: table)
// ...

现在是时候声明要由我们的导演管理的类型的内容了。为了简单起见,我们将只声明一个适配器,但您可以声明您需要的适配器数量(您也可以创建包含适配器的自己的导演并按需重用它。这是一个很好的重用和分离数据的方法)。

func viewDidLoad() {
	// ...
	let contactAdpt = TableCellAdapter<Contact, ContactCell>()
	
	// You can attach events for adapter configuration.
	// The following example show how to set the row height and dequeue
	
	contactAdpt.events.rowHeight = { ctx in
		return 60.0 // explicit row height
	}
	
	contactAdpt.events.dequeue = { ctx in
		// this is the suggested behaviour; your cell should expose a
		// property of the type of the model it can be render and you will
		// assign it on dequeue. It's type safe too!!
		ctx.cell?.contact = ctx.element
	}
	director?.registerCellAdapter(contactAdpt)

这是一个使用我们的导演使用 ContactCell 细胞渲染类型为 Contact 的对象的最小设置。您可以通过适配器的 .events 属性配置和附加大量其他属性和事件;以下是每个属性的更详细描述,但所有 UITableView/UICollectionView 的事件和属性都受支持。

现在是在我们的表格中添加一些内容的时候了。正如我们所说的,Owl 使用命令式方法来处理内容:这意味着您可以通过使用命令式函数(如 addremovemove)来设置列表的内容,这两个函数适用于分节和元素。

// You can add/remove/move elements at anytime in a declarative way
let contacts = [
	Contact(first: "John", last: "Doe"),
	Contact(first: "Adam", last: "Best"),
	...
]
// ...
director?.add(elements: contacts)
director?.remove(section: 1)

有大量方法来管理分节和分节内的元素。

director?.firstSection?.removeAt(2) // remove an element
director?.remove(section: 2) // remove section
director?.section("mySection").move(swappingAt: 0, with:1) // swap elements
director?.add(elements: [...], at: 5) // add new elements
// and more...

一旦您更改了数据模型,您可以调用 reload() 函数来将其与UI同步。

以下代码创建(自动)一个新的包含 contacts 元素的 TableSection。如果您需要,您可以手动创建一个新的分节。

// Create a new section explicitly. Each section has an unique id you can assign
// explicitly or leave Owl create an UUID for you. It's used for diff features.
let newSection = TableSection(id: "mySectionId", elements: contacts, header: "\(contacts) Contacts", footer: nil)
director?.add(section: newSection)

为了同步UI,只需调用 director?.reload(),就这样!只需几行代码就创建了一个完整功能的列表!

您可以在任何时候通过使用 TableSection/CollectionSectionTableDirector/CollectionDirector 实例中的方法来添加新分节,移动或替换项目,然后调用 reload() 来刷新内容。

刷新操作没有动画效果,但Owl also能自动根据极快的差异算法选择最佳动画来刷新数据内容,详细说明见下一章。

以下代码演示了如何为一个适配器添加滑动删除功能

catalogAdapter.events.canEditRow = { ctx in
  return true
}
        
catalogAdapter.events.deleteConfirmTitle = { ctx in
  return "Delete"
}
        
catalogAdapter.events.commitEdit = { [weak self] ctx, style in
  guard let indexPath = ctx.indexPath else { return }
  
  // By using the session to reload you will also receive events like didEndEditingCell
  self?.tableDirector?.reload(afterUpdate: { dir in
    dir.sections[indexPath.section].remove(at: indexPath.row)
    return .none
  }, completion: nil)
}

↑ 返回顶部

3.0 - 如何操作

3.1 - 创建模型

模型可以是有结构或类;唯一的要求是它需要符合ElementRepresentable协议。这个协议被差异算法使用,以便评估应用模型的变化差异并选择最佳的动画来展示。

这意味着你不需要执行任何performBatches更新,也不需要手动调用reloadRows/removeRows/addRows方法……Owl都自动为你处理这些繁琐且容易崩溃的操作。

以下代码演示了我们的Contact模型声明

public class Contact: ElementRepresentable {
    let firstName: String
    let lastName: String

	public var differenceIdentifier: String {
		return "\(self.firstName)_\(self.lastName)"
	}

	public func isContentEqual(to other: Differentiable) -> Bool {
		guard let other = other as? Contact else { return false }
		return other == self
	}

	init(first: String, last: String) {
		self.firstName = first
		self.lastName = last
	}
}

通过添加以下方式来实现协议的一致性:

  • differenceIdentifier属性:为了区分添加或删除的模型,模型需要唯一标识(这是添加uuid属性的理想位置)
  • isContentEqual(to:)函数:用于检查是否有任何属性发生变化,用于替换更改。如果模型在重新加载之间数据发生变化,Owl会相应地更新单元格的UI。

↑ 返回顶部

3.2 - 创建单元格

第二步是创建模型的UI表示。通常是UITableViewCellUICollectionViewCell的子类。

重用标识符

单元格的reuseIdentifier值应该是其类的本身名称(例如,ContactCellContactCell作为标识符;也可以根据需要配置,但这是一个好习惯)。

最佳实践

您不需要遵循任何特殊的协议,但为了保持代码的整洁,我们建议创建一个公共属性,它接受模型实例并将其设置在适配器的 dequeue 事件上。

public class ContactCell: UITableViewCell {

	// Your outlets
    @IBOutlet public var ...

	// Define a property you set on adapter's dequeue event
	public var contact: Contact? {
		didSet {
           // setup your UI according with instance data
		}
	}
}

在您的适配器中

contactAdpt.events.dequeue = { ctx in
	ctx.cell?.contact = ctx.element
}

ctx 是一个包括事件所需所有信息(包括模型类型安全的实例)的对象。

↑ 返回顶部

3.3 - 管理自适应大小的单元格

自适应单元格的配置简单,适用于表格和集合视图。

Owl 使用自动布局支持轻松设置单元格大小。您可以按适配器或集合/表格设置单元格大小。对于由自动布局驱动的单元格大小设置,将 rowHeight(对于 TableDirector)或 itemSize(对于 CollectionDirector/FlowCollectionDirector)设置为自动布局值,然后提供一个估计值。

接受值包括

  • default:您必须提供单元格的高度(表格)或大小(集合)
  • auto(estimated: CGFloat):使用自动布局评估单元格的高度;对于集合视图,您还可以通过在单元格实例中覆盖 preferredLayoutAttributesFitting() 函数来提供自己的计算。
  • explicit(CGFloat):为所有单元格类型提供固定高度(如果您计划所有单元格大小相同,则更快)

↑ 返回顶部

3.4 - 从 Storyboard/Xib/Class 加载 Cells/View

此行为用于加载/回收表格和集合视图的单元格和头部/尾部

实体 默认可复用标识符 默认加载方法
Cells(《UITableViewCell》,《UICollectionViewCell》) 类名 从父表格的 Storyboard 单元格原型列表中使用可复用标识符加载 UI
Header/Footer View(《UITableViewHeaderFooterView》,《UICollectionReusableView》) 类名 尝试将从同一类本身同包含包含有同一名称的 xib 的束中加载视图作为根元素(例如,“GroupHeaderView.xib”)

您可以通过在适配器(对于表格的单元格和头部/尾部为 TableAdapterTableHeaderFooterAdapter,对于集合的单元格和头部/尾部为 CollectionAdapter/CollectionHeaderFooterAdapter)之前设置以下属性来自定义此行为。

  • reusableViewIdentifier:设置一个新的可重用标识符以使用。
  • reusableViewLoadSource:更改用于从队列中获取视图的源(对于单元格,默认值为 .fromStoryboard,对于标题/页脚是 .fromXib(name:nil, bundle: nil))。允许的值有

默认情况下,单元格或标题/页脚将使用类的名称本身作为 reusableIdentifier(例如,你的单元格 ContactCell 必须将标识符设置为 ContactCell)。这是一个有用的约定,可以避免奇怪的标识符。

但是,您可以通过设置 cellReuseIdentifier(在 TableAdapterCollectionAdapter 中用于单元格)和 viewReuseIdentifier(在 TableHeaderFooterAdapter/CollectionHeaderFooterAdapter 中用于标题/页脚)来覆盖这种行为。

另一个有趣的属性是 reusableViewLoadSource;这指定 Owl 需要在哪里搜索您即将加载的单元格/视图的 UI。允许的值有

  • fromStoryboard:从表/集合原型列表中的故事板中加载(单元格的默认值)。
  • fromXib(name: String?, bundle: Bundle?):从一个特定束包中的 xib 文件中加载(如果 name 为 nil,则使用与单元格类相同的文件名,即 ContactCell.xib;如果 bundlenil,则使用与类相同的束包)。
  • fromClass:从类中加载。

以下示例从与类名称相同的外部 xib(例如,ContactCell.xib)加载单元格,并包含在单元格类的相同束包中。相同的做法也适用于标题/页脚。

let contactAdpt = TableCellAdapter<Contact, ContactCell>()
// instead of load cell from storyboard it will be loaded by
// reading the root view inside the xib with the same name of the class
contactAdpt.reusableViewLoadSource = .fromXib(name:nil, bundle:nil)
// optionally you can also set a custom id
contactAdpt.reusableViewIdentifier = "CustomContactCellID"
// configure...
director?.registerCellAdapter(contactAdpt)

↑ 返回顶部

3.5 - 自定义部分标题/页脚

部分(无论是 TableSection 还是 CollectionSection)可以配置为具有简单(String)或复杂(自定义视图)的标题/页脚。自定义标题/页脚的配置方式与您为单元格所做的相同,通过使用适配器概念。

要创建一个基于简单字符串的标题/页脚,只需将其值传递到部分对象的初始化中

let newTableSection = TableSection(id: "sectionId", elements: contacts, header: "\(contacts.count) Contacts", footer: nil)

以下代码创建了一个包含该部分内联系人数的字符串标题的部分。

要创建基于视图的标题/页脚,您必须使用以下内容注册一个适配器

  • TableHeaderFooterAdapter<View> 类用于表格(其中 ViewUITableViewHeaderFooterView 类的子类)
  • CollectionHeaderFooterAdapter<View> 用于集合(其中 ViewUICollectionReusableView类的子类)

在这种情况下,适配器仅保留标题/页脚视图类类型的引用(不涉及显式模型)。

以下示例为集合创建了一个基于视图的标题,并使用一些自定义数据配置了它

// In our case our custom view is a class EmojiHeaderView subclass of UICollectionReusableView 
let emojiHeader = CollectionHeaderFooterAdapter<EmojiHeaderView> { ctx in
	ctx.events.dequeue = { ctx in // register for view dequeue events to setup some data
		ctx.view?.titleLabel.text = ctx.section?.identifier ?? "-"
	}
}
// Register adapter for header/footer
director?.registerHeaderFooterAdapter(headerAdapter)

一旦您已注册了您的标题/页脚,您就可以将其分配给任何部分的初始化

let newSection = CollectionSection(id: "Section \(idx)" ,elements: elements, header: emojiHeader, footer: nil)

ctx 参数中,您将接收到生成事件的部分。

↑ 返回顶部

3.6 - 使用自动动画重新加载数据

Owl支持在更改数据源之间自动动画重新加载数据。使用此功能,您不需要考虑在更改数据源时调用(甚至 以正确的顺序reloadRows/deleteRows/removeRows 方法:只需在回调中更改模型并执行更改,在最后,Owl将生成正确的动画序列。

这是因为,在 UITableViewUICollectionViewperformBatchUpdates 中,有些操作组合在同时应用时会导致崩溃。DifferenceKit 采用了一种分割差异集的方法,将差异分割成可以无崩溃执行批处理的最低阶段的集合。

以下示例

director?.reload(afterUpdate: { dir in
   dir.firstSection()!.elements.shuffled() // shuffle some items
   dir.firstSection()!.remove(at: 4) // remove item at index 4
   // ... do any other stuff with sections or elements in section
}, completion: nil)

在调用Owl结束时,将比较前后数据模型并生成一个要执行的动画更改集,

差异算法基于优化的DifferenceKit框架,这是一个针对性能的O(n)差异算法,适用于Swift。它支持所有动画UI批更新的操作,包括分区重新加载。

与IGListKit相比,它允许

支持的集合

线性 分区的 重复的元素/分区
Owl
RxDataSources
IGListKit

线性意味着一维集合。
分区的意味着二维集合。

支持元素差异

删除 插入 移动 重新加载 跨分区移动
Owl
RxDataSources
IGListKit

支持的分区差异

删除 插入 移动 重新加载
Owl
RxDataSources
IGListKit

而且,它的性能远比IGListKitRxDataSources 相当对手要快!

↑ 返回顶部

3.7 - 使用头部和尾部

Owl中的头部和尾部使用已用于Cell的相同方式管理,使用Adapter;然而,由于它的性质(与单元格不同,它们不是严格关联到特定的模型类),您将创建一个仅链接用于渲染视图的适配器(对于 UITableViewUITableViewControllerHeaderFooterView 的子类,对于 UICollectionViewUICollectionReusableView 的子类)。

A. 创建适用于UITableView的头/尾部

使用 storyboard 在您的 UITableView 中使用头部/尾部并不容易;最可行的方法是创建一个与您的 UITableViewHeaderFooterView 子类同名的 xib 文件。

假设您想为您的联系人表创建一个名为 GroupHeaderView 的头部/尾部类。您需要创建

GroupHeaderView.xib 文件包含了类的 UI(您仍然可以通过覆盖 GroupHeaderView.swift 实现中的 ReusableCellViewProtocol 协议来自定义视图的加载方式。默认情况下,它期望一个包含 UI 的单独的 xib 文件,但您也可以直接从类中加载它或提供不同的文件名)。此 xib 文件包含一个 GroupHeaderView 视图子类。

GroupHeaderView.swift 文件包含您的类实现。在标准配置下,您不需要做任何特殊操作;只需将 UITableViewHeaderFooterView 子类化即可。

import UIKit

// Nothing special is required to support header/footer class in Owl.
// This is just a simple class.

public class GroupHeaderView: UITableViewHeaderFooterView {
	@IBOutlet public var headerTitleLabel: UILabel?
	@IBOutlet public var headerSubtitleLabel: UILabel?
	...
}

下一步是注册您的头部/尾部适配器

let groupHeaderAdapter = TableHeaderFooterAdapter<GroupHeaderView> { config in

  // Configure content of the header/footer
  config.events.dequeue = { ctx in
    ctx.view?.headerTitleLabel?.text = "..."
    ctx.view?.headerSubtitleLabel?.text = "Section #\(ctx.section)"
  }
   
  // Configure height of the header/footer
  config.events.height = { _ in
    return 30
  }
}

到这一步,您只需使用以下方式即可将此适配器分配给任何 TableSection 实例

let newSection = TableSection(elements: [...], headerView: groupHeaderAdapter, footerView: nil)
director?.add(section: section)
director?.reload()

以上就是全部!您可以在多个部分中使用 groupHeaderAdapter,而不会有任何问题。

B. 为 UICollectionView 创建 Header/Footer

为集合视图创建头部/尾部的方式与之前为表格使用的方法非常相似;唯一的重大区别是,当您使用 storyboards 时,可以使用指定在集合视图中的头部/尾部(就像单元格一样)。

首先,您需要创建头部/尾部类的实例。以下示例创建了一个自定义头部,即 EmojiHeaderView.swift

import UIKit

public class EmojiHeaderView: UICollectionReusableView {
    @IBOutlet public var titleLabel: UILabel!
    // ...
}

至于表格的头部/尾部视图,您不需要为您的子类做任何特殊处理。

接下来,您需要为您的集合视图启用头部/尾部,并将新创建的视图设置为您的视图类子类。

现在,您已准备好创建用于保存头部/尾部的适配器。

let headerAdapter = CollectionHeaderFooterAdapter<EmojiHeaderView> { cfg in
  // dequeue
  cfg.events.dequeue = { ctx in
    ctx.view?.titleLabel.text = ctx.section?.identifier ?? "-"
  }
}

director?.registerHeaderFooterAdapter(headerAdapter)

现在,您可以将头部/尾部适配器分配给一个或多个部分实例

let section = CollectionSection(id: "Section \(idx)", elements: rawSection)
section.headerView = headerAdapter
section.headerSize = CGSize(width: self.collection.frame.size.width, height: 30)