Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve using elements in lists #412

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
05cbbfe
Move isEquivalent to its own protocol.
kyleve Jul 25, 2022
ca36edc
Update APIs used to add elements to lists
kyleve Jul 25, 2022
0937a2e
Update single builder
kyleve Jul 25, 2022
fa730f9
Remove unneeded overloads
kyleve Jul 25, 2022
f90444d
Remove no longer needed type erased conversion methods
kyleve Jul 25, 2022
c8f0acc
Ensure we properly respect Equatable or IsEquivalentContent.
kyleve Jul 26, 2022
bcc1074
Also fix header/footer Equatable and IsEquivalent resolution.
kyleve Jul 26, 2022
732e652
Add ListElementNonConvertible
kyleve Jul 26, 2022
89c4d7d
Update section additions
kyleve Jul 26, 2022
1dcaa3d
Additional self review and fixes
kyleve Jul 26, 2022
966d397
Update CHANGELOG
kyleve Jul 26, 2022
1b7d84a
Rename: .item() to .listItem()
kyleve Jul 27, 2022
5b167f1
Implement automatic isEquivalent checking for Elements
kyleve Jul 28, 2022
057e1c9
Docs, add default IsEquivalentContent implementation.
kyleve Jul 29, 2022
b60f091
Rename to areEquatablePropertiesEqual
kyleve Jul 29, 2022
f39e3b4
Update comments and default implementations
kyleve Jul 29, 2022
2d87e95
Comment cleanup
kyleve Jul 29, 2022
d15b036
Remove Property Wrapper
kyleve Jul 29, 2022
090e38a
After chatting with Tim, update how we traverse properties.
kyleve Jul 29, 2022
9778469
Rename IsEquivalentContent, add some more tests.
kyleve Jul 29, 2022
0260289
Peformance: Remove some casting checks
kyleve Jul 29, 2022
d166aa6
Found a Mirror bug, add a test for it
kyleve Jul 30, 2022
6467a3e
Add Swift 5.7 support
kyleve Jul 30, 2022
bd6cc74
Improve error messages
kyleve Jul 30, 2022
01675e4
Address self review
kyleve Aug 8, 2022
2c9da83
fix
kyleve Aug 8, 2022
4294b9b
Add more specific error
kyleve Aug 8, 2022
8fbfa30
Update how we handle backgrounds for elements
kyleve Aug 8, 2022
9ac3840
Update tests to compare equatability performance
kyleve Aug 11, 2022
d65a3d4
Code review
kyleve Feb 2, 2023
964a924
Follow up: Remove automatic equality, replace with easier method to d…
kyleve May 25, 2023
938ef81
Additional renames
kyleve May 25, 2023
58e8a6f
Improve error messages
kyleve May 26, 2023
5c9de6d
Remove applies overrides
kyleve May 26, 2023
59bce3b
Update demos to use key path equivalency
kyleve May 26, 2023
40179f7
Self review and cleanup
kyleve Jun 11, 2023
e3c5ce4
KeyPathEquivalent updates
kyleve Jun 15, 2023
d00c534
Split test case into two
kyleve Jun 15, 2023
dadf9dd
Merge remote-tracking branch 'origin/main' into kve/element-usage-imp…
kyleve Oct 24, 2023
ef78668
Merge remote-tracking branch 'origin/main' into kve/element-usage-imp…
kyleve Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions BlueprintUILists/Sources/Element+HeaderFooter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// Element+HeaderFooter.swift
// BlueprintUILists
//
// Created by Kyle Van Essen on 7/24/22.
//

import BlueprintUI
import ListableUI


// MARK: HeaderFooter / HeaderFooterContent Extensions


