-
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 36 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,126 @@ | ||
// | ||
// Element+HeaderFooter.swift | ||
// BlueprintUILists | ||
// | ||
// Created by Kyle Van Essen on 7/24/22. | ||
// | ||
|
||
import BlueprintUI | ||
import ListableUI | ||
|
||
|
||
// MARK: HeaderFooter / HeaderFooterContent Extensions | ||
|
||
|
||
extension Element { | ||
|
||
/// Ensures that a well-formed error is presented when a non-Equatable or non-LayoutEquivalent element is provided. | ||
@available(*, unavailable, message: "To be directly added to a List, an Element must conform to Equatable or LayoutEquivalent.") | ||
public func listHeaderFooter( | ||
background : @escaping () -> Element? = { nil }, | ||
pressedBackground : @escaping () -> Element? = { nil }, | ||
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in } | ||
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> { | ||
fatalError() | ||
} | ||
} | ||
|
||
/// Ensures that the `Equatable` initializer for `WrappedHeaderFooterContent` is called. | ||
extension Element where Self:Equatable { | ||
|
||
public func listHeaderFooter( | ||
background : @escaping () -> Element? = { nil }, | ||
pressedBackground : @escaping () -> Element? = { nil }, | ||
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in } | ||
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> { | ||
HeaderFooter( | ||
WrappedHeaderFooterContent( | ||
represented: self, | ||
background: background, | ||
pressedBackground: pressedBackground | ||
), | ||
configure: configure | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `LayoutEquivalent` initializer for `WrappedHeaderFooterContent` is called. | ||
extension Element where Self:LayoutEquivalent { | ||
|
||
@_disfavoredOverload | ||
public func listHeaderFooter( | ||
background : @escaping () -> Element? = { nil }, | ||
pressedBackground : @escaping () -> Element? = { nil }, | ||
configure : (inout HeaderFooter<WrappedHeaderFooterContent<Self>>) -> () = { _ in } | ||
) -> HeaderFooter<WrappedHeaderFooterContent<Self>> { | ||
HeaderFooter( | ||
WrappedHeaderFooterContent( | ||
represented: self, | ||
background: background, | ||
pressedBackground: pressedBackground | ||
), | ||
configure: configure | ||
) | ||
} | ||
} | ||
|
||
|
||
public struct WrappedHeaderFooterContent<ElementType:Element> : BlueprintHeaderFooterContent | ||
{ | ||
public let represented : ElementType | ||
|
||
private let isEquivalent : (Self, Self) -> Bool | ||
|
||
init( | ||
represented : ElementType, | ||
background : @escaping () -> Element?, | ||
pressedBackground : @escaping () -> Element? | ||
) where ElementType:Equatable | ||
{ | ||
self.represented = represented | ||
|
||
self.backgroundProvider = background | ||
self.pressedBackgroundProvider = pressedBackground | ||
|
||
self.isEquivalent = { | ||
$0.represented == $1.represented | ||
} | ||
} | ||
|
||
init( | ||
represented : ElementType, | ||
background : @escaping () -> Element?, | ||
pressedBackground : @escaping () -> Element? | ||
) where ElementType:LayoutEquivalent | ||
{ | ||
self.represented = represented | ||
|
||
self.backgroundProvider = background | ||
self.pressedBackgroundProvider = pressedBackground | ||
|
||
self.isEquivalent = { | ||
$0.represented.isEquivalent(to: $1.represented) | ||
} | ||
} | ||
|
||
public func isEquivalent(to other: Self) -> Bool { | ||
isEquivalent(self, other) | ||
} | ||
|
||
public var elementRepresentation: Element { | ||
represented | ||
} | ||
|
||
var backgroundProvider : () -> Element? = { nil } | ||
|
||
public var background: Element? { | ||
backgroundProvider() | ||
} | ||
|
||
var pressedBackgroundProvider : () -> Element? = { nil } | ||
|
||
public var pressedBackground: Element? { | ||
pressedBackgroundProvider() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// | ||
// 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 { | ||
|
||
/// Ensures that a well-formed error is presented when a non-Equatable or non-LayoutEquivalent element is provided. | ||
@available(*, unavailable, message: "To be directly added to a List, an Element must conform to Equatable or LayoutEquivalent.") | ||
public func listItem( | ||
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. I think this is the right direction, and only question if we should require the presence of an id. 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. I subsequently understood that this was meant to improve the builder experience. I have mixed feelings about that but do think it matches the standard folks have come to expect from SwiftUI. 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. My 2c / thoughts are that, since you only really need to provide an ID as an optimization (eg if your list is changing a lot, or you're reading data back out – eg for reordering), the framework can already make a pretty good guess what you meant, so requiring it isn't super useful 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. (And indeed, people get the ID wrong pretty often – eg just passing a 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. Hey its been over a year, I am going back on this and requiring the ID! If we see it really getting abused again, we can remove this. 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. Actually, hmm, this does make quite a few places less ergonomic. I'm going to ruminate on this one a bit more. 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. Ok, I went back on this again. Not doing an ID. |
||
id : AnyHashable? = nil, | ||
selection: ItemSelectionStyle = .notSelectable, | ||
background : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
selectedBackground : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in } | ||
) -> Item<WrappedElementContent<Self>> { | ||
fatalError() | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `Equatable` initializer for `WrappedElementContent` is called. | ||
extension Element where Self:Equatable { | ||
|
||
public func listItem( | ||
id : AnyHashable? = nil, | ||
selection: ItemSelectionStyle = .notSelectable, | ||
background : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
selectedBackground : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in } | ||
) -> Item<WrappedElementContent<Self>> { | ||
Item( | ||
WrappedElementContent( | ||
identifierValue: id, | ||
represented: self, | ||
background: background, | ||
selectedBackground: selectedBackground | ||
), | ||
configure: { | ||
$0.selectionStyle = selection | ||
|
||
configure(&$0) | ||
} | ||
) | ||
} | ||
} | ||
|
||
|
||
/// Ensures that the `LayoutEquivalent` initializer for `WrappedElementContent` is called. | ||
extension Element where Self:LayoutEquivalent { | ||
|
||
@_disfavoredOverload | ||
public func listItem( | ||
id : AnyHashable? = nil, | ||
selection: ItemSelectionStyle = .notSelectable, | ||
background : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
selectedBackground : @escaping (ApplyItemContentInfo) -> Element? = { _ in nil }, | ||
configure : (inout Item<WrappedElementContent<Self>>) -> () = { _ in } | ||
) -> Item<WrappedElementContent<Self>> { | ||
Item( | ||
WrappedElementContent( | ||
identifierValue: id, | ||
represented: self, | ||
background: background, | ||
selectedBackground: selectedBackground | ||
), | ||
configure: { | ||
$0.selectionStyle = selection | ||
|
||
configure(&$0) | ||
} | ||
) | ||
} | ||
} | ||
|
||
|
||
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, | ||
background : @escaping (ApplyItemContentInfo) -> Element?, | ||
selectedBackground : @escaping (ApplyItemContentInfo) -> Element? | ||
) where ElementType:Equatable | ||
{ | ||
self.represented = represented | ||
self.identifierValue = identifierValue | ||
|
||
self.backgroundProvider = background | ||
self.selectedBackgroundProvider = selectedBackground | ||
|
||
self.isEquivalent = { | ||
$0.represented == $1.represented | ||
} | ||
} | ||
|
||
init( | ||
identifierValue: AnyHashable?, | ||
represented: ElementType, | ||
background : @escaping (ApplyItemContentInfo) -> Element?, | ||
selectedBackground : @escaping (ApplyItemContentInfo) -> Element? | ||
) where ElementType:LayoutEquivalent | ||
{ | ||
self.represented = represented | ||
self.identifierValue = identifierValue | ||
|
||
self.backgroundProvider = background | ||
self.selectedBackgroundProvider = selectedBackground | ||
|
||
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 | ||
} | ||
|
||
var backgroundProvider: (ApplyItemContentInfo) -> Element? | ||
|
||
public func backgroundElement(with info: ApplyItemContentInfo) -> Element? { | ||
backgroundProvider(info) | ||
} | ||
|
||
var selectedBackgroundProvider: (ApplyItemContentInfo) -> Element? | ||
|
||
public func selectedBackgroundElement(with info: ApplyItemContentInfo) -> Element? { | ||
selectedBackgroundProvider(info) | ||
} | ||
} |
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