-
Notifications
You must be signed in to change notification settings - Fork 28
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
base: main
Are you sure you want to change the base?
Changes from 11 commits
05cbbfe
ca36edc
0937a2e
fa730f9
f90444d
c8f0acc
bcc1074
732e652
89c4d7d
1dcaa3d
966d397
1b7d84a
5b167f1
057e1c9
b60f091
f39e3b4
2d87e95
d15b036
090e38a
9778469
0260289
d166aa6
6467a3e
bd6cc74
01675e4
2c9da83
4294b9b
8fbfa30
9ac3840
d65a3d4
964a924
938ef81
58e8a6f
5c9de6d
59bce3b
40179f7
e3c5ce4
d00c534
dadf9dd
ef78668
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// | ||
// 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 | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `Equatable` initializer for `WrappedHeaderFooterContent` is called. | ||
extension Element where Self:Equatable { | ||
|
||
public func headerFooter( | ||
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in } | ||
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> { | ||
HeaderFooter( | ||
WrappedHeaderFooterContent(represented: self), | ||
configure: configure | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `IsEquivalentContent` initializer for `WrappedHeaderFooterContent` is called. | ||
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 | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// | ||
// Element+Item.swift | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Hard to say – there's a bunch of existing
But in this case the underlying value is
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
The main benefit to providing IDs is during mutative diffs, the list can more intelligently manage the changes.
Same thing as you'd do before with |
||
// 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 | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `Equatable` initializer for `WrappedElementContent` is called. | ||
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 | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `IsEquivalentContent` initializer for `WrappedElementContent` is called. | ||
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with this, but it also might be a good place to pump the breaks on overloading and provide an argument label or some such to disambiguate.
This would also solve the potential issue of branching implementations which could in pathological cases cause unpredictable behavior for types which adopt multiple (non-inherited) protocols.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed for the result builders in particular – so it's not that we can provide an overload that's useful – it's that we need to signal to the compiler that this is the method set, etc to use for these passing types