extension Element {

/// Converts the given `Element` into a Listable `HeaderFooter`. You many also optionally
/// configure the header / footer, setting its values such as the `onTap` callbacks, etc.
///
/// ```swift
/// MyElement(...)
/// .headerFooter { header in
/// header.onTap = { ... }
/// }
/// ```
///
/// ## ⚠️ Performance Considerations
/// Unless your `Element` conforms to `Equatable` or `IsEquivalentContent`,
/// it will return `false` for `isEquivalent` for each content update, which can dramatically
/// hurt performance for longer lists (eg, more than 20 items): it will be re-measured for each content update.
///
/// It is encouraged for these longer lists, you ensure your `Element` conforms to one of these protocols.
public func headerFooter(
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in }
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> {
HeaderFooter(
WrappedHeaderFooterContent(represented: self),
configure: configure
)
}
}


extension Element where Self:Equatable {

public func headerFooter(
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in }
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> {
HeaderFooter(
WrappedHeaderFooterContent(represented: self),
configure: configure
)
}
}


extension Element where Self:IsEquivalentContent {

public func headerFooter(
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in }
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> {
HeaderFooter(
WrappedHeaderFooterContent(represented: self),
configure: configure
)
}
}


public struct WrappedHeaderFooterContent<ElementType:Element> : BlueprintHeaderFooterContent
{
public let represented : ElementType

private let isEquivalent : (Self, Self) -> Bool

init(represented : ElementType) {
self.represented = represented

self.isEquivalent = { _, _ in false }
}

init(represented : ElementType) where ElementType:Equatable {
self.represented = represented

self.isEquivalent = {
$0.represented == $1.represented
}
}

init(represented : ElementType) where ElementType:IsEquivalentContent {
self.represented = represented

self.isEquivalent = {
$0.represented.isEquivalent(to: $1.represented)
}
}

public func isEquivalent(to other: Self) -> Bool {
isEquivalent(self, other)
}

public var elementRepresentation: Element {
represented
}
}

134 changes: 134 additions & 0 deletions BlueprintUILists/Sources/Element+Item.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// Element+Item.swift
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylebshr Creating a thread to reply to stuff, so we can talk in a thread vs top level comments:

a) Is this an even bigger foot gun than what we have now? Many people probably won't make their element equatable or IsEquivalentItem

Hard to say – there's a bunch of existing ElementItem usages in POS now, and they're all pretty much wrong – the API was hard to use, so there's a lot of this:

ElementItem("formFields-element", id: \.self) { _, _ in
   // Return an element
}

ElementItem(transaction.token, id: \.self) { _, _ in ... }

ElementItem("error-state", id: \.self) { _, _ in ... }

ElementItem("business-organization-name-content", id: \.self) { _, _ in ... }

Etc...

But in this case the underlying value is "formFields-element", aka a static string; so isEquivalent always returns true, and the item will never re-size even when it should – which is already a foot gun and breaks things in weird ways.

b) Is there a performance concern from losing identifiers (most people won't add one)

Identifiers? No – the list smart enough to do a "best attempt" at creating stable identifiers when there's duplicate IDs (and identifiers are already salted with the ItemContent type), so even without explicitly provided IDs, you'll get stable IDs across updates. Eg, if this is your list, these will be the identifiers:

MyElement1() -> Identifier(MyElement1, 1)
MyElement2() -> Identifier(MyElement2, 1)
MyElement2() -> Identifier(MyElement2, 2)
MyElement1() -> Identifier(MyElement1, 2)

The main benefit to providing IDs is during mutative diffs, the list can more intelligently manage the changes.

c) Most elements have closures, which make equatability impossible. What's the correct thing to do there. Should there be more helpers for interacting with the row tap handlers, so that the element doesn't have to be re-applied for the closure to be correct?

Same thing as you'd do before with ItemContent / BlueprintItemContent; you need to manually implement Equatable or isEquivalent, and compare the equatable parameters that you can. There are some clever things we can do here to avoid needing this, but I didn't explore it: We can use mirror to find equatable parameters and compare them

// BlueprintUILists
//
// Created by Kyle Van Essen on 7/24/22.
//

import BlueprintUI
import ListableUI


// MARK: Item / ItemContent Extensions

