-
Notifications
You must be signed in to change notification settings - Fork 32
Improve using elements in lists #412
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
base: main
Are you sure you want to change the base?
Changes from 3 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,89 @@ | ||
| // | ||
| // 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 | ||
| ) | ||
| } | ||
|
|
||
| /// Used by internal Listable methods to convert type-erased `Element` instances into `Item` instances. | ||
| func toHeaderFooterConvertible() -> AnyHeaderFooterConvertible { | ||
| /// We use `type(of:)` to ensure we get the actual type, not just `Element`. | ||
| WrappedHeaderFooterContent( | ||
| represented: self | ||
| ) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| public struct WrappedHeaderFooterContent<ElementType:Element> : BlueprintHeaderFooterContent | ||
| { | ||
| public let represented : ElementType | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| false | ||
| } | ||
|
|
||
| public var elementRepresentation: Element { | ||
| represented | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension WrappedHeaderFooterContent where ElementType : Equatable { | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| represented == other.represented | ||
| } | ||
|
|
||
| public var reappliesToVisibleView: ReappliesToVisibleView { | ||
| .ifNotEquivalent | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension WrappedHeaderFooterContent where ElementType : IsEquivalentContent { | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| represented.isEquivalent(to: other.represented) | ||
| } | ||
|
|
||
| public var reappliesToVisibleView: ReappliesToVisibleView { | ||
| .ifNotEquivalent | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // | ||
| // Element+Item.swift | ||
|
Contributor
Author
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`. You many also optionally | ||
| /// configure the item, setting its values such as the `onDisplay` callbacks, etc. | ||
| /// | ||
| /// ```swift | ||
| /// MyElement(...) | ||
| /// .item { 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.
Outdated
Show resolved
Hide resolved
|
||
| configure : (inout Item<WrappedElementContent<Self, ObjectIdentifier>>) -> () = { _ in } | ||
| ) -> Item<WrappedElementContent<Self, ObjectIdentifier>> { | ||
| Item( | ||
| WrappedElementContent( | ||
| represented: self, | ||
| identifierValue: ObjectIdentifier(Self.Type.self) | ||
| ), | ||
| configure: configure | ||
| ) | ||
| } | ||
|
|
||
| /// 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. | ||
| /// | ||
| /// ```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<ID:Hashable>( | ||
| id : ID, | ||
| configure : (inout Item<WrappedElementContent<Self, ID>>) -> () = { _ in } | ||
| ) -> Item<WrappedElementContent<Self, ID>> { | ||
| Item( | ||
| WrappedElementContent( | ||
| represented: self, | ||
| identifierValue: id | ||
| ), | ||
| configure: configure | ||
| ) | ||
| } | ||
|
|
||
| /// Used by internal Listable methods to convert type-erased `Element` instances into `Item` instances. | ||
| func toAnyItemConvertible() -> AnyItemConvertible { | ||
kyleve marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// We use `type(of:)` to ensure we get the actual type, not just `Element`. | ||
| WrappedElementContent( | ||
| represented: self, | ||
| identifierValue: ObjectIdentifier(type(of: self)) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| public struct WrappedElementContent<ElementType:Element, IdentifierValue:Hashable> : BlueprintItemContent | ||
| { | ||
| public let represented : ElementType | ||
|
|
||
| public let identifierValue: IdentifierValue | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| false | ||
| } | ||
|
|
||
| public func element(with info: ApplyItemContentInfo) -> Element { | ||
| represented | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension WrappedElementContent where ElementType : Equatable { | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| represented == other.represented | ||
| } | ||
|
|
||
| public var reappliesToVisibleView: ReappliesToVisibleView { | ||
kyleve marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .ifNotEquivalent | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension WrappedElementContent where ElementType : IsEquivalentContent { | ||
|
|
||
| public func isEquivalent(to other: Self) -> Bool { | ||
| represented.isEquivalent(to: other.represented) | ||
| } | ||
|
|
||
| public var reappliesToVisibleView: ReappliesToVisibleView { | ||
| .ifNotEquivalent | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // | ||
| // 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 | ||
| /// } | ||
| /// ``` | ||
|
Collaborator
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. Thanks for the summary. |
||
| /// | ||
| /// ## Note | ||
| /// Takes advantage of `@_disfavoredOverload` to avoid ambiguous method resolution with the default implementations. | ||
kyleve marked this conversation as resolved.
Outdated
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 { | ||
|
|
||
| /// Required by every result builder to build combined results from statement blocks. | ||
| @_disfavoredOverload static func buildBlock(_ components: [Element]...) -> Component { | ||
| components.reduce(into: []) { $0 += $1.map { $0.toAnyItemConvertible() } } | ||
| } | ||
|
|
||
| /// If declared, provides contextual type information for statement expressions to translate them into partial results. | ||
| @_disfavoredOverload static func buildExpression(_ expression: Element) -> Component { | ||
| [expression.toAnyItemConvertible()] | ||
| } | ||
|
|
||
| /// If declared, provides contextual type information for statement expressions to translate them into partial results. | ||
| @_disfavoredOverload static func buildExpression(_ expression: [Element]) -> Component { | ||
| expression.map { $0.toAnyItemConvertible() } | ||
| } | ||
|
|
||
| /// Enables support for `if` statements that do not have an `else`. | ||
| @_disfavoredOverload static func buildOptional(_ component: [Element]?) -> Component { | ||
| component?.map { $0.toAnyItemConvertible() } ?? [] | ||
| } | ||
|
|
||
| /// With buildEither(second:), enables support for 'if-else' and 'switch' statements by folding conditional results into a single result. | ||
| @_disfavoredOverload static func buildEither(first component: [Element]) -> Component { | ||
| component.map { $0.toAnyItemConvertible() } | ||
| } | ||
|
|
||
| /// With buildEither(first:), enables support for 'if-else' and 'switch' statements by folding conditional results into a single result. | ||
| @_disfavoredOverload static func buildEither(second component: [Element]) -> Component { | ||
| component.map { $0.toAnyItemConvertible() } | ||
| } | ||
|
|
||
| /// Enables support for 'for..in' loops by combining the results of all iterations into a single result. | ||
| @_disfavoredOverload static func buildArray(_ components: [[Element]]) -> Component { | ||
| components.flatMap { $0.map { $0.toAnyItemConvertible() } } | ||
| } | ||
|
|
||
| /// If declared, this will be called on the partial result of an `if #available` block to allow the result builder to erase type information. | ||
| @_disfavoredOverload static func buildLimitedAvailability(_ component: [Element]) -> Component { | ||
| component.map { $0.toAnyItemConvertible() } | ||
| } | ||
|
|
||
| /// If declared, this will be called on the partial result from the outermost block statement to produce the final returned result. | ||
| @_disfavoredOverload static func buildFinalResult(_ component: [Element]) -> FinalResult { | ||
| component.map { $0.toAnyItemConvertible() } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| public extension ListableOptionalBuilder where ContentType == AnyHeaderFooterConvertible { | ||
|
|
||
| static func buildBlock(_ content: Element) -> ContentType { | ||
| return content.toHeaderFooterConvertible() | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.