-
Notifications
You must be signed in to change notification settings - Fork 76
EpoxyCore
EpoxyCore
contains shared logic that all of the other Epoxy
module rely on. While we feel that EpoxyCore
is best used with the rest of Epoxy
, you can certainly use it by itself.
The views section of EpoxyCore
contains protocols that define a standardized way to create and configure UIView
s in your app.
Together, these protocols form the EpoxyableView
protocol composition, which you can conform custom custom views to make them simple to integrate with any Epoxy APIs:
public typealias EpoxyableView = StyledView & ContentConfigurableView & BehaviorsConfigurableView
When your UIView
conforms to EpoxyableView
, you can use a convenience API for creating Epoxy models for that view, for example here's how you would create an ItemModel
for a ButtonRow
view that conforms to EpoxyableView
:
ButtonRow.barModel(
content: .init(text: "Click me!"),
behaviors: .init(didTap: {
// Handle button selection
}),
style: .system)
Let's go through each of the constituent protocols of EpoxyableView
and see what they each define, and why they're useful:
The StyledView
protocol defines a standard way of initializing a UIView
with an associated Style
type:
public protocol StyledView: UIView {
associatedtype Style: Hashable = Never
init(style: Style)
}
The intention is for your Style
type to contain everything you need to configure a given view when it is initialized. As an example, we could write a Style
for UILabel
that configures the label with a font
and textColor
:
extension UILabel: StyledView {
struct Style: Hashable {
let font: UIFont
let textColor: UIColor
}
convenience init(style: Style) {
super.init(frame: .zero)
font = style.font
textColor = style.textColor
}
}
let label = UILabel(style: .init(font: UIFont.preferredFont(forTextStyle: .body), textColor: .black))
Having a consistent way to initialize all of the views in your application makes using your components easier, and requires less documentation while still maintaining clarity.
The style is Hashable
to enable differentiating between styles, and is used mainly by EpoxyCollectionView
, allowing views of the same type with equal Style
s to be reused.
The style defaults to Never
to allow for views that don't have initializer parameters to conform to EpoxyableView
. For example, a simple divider view might have no initializer parameters.
The ContentConfigurableView
protocol defines a standardized way to set the content of a view. Instead of having a separate property for each part of the content your view displays, we wrap it up into one type so we can set it all at once:
public protocol ContentConfigurableView: UIView {
associatedtype Content: Equatable = Never
func setContent(_ content: Self.Content, animated: Bool)
}
As an example, imagine we had a component called CheckboxRow
that displays a title, subtitle, and a checkbox that is either checked or unchecked. We could have 3 properties on this component: title: String
, subtitle: String
, and isChecked: Bool
. While this is certainly an acceptable way to write components, we find it easier to have a consistent way to set the content on any component which makes it much easier to know how to use and interact with a component. Here's what that content would look like for CheckboxRow
:
class CheckboxRow: UIView, ContentConfigurableView {
struct Content: Equatable {
var title: String?
var subtitle: String?
var isChecked: Bool
}
let titleLabel = UILabel()
let subtitleLabel = UILabel()
let checkboxView = UIImageView()
func setContent(_ content: Content, animated: Bool) {
titleLabel.text = content.title
subtitleLabel.text = content.subtitle
checkboxView.image = content.isChecked ? style.checkedImage : style.uncheckedImage
}
}
The Content
type being Equatable
is used by EpoxyCollectionView
to perform performant diffing when setting sections on a CollectionView
.
The content defaults to Never
to allow for views that don't have content to conform to EpoxyableView
. For example, a simple divider view might have no content.
The BehaviorsConfigurableView
protocol defines a standardized way to set the "behaviors" associated with a view. Behaviors are defined as the non-Equatable
properties that don't belong in the content, e.g. callback closures or delegates. Similar to ContentConfigurableView
, we wrap up all of the behaviors into a single type so that they can all be set at once:
public protocol BehaviorsConfigurableView: UIView {
associatedtype Behaviors = Never
func setBehaviors(_ behaviors: Self.Behaviors?)
}
As an example, we could define a ButtonRow
that has a tap handler closure as part of its Behaviors
:
final class ButtonRow: UIView, BehaviorsConfigurableView {
struct Behaviors {
var didTap: (() -> Void)?
}
func setBehaviors(_ behaviors: Behaviors?) {
didTap = behaviors?.didTap
}
private let button = UIButton(type: .system)
private var didTap: (() -> Void)?
private func setUp() {
button.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
@objc
private func handleTap() {
didTap?()
}
}
The setBehaviors
function takes an optional Behaviors
parameter as behaviors are optional when creating Epoxy models. If you do not supply behaviors when creating an EpoxyableView
, they will be reset on the view before it is reused by calling setBehaviors
with a nil
behaviors parameter.
Unlike content, behaviors are not required to be Equatable
. As a result, behaviors will be set more often than content, needing to be updated every time the view's corresponding Epoxy model is updated. As such, setting behaviors should be as lightweight as possible.
The behaviors default to Never
to allow for views that don't have behaviors to conform to EpoxyableView
. For example, a simple divider view might have no behaviors.
The diffing section of EpoxyCore
implements a version of Paul Heckel's difference algorithm for fast and efficient diffing between two collections. The two protocols of note here are Diffable
and DiffableSection
:
/// A protocol that allows us to check identity and equality between items for the purposes of
/// diffing.
public protocol Diffable {
/// Checks for equality between items when diffing.
///
/// - Parameters:
/// - otherDiffableItem: The other item to check equality against while diffing.
func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool
/// The identifier to use when checking identity while diffing.
var diffIdentifier: AnyHashable { get }
}
/// A protocol that allows us to check identity and equality between sections of `Diffable` items
/// for the purposes of diffing.
public protocol DiffableSection: Diffable {
/// The diffable items in this section.
associatedtype DiffableItems: Collection where
DiffableItems.Index == Int,
DiffableItems.Element: Diffable
/// The diffable items in this section.
var diffableItems: DiffableItems { get }
}
Conforming to these protocols allows you to easily create changesets between two sets of Diffables
or two sets of DiffableSections
:
extension String: Diffable {
public func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool {
guard let otherString = otherDiffableItem as? String else { return false }
return self == otherString
}
public var diffIdentifier: AnyHashable { self }
}
let set1 = ["a", "b", "c", "d"]
let set2 = ["c", "d", "e", "a"]
let changeset = set1.makeChangeset(from: set2)
The result of the makeChangeset(from:)
call will be an IndexChangeset
populated with the minimal set of inserts, deletes, updates, and moves that are needed to go from set1
to set2
DiffableSection
is very similar where each DiffableSection
contains a set of Diffable
items internally. As an example, we could use the String
extension above and introduce a StringSection
to get changesets between sections of Strings
struct StringSection: DiffableSection, Equatable {
var diffIdentifier: AnyHashable
var diffableItems: [String]
func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool {
guard let otherSection = otherDiffableItem as? StringSection else { return false }
return self == otherSection
}
}
let section1 = StringSection(
diffIdentifier: 1,
diffableItems: ["a", "b", "c", "d"])
let section2 = StringSection(
diffIdentifier: 2,
diffableItems: ["c", "d", "e", "a"])
let section3 = StringSection(
diffIdentifier: 1,
diffableItems: ["d", "e", "f"])
let changeset = [section1, section2].makeSectionedChangeset(from: [section3])
The resulting changeset
above will be populated with the necessary changes to go from the set of sections in the first array to the set of sections in the second array. It will include information about the sections that have been moved, inserted, or deleted as well as the items that have been moved, inserted, or deleted using IndexPaths
to give information on the item's location in the section.
EpoxyLogger
provides a way to intercept assertions, assertionFailures, and warnings that occur within Epoxy. In order to use it, you just need to set the global EpoxyLogger.shared
to your own instance of EpoxyLogger
. As an example, you could do this in your AppDelegate
to intercept assertions and log them to a server:
import Epoxy
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
-> Bool
{
...
EpoxyLogger.shared = EpoxyLogger(
assert: { condition, message, fileID, line in
// custom handling of assertions here
},
assertionFailure: { message, fileID, line in
// custom handling of assertion failures here
},
warn: { message, fileID, line in
// custom handling of warnings here
})
return true
}
}
WIP
- Overview
ItemModel
andItemModeling
- Using
EpoxyableView
CollectionViewController
CollectionView
- Handling selection
- Setting view delegates and closures
- Highlight and selection states
- Responding to view appear / disappear events
- Using
UICollectionViewFlowLayout
- Overview
GroupItem
andGroupItemModeling
- Composing groups
- Spacing
StaticGroupItem
GroupItem
withoutEpoxyableView
- Creating components inline
- Alignment
- Accessibility layouts
- Constrainable and ConstrainableContainer
- Accessing properties of underlying Constrainables