extension Element {

/// Converts the given `Element` into a Listable `Item` with the provided ID. You can use this ID
/// to scroll to or later access the item through the regular list access APIs.
/// You many also optionally configure the item, setting its values such as the `onDisplay` callbacks, etc.
kyleve marked this conversation as resolved.
Show resolved Hide resolved
///
/// ```swift
/// MyElement(...)
/// .item(id: "my-provided-id") { item in
/// item.insertAndRemoveAnimations = .scaleUp
/// }
/// ```
///
/// ## ⚠️ Performance Considerations
/// Unless your `Element` conforms to `Equatable` or `IsEquivalentContent`,
/// it will return `false` for `isEquivalent` for each content update, which can dramatically
/// hurt performance for longer lists (eg, more than 20 items): it will be re-measured for each content update.
///
/// It is encouraged for these longer lists, you ensure your `Element` conforms to one of these protocols.
public func item(
kyleve marked this conversation as resolved.
Show resolved Hide resolved
id : AnyHashable = ObjectIdentifier(Self.Type.self),
kyleve marked this conversation as resolved.
Show resolved Hide resolved
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in }
) -> Item<WrappedElementContent<Self>> {
Item(
WrappedElementContent(
identifierValue: id,
represented: self
),
configure: configure
)
}
}

extension Element where Self:Equatable {

public func item(
id : AnyHashable = ObjectIdentifier(Self.Type.self),
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in }
) -> Item<WrappedElementContent<Self>> {
Item(
WrappedElementContent(
identifierValue: id,
represented: self
),
configure: configure
)
}
}


extension Element where Self:IsEquivalentContent {

public func item(
id : AnyHashable = ObjectIdentifier(Self.Type.self),
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in }
) -> Item<WrappedElementContent<Self>> {
Item(
WrappedElementContent(
identifierValue: id,
represented: self
),
configure: configure
)
}
}


