diff --git a/BlueprintUILists/Sources/Element+HeaderFooter.swift b/BlueprintUILists/Sources/Element+HeaderFooter.swift new file mode 100644 index 000000000..99e5d574c --- /dev/null +++ b/BlueprintUILists/Sources/Element+HeaderFooter.swift @@ -0,0 +1,81 @@ +// +// 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>) -> () = { _ in } + ) -> HeaderFooter> { + HeaderFooter( + WrappedHeaderFooterContent( + represented: self + ), + configure: configure + ) + } +} + + +public struct WrappedHeaderFooterContent : 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 + } +} diff --git a/BlueprintUILists/Sources/Element+Item.swift b/BlueprintUILists/Sources/Element+Item.swift new file mode 100644 index 000000000..cf3f863ef --- /dev/null +++ b/BlueprintUILists/Sources/Element+Item.swift @@ -0,0 +1,122 @@ +// +// Element+Item.swift +// 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( + configure : (inout Item>) -> () = { _ in } + ) -> Item> { + 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 : ID, + configure : (inout Item>) -> () = { _ in } + ) -> Item> { + 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 { + /// 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 : 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 { + .ifNotEquivalent + } +} + + +extension WrappedElementContent where ElementType : IsEquivalentContent { + + public func isEquivalent(to other: Self) -> Bool { + represented.isEquivalent(to: other.represented) + } + + public var reappliesToVisibleView: ReappliesToVisibleView { + .ifNotEquivalent + } +} diff --git a/BlueprintUILists/Sources/Element.swift b/BlueprintUILists/Sources/Element.swift deleted file mode 100644 index 544376514..000000000 --- a/BlueprintUILists/Sources/Element.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Element.swift -// BlueprintUILists -// -// Created by Kyle Van Essen on 7/24/22. -// - -import BlueprintUI -import ListableUI - - -// MARK: Item / ItemContent Extensions - -extension Element { - - func asItem( - configure : (inout Item>) -> () = { _ in } - ) -> Item> { - Item( - WrappedElementContent( - represented: self, - identifierValue: ObjectIdentifier(Self.Type.self) - ), - configure: configure - ) - } - - func asItem( - id : ID, - configure : (inout Item>) -> () = { _ in } - ) -> Item> { - Item( - WrappedElementContent( - represented: self, - identifierValue: id - ), - configure: configure - ) - } -} - - -public struct WrappedElementContent : 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 { - .ifNotEquivalent - } -} diff --git a/BlueprintUILists/Sources/HeaderFooter.swift b/BlueprintUILists/Sources/ElementHeaderFooter.swift similarity index 95% rename from BlueprintUILists/Sources/HeaderFooter.swift rename to BlueprintUILists/Sources/ElementHeaderFooter.swift index b255c14f7..4a12e352a 100644 --- a/BlueprintUILists/Sources/HeaderFooter.swift +++ b/BlueprintUILists/Sources/ElementHeaderFooter.swift @@ -1,5 +1,5 @@ // -// HeaderFooter.swift +// ElementHeaderFooter.swift // BlueprintUILists // // Created by Kyle Van Essen on 10/9/20. @@ -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. @@ -62,6 +64,8 @@ public func ElementHeaderFooter( ) } +/// +/// ⚠️ 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. diff --git a/BlueprintUILists/Sources/Item.swift b/BlueprintUILists/Sources/ElementItem.swift similarity index 96% rename from BlueprintUILists/Sources/Item.swift rename to BlueprintUILists/Sources/ElementItem.swift index d82355ef8..7a0d5bbab 100644 --- a/BlueprintUILists/Sources/Item.swift +++ b/BlueprintUILists/Sources/ElementItem.swift @@ -1,5 +1,5 @@ // -// Item.swift +// ElementItem.swift // BlueprintUILists // // Created by Kyle Van Essen on 9/10/20. @@ -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. @@ -68,6 +70,8 @@ public func ElementItem( /// +/// ⚠️ 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. /// diff --git a/BlueprintUILists/Sources/ListableBuilder+Element.swift b/BlueprintUILists/Sources/ListableBuilder+Element.swift new file mode 100644 index 000000000..26916c82b --- /dev/null +++ b/BlueprintUILists/Sources/ListableBuilder+Element.swift @@ -0,0 +1,74 @@ +// +// 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 +/// } +/// ``` +/// +/// ## Note +/// Takes advantage of `@_disfavoredOverload` to avoid ambiguous method resolution with the default implementations. +/// 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() } + } +} + diff --git a/BlueprintUILists/Sources/Section+Element.swift b/BlueprintUILists/Sources/Section+Element.swift new file mode 100644 index 000000000..ecbc9d1c5 --- /dev/null +++ b/BlueprintUILists/Sources/Section+Element.swift @@ -0,0 +1,56 @@ +// +// Section+Element.swift +// BlueprintUILists +// +// Created by Kyle Van Essen on 7/24/22. +// + +import BlueprintUI +import ListableUI + + +extension Section { + + /// Adds `Element` support when building a `Section`: + /// + /// ```swift + /// Section("id") { section in + /// section.add(Element1()) + /// section.add(Element2()) + /// } + /// ``` + public mutating func add(_ item : Element) + { + self.items.append(item.toAnyItemConvertible().toAnyItem()) + } + + /// ```swift + /// Section("id") { section in + /// section += Element1() + /// section += Element2() + /// } + /// ``` + + public static func += (lhs : inout Section, rhs : Element) + { + lhs.add(rhs) + } + + /// Adds `Element` support when building a `Section`: + /// + /// ```swift + /// Section("3") { section in + /// section.add { + /// TestContent1() // An ItemContent + /// + /// Element1() // A Element + /// Element2() // A Element + /// } + /// } + /// ``` + public mutating func add( + @ListableBuilder items : () -> [Element] + ) { + self.items += items().map { $0.toAnyItemConvertible().toAnyItem() } + } +} diff --git a/BlueprintUILists/Tests/ListableBuilderAndSectionOverloadTests.swift b/BlueprintUILists/Tests/ListableBuilderAndSectionOverloadTests.swift new file mode 100644 index 000000000..d25e4ea3c --- /dev/null +++ b/BlueprintUILists/Tests/ListableBuilderAndSectionOverloadTests.swift @@ -0,0 +1,90 @@ +// +// ListableBuilderAndSectionOverloadTests.swift +// BlueprintUILists +// +// Created by Kyle Van Essen on 7/24/22. +// + +import BlueprintUILists +import XCTest + + +class ListableBuilderAndSectionOverloadTests : XCTestCase { + + func test_build() { + + let list = List { + Section("1") { + TestContent1() + TestContent1() + TestContent2() + + Element1() + Element2() + } + + Section("2") { section in + section += TestContent1() + section += TestContent2() + + section += Element1() + section += Element2() + } + + Section("3") { section in + section.add { + TestContent1() + + Element1() + Element2() + } + } + } + + XCTAssertEqual(list.properties.content.sections.count, 3) + + XCTAssertEqual(list.properties.content.sections[0].count, 5) + XCTAssertEqual(list.properties.content.sections[1].count, 4) + XCTAssertEqual(list.properties.content.sections[2].count, 3) + } +} + + +fileprivate struct Element1 : ProxyElement { + + var elementRepresentation: Element { + Empty() + } +} + + +fileprivate struct Element2 : ProxyElement { + + var elementRepresentation: Element { + Empty() + } +} + + +fileprivate struct TestContent1 : BlueprintItemContent, Equatable { + + var identifierValue: String { + "1" + } + + func element(with info: ApplyItemContentInfo) -> Element { + Empty() + } +} + + +fileprivate struct TestContent2 : BlueprintItemContent, Equatable { + + var identifierValue: String { + "1" + } + + func element(with info: ApplyItemContentInfo) -> Element { + Empty() + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0aa5a05..7cb423ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ ### Added +- Adding Blueprint Elements to your list content has become much easier: Just add them! You no longer need to use the `ElementItem` wrapper, unless you need to provide an isEquivalent implementation, a background view, or a selected background view. + + ```swift + Section("an id") { + MyContent() // Regular ItemContent + + MyElement() // A Blueprint Element + AnotherElement() // A Blueprint Element + AnotherElement() + .item(id: "my-specified-id") { item in + item.insertAndRemoveAnimations = .scaleUp + } + } + ``` + ### Removed ### Changed diff --git a/ListableUI/Sources/Section/Section.swift b/ListableUI/Sources/Section/Section.swift index 90967d7ae..04fbd7f68 100644 --- a/ListableUI/Sources/Section/Section.swift +++ b/ListableUI/Sources/Section/Section.swift @@ -138,24 +138,6 @@ public struct Section self.footer = footer() } - /// Creates a new section with result builder-style APIs. - public init( - _ identifier : IdentifierValue, - @ListableBuilder items : () -> [AnyItemConvertible], - header : () -> AnyHeaderFooterConvertible? = { nil }, - footer : () -> AnyHeaderFooterConvertible? = { nil } - ) { - self.identifier = Identifier(identifier) - - self.layouts = .init() - self.reordering = .init() - - self.items = items().map { $0.toAnyItem() } - - self.header = header() - self.footer = footer() - } - // // MARK: Reading Items //