Skip to content

EpoxyCore

Tyler Hedrick edited this page Jan 30, 2021 · 10 revisions

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.

Diffing

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 aer 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 {
  func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool {
    guard let otherString = otherDiffableItem as? String else { return false }
    return self == otherString
  }

  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 {
  let diffIdentifier: String
  let 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.

Logging

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
  }

}

Views

The views section of EpoxyCore contains protocols that define a standardized way to create and configure views in an application. Let's go through each one, what it defines, and why it is useful:

StyledView

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.

ContentConfigurableView

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 {
    let title: String
    let subtitle: String
    let isChecked: Bool
  }

  let titleLabel = UILabel()
  let subtitleLabel = UILabel()
  let checkmarkView = UIImageView()

  func setContent(_ content: Content, animated: Bool) {
    titleLabel.text = content.title
    subtitleLabel.text = content.subtitle
    checkmarkView.image = content.isChecked ? checkedImage : uncheckedImage
  }
}

The Content type being Equatable is used by EpoxyCollectionView to perform performant diffing when setting sections on a CollectionView.