public struct WrappedElementContent<ElementType:Element> : BlueprintItemContent
{
public let identifierValue: AnyHashable

public let represented : ElementType

private let isEquivalent : (Self, Self) -> Bool

init(
identifierValue: AnyHashable,
represented: ElementType
) {
self.represented = represented
self.identifierValue = identifierValue

self.isEquivalent = { _, _ in false }
}

init(
identifierValue: AnyHashable,
represented: ElementType
) where ElementType:Equatable {
self.represented = represented
self.identifierValue = identifierValue

self.isEquivalent = {
$0.represented == $1.represented
}
}

init(
identifierValue: AnyHashable,
represented: ElementType
) where ElementType:IsEquivalentContent {
self.represented = represented
self.identifierValue = identifierValue

self.isEquivalent = {
$0.represented.isEquivalent(to: $1.represented)
}
}

public func isEquivalent(to other: Self) -> Bool {
isEquivalent(self, other)
}

public func element(with info: ApplyItemContentInfo) -> Element {
represented
}

public var reappliesToVisibleView: ReappliesToVisibleView {
kyleve marked this conversation as resolved.
Show resolved Hide resolved
.ifNotEquivalent
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// HeaderFooter.swift
// ElementHeaderFooter.swift
// BlueprintUILists
//
// Created by Kyle Van Essen on 10/9/20.
Expand All @@ -9,6 +9,8 @@ import ListableUI
import BlueprintUI


///
/// ⚠️ This method is soft-deprecated! Consider using `myElement.headerFooter(...)` instead.
///
/// Provides a way to create a `HeaderFooter` for your Blueprint elements without
/// requiring the creation of a new `BlueprintHeaderFooterContent` struct.
Expand Down Expand Up @@ -62,6 +64,8 @@ public func ElementHeaderFooter<Represented>(
)
}

///
/// ⚠️ This method is soft-deprecated! Consider using `myElement.headerFooter(...)` instead.
///
/// Provides a way to create a `HeaderFooter` for your Blueprint elements without
/// requiring the creation of a new `BlueprintHeaderFooterContent` struct.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Item.swift
// ElementItem.swift
// BlueprintUILists
//
// Created by Kyle Van Essen on 9/10/20.
Expand All @@ -9,6 +9,8 @@ import ListableUI
import BlueprintUI


///
/// ⚠️ This method is soft-deprecated! Consider using `myElement.item(...)` instead.
///
/// Provides a way to create an `Item` for your Blueprint elements without
/// requiring the creation of a new `BlueprintItemContent` struct.
Expand Down Expand Up @@ -68,6 +70,8 @@ public func ElementItem<Represented, IdentifierValue:Hashable>(


///
/// ⚠️ This method is soft-deprecated! Consider using `myElement.item(...)` instead.
///
/// Provides a way to create an `Item` for your Blueprint elements without
/// requiring the creation of a new `BlueprintItemContent` struct.
///
Expand Down
21 changes: 17 additions & 4 deletions BlueprintUILists/Sources/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public struct List : Element
//
// MARK: Initialization
//

/// Create a new list, configured with the provided properties,
/// configured with the provided `ListProperties` builder.
public init(
Expand All @@ -76,13 +76,26 @@ public struct List : Element
public init(
measurement : List.Measurement = .fillParent,
configure : ListProperties.Configure = { _ in },
@ListableBuilder<Section> sections : () -> [Section]
@ListableBuilder<Section> sections : () -> [Section],
@ListableOptionalBuilder<AnyHeaderFooterConvertible> containerHeader : () -> AnyHeaderFooterConvertible? = { nil },
@ListableOptionalBuilder<AnyHeaderFooterConvertible> header : () -> AnyHeaderFooterConvertible? = { nil },
@ListableOptionalBuilder<AnyHeaderFooterConvertible> footer : () -> AnyHeaderFooterConvertible? = { nil },
@ListableOptionalBuilder<AnyHeaderFooterConvertible> overscrollFooter : () -> AnyHeaderFooterConvertible? = { nil }
) {
self.measurement = measurement

self.properties = .default(with: configure)
var properties = ListProperties.default {
$0.sections = sections()

$0.content.containerHeader = containerHeader()
$0.content.header = header()
$0.content.footer = footer()
$0.content.overscrollFooter = overscrollFooter()
}

configure(&properties)

self.properties.sections += sections()
self.properties = properties
}

//
Expand Down
56 changes: 56 additions & 0 deletions BlueprintUILists/Sources/ListableBuilder+Element.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// ListableBuilder+Element.swift
// BlueprintUILists
//
// Created by Kyle Van Essen on 7/24/22.
//

import BlueprintUI
import ListableUI


/// Adds `Element` support when building `AnyItemConvertible` arrays, which allows:
///
/// ```swift
/// Section("3") { section in
/// TestContent1() // An ItemContent
///
/// Element1() // An Element
/// Element2() // An Element
/// }
/// ```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the summary.

///
/// ## Note
/// Takes advantage of `@_disfavoredOverload` to avoid ambiguous method resolution with the default implementations.
kyleve marked this conversation as resolved.
Show resolved Hide resolved
/// See more here: https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_disfavoredoverload
///
public extension ListableBuilder where ContentType == AnyItemConvertible {

static func buildExpression<ElementType:Element>(_ expression: ElementType) -> Component {
[expression.item()]
}

static func buildExpression<ElementType:Element>(_ expression: ElementType) -> Component where ElementType:Equatable {
[expression.item()]
}

static func buildExpression<ElementType:Element>(_ expression: ElementType) -> Component where ElementType:IsEquivalentContent {
[expression.item()]
}
}


public extension ListableOptionalBuilder where ContentType == AnyHeaderFooterConvertible {

static func buildBlock<ElementType:Element>(_ content: ElementType) -> ContentType {
return content.headerFooter()
}

static func buildBlock<ElementType:Element>(_ content: ElementType) -> ContentType where ElementType:Equatable {
return content.headerFooter()
}

static func buildBlock<ElementType:Element>(_ content: ElementType) -> ContentType where ElementType:IsEquivalentContent {
return content.headerFooter()
}
}
Loading