diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 2e11d0c5c..0c05e1d03 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -145,50 +145,3 @@ jobs: - name: Run Tests run: Scripts/run_ios12_tests.sh - - - ios_11: - name: iOS 11 - - runs-on: macos-10.15 - - steps: - - name: Switch To Xcode 12 - run: sudo xcode-select -switch /Applications/Xcode_12.2.app - - - name: Checkout Repository - uses: actions/checkout@v1 - - # Build Caching - - - name: Cache Bundler - uses: actions/cache@v2 - with: - path: vendor/bundle - key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gems- - - - name: Cache Cocoapods - uses: actions/cache@v2 - with: - path: Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - # Install & Build - - - name: Bundle Install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - - name: Pod Install - run: bundle exec pod install --repo-update - - - name: Install iOS 11.1 - run: xcversion simulators --install="iOS 11.1" - - - name: Run Tests - run: Scripts/run_ios11_tests.sh diff --git a/BlueprintUILists/Sources/List.Measurement.swift b/BlueprintUILists/Sources/List.Measurement.swift new file mode 100644 index 000000000..98f72671c --- /dev/null +++ b/BlueprintUILists/Sources/List.Measurement.swift @@ -0,0 +1,83 @@ +// +// List.Measurement.swift +// BlueprintUILists +// +// Created by Kyle Van Essen on 3/25/21. +// + +import ListableUI + + +/// +/// Provides the possible options for how to size and measure a list when its measured size is queried +/// by the layout system. +/// +/// You have two options: `.fillParent` and `.measureContent`. +/// +/// When using `.fillParent`, the full available fitting size will be taken up, regardless +/// of the size of the content itself. +/// +/// When using `.measureContent`, the content will be measured within the provided fitting size +/// and the smallest of the two sizes will be returned. +/// ``` +/// .fillParent: +/// ┌───────────┐ +/// │┌─────────┐│ +/// ││ ││ +/// ││ ││ +/// ││ ││ +/// ││ ││ +/// ││ ││ +/// │└─────────┘│ +/// └───────────┘ +/// +/// .measureContent +/// ┌───────────┐ +/// │ │ +/// │ │ +/// │┌─────────┐│ +/// ││ ││ +/// ││ ││ +/// ││ ││ +/// │└─────────┘│ +/// └───────────┘ +/// ``` +extension List { + + public enum Measurement : Equatable + { + /// When using `.fillParent`, the full available space will be taken up, regardless + /// of the content size of the list itself. + /// + /// Eg, if the fitting size passed to the list is (200w, 1000h), and the list's content + /// is only (200w, 500h), (200w, 1000h) will still be returned. + /// + /// This is the setting you want to use when your list is being used to fill the content + /// of a screen, such as if it is being presented in a navigation controller or tab bar controller. + /// + /// This option is the most performant, because no content measurement has to occur. + case fillParent + + /// When using `.measureContent`, the content of the list will be measured within the provided fitting size + /// and the smallest of the two sizes will be returned. + /// + /// If you are putting a list into a sheet or popover (or even another list!), this is generally the `Sizing` type + /// you will want to use, to ensure the sheet or popover takes up the minimum amount of space possible. + /// + /// - parameters: + /// - cacheKey: If provided, the underlying `Element`'s `measurementCacheKey` will be set to this value. + /// Note that this value must be unique within the entire blueprint view – so please provide a sufficiently unique value, + /// or measurement collisions will occur (one element's measurement being used for another) for duplicate keys. + /// + /// - itemLimit: When measuring the list, how many items should be measured to determine the height. Defaults + /// to 50, which is usually enough to fill the `fittingSize`. If you truly want to determine the entire height of all of + /// the content in the list, set this to `nil` (but you should rarely need to do this). The lower this value, the less + /// overall measurement that has to occur (if the value is less than the number of items in the list), which improvements + /// measurement and layout performance. + /// + case measureContent( + cacheKey : AnyHashable? = nil, + itemLimit : Int? = ListView.defaultContentSizeItemLimit + ) + } +} diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index 1e9cfecde..395c15a78 100644 --- a/BlueprintUILists/Sources/List.swift +++ b/BlueprintUILists/Sources/List.swift @@ -53,8 +53,8 @@ public struct List : Element /// it will take up all the size it is given. You can change this to /// `.measureContent` to instead measure the optimal size. /// - /// See the `ListSizing` documentation for more. - public var sizing : ListSizing + /// See the `List.Measurement` documentation for more. + public var measurement : List.Measurement // // MARK: Initialization @@ -63,10 +63,10 @@ public struct List : Element /// Create a new list, configured with the provided properties, /// configured with the provided `ListProperties` builder. public init( - sizing : ListSizing = .fillParent, + measurement : List.Measurement = .fillParent, configure : ListProperties.Configure ) { - self.sizing = sizing + self.measurement = measurement self.properties = .default(with: configure) } @@ -79,7 +79,7 @@ public struct List : Element ElementContent { size, env in ListContent( properties: self.properties, - sizing: self.sizing, + measurement: self.measurement, environment: env ) } @@ -96,11 +96,11 @@ extension List { fileprivate struct ListContent : Element { var properties : ListProperties - var sizing : ListSizing + var measurement : List.Measurement init( properties : ListProperties, - sizing : ListSizing, + measurement : List.Measurement, environment : Environment ) { var properties = properties @@ -108,13 +108,13 @@ extension List { properties.environment.blueprintEnvironment = environment self.properties = properties - self.sizing = sizing + self.measurement = measurement } // MARK: Element public var content : ElementContent { - switch self.sizing { + switch self.measurement { case .fillParent: return ElementContent { constraint -> CGSize in constraint.maximum diff --git a/BlueprintUILists/Sources/ListSizing.swift b/BlueprintUILists/Sources/ListSizing.swift deleted file mode 100644 index fb3b750ef..000000000 --- a/BlueprintUILists/Sources/ListSizing.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// ListSizing.swift -// BlueprintUILists -// -// Created by Kyle Van Essen on 3/25/21. -// - -import ListableUI - - -/// -/// Provides the possible options for how to size and measure a list when its measured size is queried -/// by the layout system. -/// -/// You have two options: `.fillParent` and `.measureContent`. -/// -/// When using `.fillParent`, the full available fitting size will be taken up, regardless -/// of the size of the content itself. -/// -/// When using `.measureContent`, the content will be measured within the provided fitting size -/// and the smallest of the two sizes will be returned. -/// ``` -/// .fillParent: -/// ┌───────────┐ -/// │┌─────────┐│ -/// ││ ││ -/// ││ ││ -/// ││ ││ -/// ││ ││ -/// ││ ││ -/// │└─────────┘│ -/// └───────────┘ -/// -/// .measureContent -/// ┌───────────┐ -/// │ │ -/// │ │ -/// │┌─────────┐│ -/// ││ ││ -/// ││ ││ -/// ││ ││ -/// │└─────────┘│ -/// └───────────┘ -/// ``` -public enum ListSizing : Equatable -{ - /// When using `.fillParent`, the full available space will be taken up, regardless - /// of the content size of the list itself. - /// - /// Eg, if the fitting size passed to the list is (200w, 1000h), and the list's content - /// is only (200w, 500h), (200w, 1000h) will still be returned. - /// - /// This is the setting you want to use when your list is being used to fill the content - /// of a screen, such as if it is being presented in a navigation controller or tab bar controller. - /// - /// This option is the most performant, because no content measurement has to occur. - case fillParent - - /// When using `.measureContent`, the content of the list will be measured within the provided fitting size - /// and the smallest of the two sizes will be returned. - /// - /// If you are putting a list into a sheet or popover (or even another list!), this is generally the `Sizing` type - /// you will want to use, to ensure the sheet or popover takes up the minimum amount of space possible. - /// - /// - parameters: - /// - cacheKey: If provided, the underlying `Element`'s `measurementCacheKey` will be set to this value. - /// Note that this value must be unique within the entire blueprint view – so please provide a sufficiently unique value, - /// or measurement collisions will occur (one element's measurement being used for another) for duplicate keys. - /// - /// - itemLimit: When measuring the list, how many items should be measured to determine the height. Defaults - /// to 50, which is usually enough to fill the `fittingSize`. If you truly want to determine the entire height of all of - /// the content in the list, set this to `nil` (but you should rarely need to do this). The lower this value, the less - /// overall measurement that has to occur (if the value is less than the number of items in the list), which improvements - /// measurement and layout performance. - /// - case measureContent( - cacheKey : AnyHashable? = nil, - itemLimit : Int? = ListView.defaultContentSizeItemLimit - ) -} diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bfb865a..40f2540e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ ### Added +- [You can now provide default list bounds for participating layouts](https://github.com/kyleve/Listable/pull/317) via the `environment.listContentBounds` property. This allows your containing screen, eg, to provide default bounds to ensure content lays out correctly. The `table` and `grid` layout types have been updated to read these content bounds. + ### Removed ### Changed +- [`ListSizing` was renamed to `List.Measurement`](https://github.com/kyleve/Listable/pull/317), to reflect that it affects measurement and to align with Blueprint's terminology for measurement. + ### Misc # Past Releases @@ -102,7 +106,7 @@ ### Changed -- [Changed how `ListView.contentSize` is implemented](https://github.com/kyleve/Listable/pull/283) in order to improve performance. An internal list is no longer used, instead we create a layout and ask it to lay out its elements. `ListSizing` also moved to `BlueprintUILists`, as that is the only place it was used. +- [Changed how `ListView.contentSize` is implemented](https://github.com/kyleve/Listable/pull/283) in order to improve performance. An internal list is no longer used, instead we create a layout and ask it to lay out its elements. `List.Measurement` also moved to `BlueprintUILists`, as that is the only place it was used. # [0.19.0] - 2021-03-22 diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 7abaf8f21..e60dd9963 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -298,7 +298,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1110; - LastUpgradeCheck = 1220; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "Kyle Van Essen"; TargetAttributes = { 0AE8554D2390933100F2E245 = { @@ -582,7 +582,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -639,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme index 41b9ff39e..557d842eb 100644 --- a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -1,6 +1,6 @@ ListContentBounds { + self.listContentBounds?(context) ?? .init() + } +} + + +extension ListContentBounds { + + /// View and layout information passed to `environment.listContentBounds` to determine + /// the correct `ListContentBounds` for the list. + public struct Context { + + /// The size of the view in question. + public var viewSize : CGSize + + /// The layout direction. + public var direction : LayoutDirection + + /// Creates a new context to use in the `ListEnvironment`'s `listContentBounds`. + public init( + viewSize: CGSize, + direction: LayoutDirection + ) { + self.viewSize = viewSize + self.direction = direction + } + } +} + + +/// A key used to store default / provided bounds into the list's environment. +/// Useful if a parent screen would like to provide default width constraints +/// to be applied to participating layouts. +public enum ListContentBoundsKey : ListEnvironmentKey { + + public typealias Provider = (ListContentBounds.Context) -> ListContentBounds + public typealias Value = Provider? + + public static var defaultValue: Value { + nil + } +} diff --git a/ListableUI/Sources/ListEnvironment.swift b/ListableUI/Sources/Environment/ListEnvironment.swift similarity index 82% rename from ListableUI/Sources/ListEnvironment.swift rename to ListableUI/Sources/Environment/ListEnvironment.swift index 8aad0c773..554ea2d14 100644 --- a/ListableUI/Sources/ListEnvironment.swift +++ b/ListableUI/Sources/Environment/ListEnvironment.swift @@ -66,15 +66,3 @@ public struct ListEnvironment { private var values: [ObjectIdentifier: Any] = [:] } - -/// Defines a value stored in the `ListEnvironment` of a list. -/// -/// See `ListEnvironment` for more info and examples. -public protocol ListEnvironmentKey { - - /// The type of value stored by this key. - associatedtype Value - - /// The default value that will be vended by an `Environment` for this key if no other value has been set. - static var defaultValue: Self.Value { get } -} diff --git a/ListableUI/Sources/Environment/ListEnvironmentKey.swift b/ListableUI/Sources/Environment/ListEnvironmentKey.swift new file mode 100644 index 000000000..4a3e1b208 --- /dev/null +++ b/ListableUI/Sources/Environment/ListEnvironmentKey.swift @@ -0,0 +1,21 @@ +// +// ListEnvironmentKey.swift +// ListEnvironmentKey +// +// Created by Kyle Van Essen on 8/14/21. +// + +import Foundation + + +/// Defines a value stored in the `ListEnvironment` of a list. +/// +/// See `ListEnvironment` for more info and examples. +public protocol ListEnvironmentKey { + + /// The type of value stored by this key. + associatedtype Value + + /// The default value that will be vended by an `Environment` for this key if no other value has been set. + static var defaultValue: Self.Value { get } +} diff --git a/ListableUI/Sources/Layout/CollectionViewLayout.swift b/ListableUI/Sources/Layout/CollectionViewLayout.swift index d8a1c8403..2e7a3a09c 100644 --- a/ListableUI/Sources/Layout/CollectionViewLayout.swift +++ b/ListableUI/Sources/Layout/CollectionViewLayout.swift @@ -461,14 +461,19 @@ final class CollectionViewLayout : UICollectionViewLayout { let view = self.collectionView! + let context = ListLayoutLayoutContext( + collectionView: view, + environment: self.delegate.listViewLayoutCurrentEnvironment() + ) + self.layout.layout( delegate: self.delegate, - in: .init(view) + in: context ) self.layout.content.setSectionContentsFrames() - self.layout.updateLayout(in: view) + self.layout.updateLayout(in: context) self.layout.setZIndexes() @@ -482,9 +487,14 @@ final class CollectionViewLayout : UICollectionViewLayout { let view = self.collectionView! + let context = ListLayoutLayoutContext( + collectionView: view, + environment: self.delegate.listViewLayoutCurrentEnvironment() + ) + self.layout.positionStickySectionHeadersIfNeeded(in: view) - self.layout.updateLayout(in: view) + self.layout.updateLayout(in: context) } // @@ -675,6 +685,8 @@ public protocol CollectionViewLayoutDelegate : AnyObject defaults: ListLayoutDefaults ) -> ListLayoutContent + func listViewLayoutCurrentEnvironment() -> ListEnvironment + func listViewLayoutDidLayoutContents() func listViewShouldEndQueueingEditsForReorder() diff --git a/ListableUI/Sources/Layout/Grid/GridListLayout.swift b/ListableUI/Sources/Layout/Grid/GridListLayout.swift index 63e0b0d99..b2bff4765 100644 --- a/ListableUI/Sources/Layout/Grid/GridListLayout.swift +++ b/ListableUI/Sources/Layout/Grid/GridListLayout.swift @@ -18,6 +18,8 @@ extension LayoutDescription public struct GridAppearance : ListLayoutAppearance { + public var bounds : ListContentBounds? + public var sizing : Sizing public var layout : Layout @@ -33,10 +35,12 @@ public struct GridAppearance : ListLayoutAppearance public init( stickySectionHeaders : Bool = true, + bounds : ListContentBounds? = nil, sizing : Sizing = Sizing(), layout : Layout = Layout() ) { self.stickySectionHeaders = stickySectionHeaders + self.bounds = bounds self.sizing = sizing self.layout = layout } @@ -84,9 +88,6 @@ public struct GridAppearance : ListLayoutAppearance public struct Layout : Equatable { - public var padding : UIEdgeInsets - public var width : WidthConstraint - public var interSectionSpacingWithNoFooter : CGFloat public var interSectionSpacingWithFooter : CGFloat @@ -100,11 +101,7 @@ public struct GridAppearance : ListLayoutAppearance interSectionSpacingWithFooter : CGFloat = 0.0, sectionHeaderBottomSpacing : CGFloat = 0.0, itemToSectionFooterSpacing : CGFloat = 0.0 - ) - { - self.padding = padding - self.width = width - + ) { self.interSectionSpacingWithNoFooter = interSectionSpacingWithNoFooter self.interSectionSpacingWithFooter = interSectionSpacingWithFooter @@ -254,7 +251,7 @@ final class GridListLayout : ListLayout // MARK: Performing Layouts // - func updateLayout(in collectionView: UICollectionView) + func updateLayout(in context : ListLayoutLayoutContext) { } @@ -263,6 +260,14 @@ final class GridListLayout : ListLayout delegate : CollectionViewLayoutDelegate?, in context : ListLayoutLayoutContext ) { + let boundsContext = ListContentBounds.Context( + viewSize: context.viewBounds.size, + direction: self.direction + ) + + let bounds = self.layoutAppearance.bounds ?? context.environment.listContentBounds(in: boundsContext) + + let direction = self.layoutAppearance.direction let layout = self.layoutAppearance.layout let sizing = self.layoutAppearance.sizing @@ -273,8 +278,8 @@ final class GridListLayout : ListLayout let rootWidth = TableAppearance.Layout.width( with: viewWidth, - padding: HorizontalPadding(left: layout.padding.left, right: layout.padding.right), - constraint: layout.width + padding: HorizontalPadding(left: bounds.padding.left, right: bounds.padding.right), + constraint: bounds.width ) // @@ -312,12 +317,12 @@ final class GridListLayout : ListLayout switch direction { case .vertical: - lastSectionMaxY += layout.padding.top - lastContentMaxY += layout.padding.top + lastSectionMaxY += bounds.padding.top + lastContentMaxY += bounds.padding.top case .horizontal: - lastSectionMaxY += layout.padding.left - lastContentMaxY += layout.padding.left + lastSectionMaxY += bounds.padding.left + lastContentMaxY += bounds.padding.left } // @@ -415,8 +420,8 @@ final class GridListLayout : ListLayout } switch direction { - case .vertical: lastContentMaxY += layout.padding.bottom - case .horizontal: lastContentMaxY += layout.padding.right + case .vertical: lastContentMaxY += bounds.padding.bottom + case .horizontal: lastContentMaxY += bounds.padding.right } // diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayout.swift b/ListableUI/Sources/Layout/ListLayout/ListLayout.swift index 1c49442e5..accfd44ca 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayout.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayout.swift @@ -25,19 +25,31 @@ public protocol ListLayout : AnyListLayout } -public struct ListLayoutLayoutContext : Equatable { +public struct ListLayoutLayoutContext { public var viewBounds : CGRect public var safeAreaInsets : UIEdgeInsets - init(viewBounds : CGRect, safeAreaInsets : UIEdgeInsets) { + public var environment : ListEnvironment + + init( + viewBounds : CGRect, + safeAreaInsets : UIEdgeInsets, + environment : ListEnvironment + ) { self.viewBounds = viewBounds self.safeAreaInsets = safeAreaInsets + self.environment = environment } - init(_ collectionView : UICollectionView) { + init( + collectionView : UICollectionView, + environment : ListEnvironment + ) { self.viewBounds = collectionView.bounds self.safeAreaInsets = collectionView.safeAreaInsets + + self.environment = environment } } @@ -74,7 +86,7 @@ public protocol AnyListLayout : AnyObject // MARK: Performing Layouts // - func updateLayout(in collectionView : UICollectionView) + func updateLayout(in context : ListLayoutLayoutContext) func layout( delegate : CollectionViewLayoutDelegate?, diff --git a/ListableUI/Sources/Layout/Paged/PagedListLayout.swift b/ListableUI/Sources/Layout/Paged/PagedListLayout.swift index ed043d44c..307a62d09 100644 --- a/ListableUI/Sources/Layout/Paged/PagedListLayout.swift +++ b/ListableUI/Sources/Layout/Paged/PagedListLayout.swift @@ -145,7 +145,7 @@ final class PagedListLayout : ListLayout // MARK: Performing Layouts // - func updateLayout(in collectionView : UICollectionView) + func updateLayout(in context : ListLayoutLayoutContext) { // Nothing needed. } diff --git a/ListableUI/Sources/Layout/Retail Grid/RetailGridListLayout.swift b/ListableUI/Sources/Layout/Retail Grid/RetailGridListLayout.swift index 53333d295..939c9a9b9 100644 --- a/ListableUI/Sources/Layout/Retail Grid/RetailGridListLayout.swift +++ b/ListableUI/Sources/Layout/Retail Grid/RetailGridListLayout.swift @@ -254,7 +254,7 @@ final class RetailGridListLayout : ListLayout // MARK: Performing Layouts // - func updateLayout(in collectionView: UICollectionView) + func updateLayout(in context : ListLayoutLayoutContext) { } diff --git a/ListableUI/Sources/Layout/Table/TableListLayout.swift b/ListableUI/Sources/Layout/Table/TableListLayout.swift index 36f6ca041..321a9ed97 100644 --- a/ListableUI/Sources/Layout/Table/TableListLayout.swift +++ b/ListableUI/Sources/Layout/Table/TableListLayout.swift @@ -110,6 +110,9 @@ public struct TableAppearance : ListLayoutAppearance public var stickySectionHeaders : Bool + /// The bounds of the content of the list, which can be optionally constrained. + public var bounds : ListContentBounds? + /// Default sizing attributes for content in the list. public var sizing : Sizing @@ -123,10 +126,12 @@ public struct TableAppearance : ListLayoutAppearance /// Creates a new `TableAppearance` object. public init( stickySectionHeaders : Bool = true, - sizing : Sizing = Sizing(), - layout : Layout = Layout() + bounds : ListContentBounds? = nil, + sizing : Sizing = .init(), + layout : Layout = .init() ) { self.stickySectionHeaders = stickySectionHeaders + self.bounds = bounds self.sizing = sizing self.layout = layout } @@ -285,11 +290,6 @@ extension TableAppearance /// Layout options for the list. public struct Layout : Equatable { - /// The padding to place around the outside of the content of the list. - public var padding : UIEdgeInsets - /// The width of the content of the list, which can be optionally constrained. - public var width : WidthConstraint - /// The spacing between the list header and the first section. /// Not applied if there is no list header. public var headerToFirstSectionSpacing : CGFloat @@ -314,8 +314,6 @@ extension TableAppearance /// Creates a new `Layout` with the provided options. public init( - padding : UIEdgeInsets = .zero, - width : WidthConstraint = .noConstraint, headerToFirstSectionSpacing : CGFloat = 0.0, interSectionSpacingWithNoFooter : CGFloat = 0.0, interSectionSpacingWithFooter : CGFloat = 0.0, @@ -325,9 +323,6 @@ extension TableAppearance lastSectionToFooterSpacing : CGFloat = 0.0 ) { - self.padding = padding - self.width = width - self.headerToFirstSectionSpacing = headerToFirstSectionSpacing self.interSectionSpacingWithNoFooter = interSectionSpacingWithNoFooter @@ -444,7 +439,7 @@ final class TableListLayout : ListLayout // MARK: Performing Layouts // - func updateLayout(in collectionView : UICollectionView) + func updateLayout(in context : ListLayoutLayoutContext) { } @@ -453,20 +448,28 @@ final class TableListLayout : ListLayout delegate : CollectionViewLayoutDelegate?, in context : ListLayoutLayoutContext ) { + let boundsContext = ListContentBounds.Context( + viewSize: context.viewBounds.size, + direction: self.direction + ) + + let bounds = self.layoutAppearance.bounds ?? context.environment.listContentBounds(in: boundsContext) + let layout = self.layoutAppearance.layout + let sizing = self.layoutAppearance.sizing let viewSize = context.viewBounds.size let viewWidth = context.viewBounds.width let rootWidth = CustomWidth.custom(CustomWidth.Custom( - padding: HorizontalPadding(left: layout.padding.left, right: layout.padding.right), - width: layout.width, + padding: HorizontalPadding(left: bounds.padding.left, right: bounds.padding.right), + width: bounds.width, alignment: .center )) let defaultWidth = rootWidth.position(with: viewSize, defaultWidth: viewSize.width).width - + // // Item Positioning // @@ -517,10 +520,10 @@ final class TableListLayout : ListLayout switch direction { case .vertical: - lastContentMaxY += layout.padding.top + lastContentMaxY += bounds.padding.top case .horizontal: - lastContentMaxY += layout.padding.left + lastContentMaxY += bounds.padding.left } performLayout(for: self.content.header) { header in @@ -739,8 +742,8 @@ final class TableListLayout : ListLayout } switch direction { - case .vertical: lastContentMaxY += layout.padding.bottom - case .horizontal: lastContentMaxY += layout.padding.right + case .vertical: lastContentMaxY += bounds.padding.bottom + case .horizontal: lastContentMaxY += bounds.padding.right } // diff --git a/ListableUI/Sources/ListView/ListView+ContentSize.swift b/ListableUI/Sources/ListView/ListView+ContentSize.swift index 5373da22e..d2ad6c696 100644 --- a/ListableUI/Sources/ListView/ListView+ContentSize.swift +++ b/ListableUI/Sources/ListView/ListView+ContentSize.swift @@ -1,6 +1,6 @@ // -// ListSizing.swift -// Listable +// ListView+ContentSize.swift +// ListableUI // // Created by Kyle Van Essen on 9/21/20. // @@ -79,7 +79,11 @@ extension ListView layout.layout( delegate: nil, - in: .init(viewBounds: CGRect(origin: .zero, size: fittingSize), safeAreaInsets: .zero) + in: .init( + viewBounds: CGRect(origin: .zero, size: fittingSize), + safeAreaInsets: .zero, + environment: properties.environment + ) ) /// 3) Constrain the measurement to the `fittingSize`. diff --git a/ListableUI/Sources/ListView/ListView.DataSource.swift b/ListableUI/Sources/ListView/ListView.DataSource.swift index a099a027b..669a21934 100644 --- a/ListableUI/Sources/ListView/ListView.DataSource.swift +++ b/ListableUI/Sources/ListView/ListView.DataSource.swift @@ -14,8 +14,6 @@ internal extension ListView unowned var presentationState : PresentationState! unowned var storage : ListView.Storage! unowned var liveCells : LiveCells! - - var environment : ListEnvironment! func numberOfSections(in collectionView: UICollectionView) -> Int { @@ -41,7 +39,7 @@ internal extension ListView let cell = item.dequeueAndPrepareCollectionViewCell( in: collectionView, for: indexPath, - environment: environment + environment: self.view.environment ) cell.wasDequeued(with: self.liveCells) @@ -62,7 +60,7 @@ internal extension ListView for: kind, at: indexPath, reuseCache: self.headerFooterReuseCache, - environment: self.environment + environment: self.view.environment ) container.headerFooter = { diff --git a/ListableUI/Sources/ListView/ListView.Delegate.swift b/ListableUI/Sources/ListView/ListView.Delegate.swift index c7400ce13..bcc141e90 100644 --- a/ListableUI/Sources/ListView/ListView.Delegate.swift +++ b/ListableUI/Sources/ListView/ListView.Delegate.swift @@ -262,6 +262,10 @@ extension ListView ) } + func listViewLayoutCurrentEnvironment() -> ListEnvironment { + self.view.environment + } + func listViewLayoutDidLayoutContents() { self.view.visibleContent.update(with: self.view) } diff --git a/ListableUI/Sources/ListView/ListView.swift b/ListableUI/Sources/ListView/ListView.swift index c025da6ec..be52fbe6b 100644 --- a/ListableUI/Sources/ListView/ListView.swift +++ b/ListableUI/Sources/ListView/ListView.swift @@ -73,7 +73,6 @@ public final class ListView : UIView, KeyboardObserverDelegate self.dataSource.view = self self.dataSource.presentationState = self.storage.presentationState self.dataSource.storage = self.storage - self.dataSource.environment = self.environment self.dataSource.liveCells = self.liveCells self.delegate.view = self @@ -619,11 +618,15 @@ public final class ListView : UIView, KeyboardObserverDelegate // MARK: Setting & Getting Content // - public var environment : ListEnvironment { - didSet { - self.dataSource.environment = self.environment - } - } + /// The environment associated with the list, which is used to pass data through to + /// the list's layout, or through to items, headers/footers, etc. + /// + /// If you have used SwiftUI's environment, Listable's environment is similar. + /// + /// ### Note + /// Setting the environment, or a property on the environment, does **not** force a re-layout + /// of the list view. The newly provided environment values will be used during the next update. + public var environment : ListEnvironment public var content : Content { get { return self.storage.allContent } diff --git a/ListableUI/Sources/Previews/ItemPreviewAppearance.swift b/ListableUI/Sources/Previews/ItemPreviewAppearance.swift index edc301274..3981cabd1 100644 --- a/ListableUI/Sources/Previews/ItemPreviewAppearance.swift +++ b/ListableUI/Sources/Previews/ItemPreviewAppearance.swift @@ -32,7 +32,14 @@ public struct ItemPreviewAppearance : Equatable properties.appearance.backgroundColor = self.backgroundColor properties.layout = .table { - $0.layout.padding = UIEdgeInsets(top: self.padding, left: self.padding, bottom: self.padding, right: self.padding) + $0.bounds = .init( + padding: UIEdgeInsets( + top: self.padding, + left: self.padding, + bottom: self.padding, + right: self.padding + ) + ) } } } diff --git a/ListableUI/Tests/Layout/ListLayout/LayoutDescriptionTests.swift b/ListableUI/Tests/Layout/ListLayout/LayoutDescriptionTests.swift index f1c5d8a49..d2cc6582c 100644 --- a/ListableUI/Tests/Layout/ListLayout/LayoutDescriptionTests.swift +++ b/ListableUI/Tests/Layout/ListLayout/LayoutDescriptionTests.swift @@ -120,7 +120,7 @@ private final class TestLayout : ListLayout self.content = content } - func updateLayout(in collectionView: UICollectionView) { } + func updateLayout(in context : ListLayoutLayoutContext) { } func layout(delegate: CollectionViewLayoutDelegate?, in context: ListLayoutLayoutContext) {} } diff --git a/ListableUI/Tests/Layout/Table/TableListLayoutTests.swift b/ListableUI/Tests/Layout/Table/TableListLayoutTests.swift index fcb85ded1..17e97b80b 100644 --- a/ListableUI/Tests/Layout/Table/TableListLayoutTests.swift +++ b/ListableUI/Tests/Layout/Table/TableListLayoutTests.swift @@ -46,8 +46,6 @@ class TableAppearance_LayoutTests : XCTestCase { let layout = TableAppearance.Layout() - XCTAssertEqual(layout.padding, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - XCTAssertEqual(layout.width, .noConstraint) XCTAssertEqual(layout.interSectionSpacingWithNoFooter, 0.0) XCTAssertEqual(layout.interSectionSpacingWithFooter, 0.0) XCTAssertEqual(layout.sectionHeaderBottomSpacing, 0.0) @@ -98,9 +96,13 @@ class TableListLayoutTests : XCTestCase listView.configure { list in list.layout = .table { - $0.layout = .init( + + $0.bounds = .init( padding: UIEdgeInsets(top: 10.0, left: 20.0, bottom: 30.0, right: 40.0), - width: .noConstraint, + width: .noConstraint + ) + + $0.layout = .init( headerToFirstSectionSpacing: 10.0, interSectionSpacingWithNoFooter: 15.0, interSectionSpacingWithFooter: 20.0, diff --git a/ListableUI/Tests/ListView/ListView.LayoutManagerTests.swift b/ListableUI/Tests/ListView/ListView.LayoutManagerTests.swift index 9ed926943..418b84060 100644 --- a/ListableUI/Tests/ListView/ListView.LayoutManagerTests.swift +++ b/ListableUI/Tests/ListView/ListView.LayoutManagerTests.swift @@ -35,7 +35,7 @@ class LayoutManagerTests : XCTestCase /// Setting the same layout type, but changing the description should change the inner ListLayout. let newLayout1 : LayoutDescription = .table { - $0.layout.padding = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + $0.bounds = .init(padding: UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)) } manager.set(layout: newLayout1, animated: false, completion: {}) diff --git a/README.md b/README.md index b4c681a3f..3f665f518 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ struct Underflow : Equatable ### Self-Sizing Cells -Another common pain-point for standard `UITableViews` or `UICollectionViews` is handling dynamic and self sizing cells. Listable handles this transparently for you, and provides many ways to size content. Each `Item` has a `sizing` property, which can be set to any of the following values. `.default` pulls the default sizing of the item from the `ListSizing` mentioned above, where as the `thatFits` and `autolayout` values size the item based on `sizeThatFits` and `systemLayoutSizeFitting`, respectively. +Another common pain-point for standard `UITableViews` or `UICollectionViews` is handling dynamic and self sizing cells. Listable handles this transparently for you, and provides many ways to size content. Each `Item` has a `sizing` property, which can be set to any of the following values. `.default` pulls the default sizing of the item from the `List.Measurement` mentioned above, where as the `thatFits` and `autolayout` values size the item based on `sizeThatFits` and `systemLayoutSizeFitting`, respectively. ```swift public enum Sizing : Equatable