diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bb0a20f..d44684371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -255,10 +255,10 @@ Earlier releases were ad-hoc and not tracked. To see all changes, please reference [closed PRs on Github](https://github.com/kyleve/Listable/pulls?q=is%3Apr+is%3Aclosed). -[Main]: https://github.com/square/Blueprint/compare/0.13.0...HEAD -[0.13.0]: https://github.com/square/Blueprint/compare/0.12.1...0.13.0 -[0.12.1]: https://github.com/square/Blueprint/compare/0.12.0...0.12.1 -[0.12.0]: https://github.com/square/Blueprint/compare/0.11.0...0.12.0 -[0.11.0]: https://github.com/square/Blueprint/compare/0.10.1...0.11.0 -[0.10.1]: https://github.com/square/Blueprint/compare/0.10.0...0.10.1 -[0.10.0]: https://github.com/square/Blueprint/compare/0.9.0...0.10.0 +[Main]: https://github.com/kyleve/Listable/compare/0.13.0...HEAD +[0.13.0]: https://github.com/kyleve/Listable/compare/0.12.1...0.13.0 +[0.12.1]: https://github.com/kyleve/Listable/compare/0.12.0...0.12.1 +[0.12.0]: https://github.com/kyleve/Listable/compare/0.11.0...0.12.0 +[0.11.0]: https://github.com/kyleve/Listable/compare/0.10.1...0.11.0 +[0.10.1]: https://github.com/kyleve/Listable/compare/0.10.0...0.10.1 +[0.10.0]: https://github.com/kyleve/Listable/compare/0.9.0...0.10.0 diff --git a/ListableUI/Sources/AutoScrollAction.swift b/ListableUI/Sources/AutoScrollAction.swift index 108032987..2cdb2e192 100644 --- a/ListableUI/Sources/AutoScrollAction.swift +++ b/ListableUI/Sources/AutoScrollAction.swift @@ -10,6 +10,7 @@ import Foundation /// Options for auto-scrolling to items when the list is updated. public enum AutoScrollAction { + /// The list never automatically scrolls. case none @@ -71,10 +72,11 @@ public enum AutoScrollAction { } } -public extension AutoScrollAction + +extension AutoScrollAction { /// Where to scroll as a result of an `AutoScrollAction`. - enum ScrollDestination : Equatable + public enum ScrollDestination : Equatable { /// Scroll to the first item in the list. case firstItem @@ -94,8 +96,9 @@ public extension AutoScrollAction } } + /// Values used to configure the `scrollToItem(onInsertOf:)` action. - struct OnInsertedItem + public struct OnInsertedItem { /// The item in the list to scroll to when the `insertedIdentifier` is inserted. public var destination : ScrollDestination diff --git a/ListableUI/Sources/Behavior.swift b/ListableUI/Sources/Behavior.swift index 668e39150..6e2d30a5f 100644 --- a/ListableUI/Sources/Behavior.swift +++ b/ListableUI/Sources/Behavior.swift @@ -63,10 +63,11 @@ public struct Behavior : Equatable } } -public extension Behavior + +extension Behavior { /// How to adjust the `contentInset` of the list when the keyboard visibility changes. - enum KeyboardAdjustmentMode : Equatable + public enum KeyboardAdjustmentMode : Equatable { /// The `contentInset` of the list is not adjusted when the keyboard appears or disappears. case none @@ -75,8 +76,9 @@ public extension Behavior case adjustsWhenVisible } + /// How to react when the user taps on the status bar of the application. - enum ScrollsToTop : Equatable + public enum ScrollsToTop : Equatable { /// No action is performed when the user taps on the status bar. case disabled @@ -85,8 +87,9 @@ public extension Behavior case enabled } + /// The selection mode of the list view, which controls how many items (if any) can be selected at once. - enum SelectionMode : Equatable + public enum SelectionMode : Equatable { /// The list view does not allow any selections. case none @@ -112,7 +115,8 @@ public extension Behavior case multiple } - struct Underflow : Equatable + + public struct Underflow : Equatable { public var alwaysBounce : Bool public var alignment : Alignment diff --git a/ListableUI/Sources/Content.swift b/ListableUI/Sources/Content.swift index 81bf8f526..343b54c42 100644 --- a/ListableUI/Sources/Content.swift +++ b/ListableUI/Sources/Content.swift @@ -298,7 +298,7 @@ public struct Content } -internal extension Content +extension Content { struct Slice { diff --git a/ListableUI/Sources/EmbeddedList.swift b/ListableUI/Sources/EmbeddedList.swift index cdabce703..8c29aa16f 100644 --- a/ListableUI/Sources/EmbeddedList.swift +++ b/ListableUI/Sources/EmbeddedList.swift @@ -42,32 +42,6 @@ public extension Item where Content == EmbeddedList } -public extension EmbeddedList -{ - /// How you specify sizing for an embedded list. The surface area - /// of this `Sizing` enum is intentionally reduced from the standard `Sizing` - /// enum, because several of those values do not make sense for embedded lists. - enum Sizing : Equatable - { - /// Falls back to the default sizing of `Item`s in the list view. - case `default` - - /// A fixed size item with the given width or height. - /// - /// Note: Depending on the list layout type, only one of width or height may be used. - /// Eg, for list layouts, vertical lists only use the height, and horizontal lists only use the width. - case fixed(width: CGFloat = 0.0, height : CGFloat = 0.0) - - var toStandardSizing : ListableUI.Sizing { - switch self { - case .default: return .default - case .fixed(let w, let h): return .fixed(width: w, height: h) - } - } - } -} - - /// Describes item content which can be used to embed a list inside another list, /// for example if you'd like to place a horizontally scrollable list within a vertically scrolling /// list, or vice versa. @@ -134,3 +108,29 @@ public struct EmbeddedList : ItemContent ListView(frame: frame) } } + + +extension EmbeddedList +{ + /// How you specify sizing for an embedded list. The surface area + /// of this `Sizing` enum is intentionally reduced from the standard `Sizing` + /// enum, because several of those values do not make sense for embedded lists. + public enum Sizing : Equatable + { + /// Falls back to the default sizing of `Item`s in the list view. + case `default` + + /// A fixed size item with the given width or height. + /// + /// Note: Depending on the list layout type, only one of width or height may be used. + /// Eg, for list layouts, vertical lists only use the height, and horizontal lists only use the width. + case fixed(width: CGFloat = 0.0, height : CGFloat = 0.0) + + var toStandardSizing : ListableUI.Sizing { + switch self { + case .default: return .default + case .fixed(let w, let h): return .fixed(width: w, height: h) + } + } + } +} diff --git a/ListableUI/Sources/HeaderFooter/AnyHeaderFooter.swift b/ListableUI/Sources/HeaderFooter/AnyHeaderFooter.swift new file mode 100644 index 000000000..2dc000ac2 --- /dev/null +++ b/ListableUI/Sources/HeaderFooter/AnyHeaderFooter.swift @@ -0,0 +1,31 @@ +// +// AnyHeaderFooter.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public protocol AnyHeaderFooter : AnyHeaderFooter_Internal +{ + var sizing : Sizing { get set } + var layout : HeaderFooterLayout { get set } +} + + +public protocol AnyHeaderFooter_Internal +{ + var layout : HeaderFooterLayout { get } + + func apply( + to headerFooterView : UIView, + for reason : ApplyReason, + with info : ApplyHeaderFooterContentInfo + ) + + func anyIsEquivalent(to other : AnyHeaderFooter) -> Bool + + func newPresentationHeaderFooterState(performsContentCallbacks : Bool) -> Any +} diff --git a/ListableUI/Sources/HeaderFooter/HeaderFooter.swift b/ListableUI/Sources/HeaderFooter/HeaderFooter.swift index 35ee66058..36934c00b 100644 --- a/ListableUI/Sources/HeaderFooter/HeaderFooter.swift +++ b/ListableUI/Sources/HeaderFooter/HeaderFooter.swift @@ -6,31 +6,10 @@ // -public protocol AnyHeaderFooter : AnyHeaderFooter_Internal -{ - var sizing : Sizing { get set } - var layout : HeaderFooterLayout { get set } -} - -public protocol AnyHeaderFooter_Internal -{ - var layout : HeaderFooterLayout { get } - - func apply( - to headerFooterView : UIView, - for reason : ApplyReason, - with info : ApplyHeaderFooterContentInfo - ) - - func anyIsEquivalent(to other : AnyHeaderFooter) -> Bool - - func newPresentationHeaderFooterState(performsContentCallbacks : Bool) -> Any -} - - public typealias Header = HeaderFooter public typealias Footer = HeaderFooter + public struct HeaderFooter : AnyHeaderFooter { public var content : Content @@ -123,15 +102,3 @@ extension HeaderFooter : SignpostLoggable ) } } - - -public struct HeaderFooterLayout : Equatable -{ - public var width : CustomWidth - - public init( - width : CustomWidth = .default - ) { - self.width = width - } -} diff --git a/ListableUI/Sources/HeaderFooter/HeaderFooterLayout.swift b/ListableUI/Sources/HeaderFooter/HeaderFooterLayout.swift new file mode 100644 index 000000000..f544a9d24 --- /dev/null +++ b/ListableUI/Sources/HeaderFooter/HeaderFooterLayout.swift @@ -0,0 +1,20 @@ +// +// HeaderFooterLayout.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public struct HeaderFooterLayout : Equatable +{ + public var width : CustomWidth + + public init( + width : CustomWidth = .default + ) { + self.width = width + } +} diff --git a/ListableUI/Sources/Internal/Presentation State/PresentationState.RefreshControl.swift b/ListableUI/Sources/Internal/Presentation State/PresentationState.RefreshControl.swift new file mode 100644 index 000000000..9cffabbcb --- /dev/null +++ b/ListableUI/Sources/Internal/Presentation State/PresentationState.RefreshControl.swift @@ -0,0 +1,53 @@ +// +// RefreshControl.PresentationState.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +extension PresentationState +{ + internal final class RefreshControlState + { + public var model : RefreshControl + public var view : UIRefreshControl + + public init(_ model : RefreshControl) + { + self.model = model + self.view = UIRefreshControl() + + self.view.addTarget(self, action: #selector(refreshControlChanged), for: .valueChanged) + } + + func update(with control : RefreshControl) + { + self.model = control + + if let title = self.model.title { + switch title { + case .string(let string): self.view.attributedTitle = NSAttributedString(string: string) + case .attributed(let string): self.view.attributedTitle = string + } + } else { + self.view.attributedTitle = nil + } + + self.view.tintColor = self.model.tintColor + + if self.model.isRefreshing { + self.view.beginRefreshing() + } else { + self.view.endRefreshing() + } + } + + @objc func refreshControlChanged() + { + self.model.onRefresh() + } + } +} diff --git a/ListableUI/Sources/Internal/Presentation State/PresentationState.swift b/ListableUI/Sources/Internal/Presentation State/PresentationState.swift index 6b418a890..25325025e 100644 --- a/ListableUI/Sources/Internal/Presentation State/PresentationState.swift +++ b/ListableUI/Sources/Internal/Presentation State/PresentationState.swift @@ -14,7 +14,7 @@ final class PresentationState // MARK: Properties // - var refreshControl : RefreshControl.PresentationState? + var refreshControl : RefreshControlState? var header : HeaderFooterViewStatePair = .init() var footer : HeaderFooterViewStatePair = .init() @@ -273,7 +273,7 @@ final class PresentationState if let existing = self.refreshControl, let new = new { existing.update(with: new) } else if self.refreshControl == nil, let new = new { - let newControl = RefreshControl.PresentationState(new) + let newControl = RefreshControlState(new) view.refreshControl = newControl.view self.refreshControl = newControl } else if self.refreshControl != nil, new == nil { diff --git a/ListableUI/Sources/Item/AnyItem.swift b/ListableUI/Sources/Item/AnyItem.swift new file mode 100644 index 000000000..38d428e6f --- /dev/null +++ b/ListableUI/Sources/Item/AnyItem.swift @@ -0,0 +1,37 @@ +// +// AnyItem.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public protocol AnyItem : AnyItem_Internal +{ + var identifier : AnyIdentifier { get } + + var anyContent : Any { get } + + var sizing : Sizing { get set } + var layout : ItemLayout { get set } + var selectionStyle : ItemSelectionStyle { get set } + var insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? { get set } + var swipeActions : SwipeActionsConfiguration? { get set } + + var reordering : Reordering? { get set } +} + + +public protocol AnyItem_Internal +{ + func anyWasMoved(comparedTo other : AnyItem) -> Bool + func anyIsEquivalent(to other : AnyItem) -> Bool + + func newPresentationItemState( + with dependencies : ItemStateDependencies, + updateCallbacks : UpdateCallbacks, + performsContentCallbacks : Bool + ) -> Any +} diff --git a/ListableUI/Sources/Item/DefaultItemProperties.swift b/ListableUI/Sources/Item/DefaultItemProperties.swift new file mode 100644 index 000000000..03d4b800f --- /dev/null +++ b/ListableUI/Sources/Item/DefaultItemProperties.swift @@ -0,0 +1,44 @@ +// +// DefaultItemProperties.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +/// Allows specifying default properties to apply to an item when it is initialized, +/// if those values are not provided to the initializer. +/// Only non-nil values are used – if you do not want to provide a default value, +/// simply leave the property nil. +/// +/// The order of precedence used when assigning values is: +/// 1) The value passed to the initializer. +/// 2) The value from `defaultItemProperties` on the contained `ItemContent`, if non-nil. +/// 3) A standard, default value. +public struct DefaultItemProperties +{ + public var sizing : Sizing? + public var layout : ItemLayout? + + public var selectionStyle : ItemSelectionStyle? + + public var insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? + + public var swipeActions : SwipeActionsConfiguration? + + public init( + sizing : Sizing? = nil, + layout : ItemLayout? = nil, + selectionStyle : ItemSelectionStyle? = nil, + insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? = nil, + swipeActions : SwipeActionsConfiguration? = nil + ) { + self.sizing = sizing + self.layout = layout + self.selectionStyle = selectionStyle + self.insertAndRemoveAnimations = insertAndRemoveAnimations + self.swipeActions = swipeActions + } +} diff --git a/ListableUI/Sources/Item/Item+Callbacks.swift b/ListableUI/Sources/Item/Item+Callbacks.swift new file mode 100644 index 000000000..cab2af475 --- /dev/null +++ b/ListableUI/Sources/Item/Item+Callbacks.swift @@ -0,0 +1,78 @@ +// +// Item+Callbacks.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public extension Item +{ + /// Value passed to the `onDisplay` callback for `Item`. + struct OnDisplay + { + public typealias Callback = (OnDisplay) -> () + + public var item : Item + + public var isFirstDisplay : Bool + } + + /// Value passed to the `onEndDisplay` callback for `Item`. + struct OnEndDisplay + { + public typealias Callback = (OnEndDisplay) -> () + + public var item : Item + + public var isFirstEndDisplay : Bool + } + + /// Value passed to the `onSelect` callback for `Item`. + struct OnSelect + { + public typealias Callback = (OnSelect) -> () + + public var item : Item + } + + /// Value passed to the `onDeselect` callback for `Item`. + struct OnDeselect + { + public typealias Callback = (OnDeselect) -> () + + public var item : Item + } + + struct OnInsert + { + public typealias Callback = (OnInsert) -> () + + public var item : Item + } + + struct OnRemove + { + public typealias Callback = (OnRemove) -> () + + public var item : Item + } + + struct OnMove + { + public typealias Callback = (OnMove) -> () + + public var old : Item + public var new : Item + } + + struct OnUpdate + { + public typealias Callback = (OnUpdate) -> () + + public var old : Item + public var new : Item + } +} diff --git a/ListableUI/Sources/Item/Item.swift b/ListableUI/Sources/Item/Item.swift index ea4279bd7..99a8f346d 100644 --- a/ListableUI/Sources/Item/Item.swift +++ b/ListableUI/Sources/Item/Item.swift @@ -6,45 +6,6 @@ // -public enum ItemPosition -{ - case single - - case first - case middle - case last -} - - -public protocol AnyItem : AnyItem_Internal -{ - var identifier : AnyIdentifier { get } - - var anyContent : Any { get } - - var sizing : Sizing { get set } - var layout : ItemLayout { get set } - var selectionStyle : ItemSelectionStyle { get set } - var insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? { get set } - var swipeActions : SwipeActionsConfiguration? { get set } - - var reordering : Reordering? { get set } -} - - -public protocol AnyItem_Internal -{ - func anyWasMoved(comparedTo other : AnyItem) -> Bool - func anyIsEquivalent(to other : AnyItem) -> Bool - - func newPresentationItemState( - with dependencies : ItemStateDependencies, - updateCallbacks : UpdateCallbacks, - performsContentCallbacks : Bool - ) -> Any -} - - public struct Item : AnyItem { public var identifier : AnyIdentifier @@ -209,112 +170,6 @@ public struct Item : AnyItem } -public extension Item -{ - /// Value passed to the `onDisplay` callback for `Item`. - struct OnDisplay - { - public typealias Callback = (OnDisplay) -> () - - public var item : Item - - public var isFirstDisplay : Bool - } - - /// Value passed to the `onEndDisplay` callback for `Item`. - struct OnEndDisplay - { - public typealias Callback = (OnEndDisplay) -> () - - public var item : Item - - public var isFirstEndDisplay : Bool - } - - /// Value passed to the `onSelect` callback for `Item`. - struct OnSelect - { - public typealias Callback = (OnSelect) -> () - - public var item : Item - } - - /// Value passed to the `onDeselect` callback for `Item`. - struct OnDeselect - { - public typealias Callback = (OnDeselect) -> () - - public var item : Item - } - - struct OnInsert - { - public typealias Callback = (OnInsert) -> () - - public var item : Item - } - - struct OnRemove - { - public typealias Callback = (OnRemove) -> () - - public var item : Item - } - - struct OnMove - { - public typealias Callback = (OnMove) -> () - - public var old : Item - public var new : Item - } - - struct OnUpdate - { - public typealias Callback = (OnUpdate) -> () - - public var old : Item - public var new : Item - } -} - - -/// Allows specifying default properties to apply to an item when it is initialized, -/// if those values are not provided to the initializer. -/// Only non-nil values are used – if you do not want to provide a default value, -/// simply leave the property nil. -/// -/// The order of precedence used when assigning values is: -/// 1) The value passed to the initializer. -/// 2) The value from `defaultItemProperties` on the contained `ItemContent`, if non-nil. -/// 3) A standard, default value. -public struct DefaultItemProperties -{ - public var sizing : Sizing? - public var layout : ItemLayout? - - public var selectionStyle : ItemSelectionStyle? - - public var insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? - - public var swipeActions : SwipeActionsConfiguration? - - public init( - sizing : Sizing? = nil, - layout : ItemLayout? = nil, - selectionStyle : ItemSelectionStyle? = nil, - insertAndRemoveAnimations : ItemInsertAndRemoveAnimations? = nil, - swipeActions : SwipeActionsConfiguration? = nil - ) { - self.sizing = sizing - self.layout = layout - self.selectionStyle = selectionStyle - self.insertAndRemoveAnimations = insertAndRemoveAnimations - self.swipeActions = swipeActions - } -} - - extension Item : SignpostLoggable { var signpostInfo : SignpostLoggingInfo { @@ -326,114 +181,4 @@ extension Item : SignpostLoggable } -public struct Reordering -{ - public var sections : Sections - - public typealias CanReorder = (Result) -> Bool - public var canReorder : CanReorder? - - public typealias DidReorder = (Result) -> () - public var didReorder : DidReorder - - public init( - sections : Sections = .same, - canReorder : CanReorder? = nil, - didReorder : @escaping DidReorder - ) { - self.sections = sections - self.canReorder = canReorder - self.didReorder = didReorder - } - - public enum Sections : Equatable - { - case same - } - - public struct Result - { - public var fromSection : Section - public var fromIndexPath : IndexPath - - public var toSection : Section - public var toIndexPath : IndexPath - } -} - - -public struct ItemLayout : Equatable -{ - public var itemSpacing : CGFloat? - public var itemToSectionFooterSpacing : CGFloat? - - public var width : CustomWidth - - public init( - itemSpacing : CGFloat? = nil, - itemToSectionFooterSpacing : CGFloat? = nil, - width : CustomWidth = .default - ) { - self.itemSpacing = itemSpacing - self.itemSpacing = itemSpacing - - self.width = width - } -} - - -public struct ItemState : Hashable -{ - public init(isSelected : Bool, isHighlighted : Bool) - { - self.isSelected = isSelected - self.isHighlighted = isHighlighted - } - - public init(cell : UICollectionViewCell) - { - self.isSelected = cell.isSelected - self.isHighlighted = cell.isHighlighted - } - - /// If the item is currently selected. - public var isSelected : Bool - - /// If the item is currently highlighted. - public var isHighlighted : Bool - - /// If the item is either selected or highlighted. - public var isActive : Bool { - self.isSelected || self.isHighlighted - } -} - -/// Controls the selection style and behavior of an item in a list. -public enum ItemSelectionStyle : Equatable -{ - /// The item is not selectable at all. - case notSelectable - - /// The item is temporarily selectable. Once the user lifts their finger, the item is deselected. - case tappable - - /// The item is persistently selectable. Once the user lifts their finger, the item is maintained. - case selectable(isSelected : Bool = false) - - var isSelected : Bool { - switch self { - case .notSelectable: return false - case .tappable: return false - case .selectable(let selected): return selected - } - } - - var isSelectable : Bool { - switch self { - case .notSelectable: return false - case .tappable: return true - case .selectable(_): return true - } - } -} diff --git a/ListableUI/Sources/Item/ItemLayout.swift b/ListableUI/Sources/Item/ItemLayout.swift new file mode 100644 index 000000000..3047795d9 --- /dev/null +++ b/ListableUI/Sources/Item/ItemLayout.swift @@ -0,0 +1,28 @@ +// +// ItemLayout.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public struct ItemLayout : Equatable +{ + public var itemSpacing : CGFloat? + public var itemToSectionFooterSpacing : CGFloat? + + public var width : CustomWidth + + public init( + itemSpacing : CGFloat? = nil, + itemToSectionFooterSpacing : CGFloat? = nil, + width : CustomWidth = .default + ) { + self.itemSpacing = itemSpacing + self.itemSpacing = itemSpacing + + self.width = width + } +} diff --git a/ListableUI/Sources/Item/ItemPosition.swift b/ListableUI/Sources/Item/ItemPosition.swift new file mode 100644 index 000000000..a26b199a6 --- /dev/null +++ b/ListableUI/Sources/Item/ItemPosition.swift @@ -0,0 +1,18 @@ +// +// ItemPosition.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public enum ItemPosition +{ + case single + + case first + case middle + case last +} diff --git a/ListableUI/Sources/Item/ItemSelectionStyle.swift b/ListableUI/Sources/Item/ItemSelectionStyle.swift new file mode 100644 index 000000000..cb1247da3 --- /dev/null +++ b/ListableUI/Sources/Item/ItemSelectionStyle.swift @@ -0,0 +1,38 @@ +// +// ItemSelectionStyle.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +/// Controls the selection style and behavior of an item in a list. +public enum ItemSelectionStyle : Equatable +{ + /// The item is not selectable at all. + case notSelectable + + /// The item is temporarily selectable. Once the user lifts their finger, the item is deselected. + case tappable + + /// The item is persistently selectable. Once the user lifts their finger, the item is maintained. + case selectable(isSelected : Bool = false) + + var isSelected : Bool { + switch self { + case .notSelectable: return false + case .tappable: return false + case .selectable(let selected): return selected + } + } + + var isSelectable : Bool { + switch self { + case .notSelectable: return false + case .tappable: return true + case .selectable(_): return true + } + } +} diff --git a/ListableUI/Sources/Item/ItemState.swift b/ListableUI/Sources/Item/ItemState.swift new file mode 100644 index 000000000..4d936cc45 --- /dev/null +++ b/ListableUI/Sources/Item/ItemState.swift @@ -0,0 +1,35 @@ +// +// ItemState.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public struct ItemState : Hashable +{ + public init(isSelected : Bool, isHighlighted : Bool) + { + self.isSelected = isSelected + self.isHighlighted = isHighlighted + } + + public init(cell : UICollectionViewCell) + { + self.isSelected = cell.isSelected + self.isHighlighted = cell.isHighlighted + } + + /// If the item is currently selected. + public var isSelected : Bool + + /// If the item is currently highlighted. + public var isHighlighted : Bool + + /// If the item is either selected or highlighted. + public var isActive : Bool { + self.isSelected || self.isHighlighted + } +} diff --git a/ListableUI/Sources/Item/Reordering.swift b/ListableUI/Sources/Item/Reordering.swift new file mode 100644 index 000000000..8ba6f36ec --- /dev/null +++ b/ListableUI/Sources/Item/Reordering.swift @@ -0,0 +1,44 @@ +// +// Reordering.swift +// ListableUI +// +// Created by Kyle Van Essen on 12/14/20. +// + +import Foundation + + +public struct Reordering +{ + public var sections : Sections + + public typealias CanReorder = (Result) -> Bool + public var canReorder : CanReorder? + + public typealias DidReorder = (Result) -> () + public var didReorder : DidReorder + + public init( + sections : Sections = .same, + canReorder : CanReorder? = nil, + didReorder : @escaping DidReorder + ) { + self.sections = sections + self.canReorder = canReorder + self.didReorder = didReorder + } + + public enum Sections : Equatable + { + case same + } + + public struct Result + { + public var fromSection : Section + public var fromIndexPath : IndexPath + + public var toSection : Section + public var toIndexPath : IndexPath + } +} diff --git a/ListableUI/Sources/Layout/Grid/GridListLayout.swift b/ListableUI/Sources/Layout/Grid/GridListLayout.swift index 26b0a0e6b..5205708de 100644 --- a/ListableUI/Sources/Layout/Grid/GridListLayout.swift +++ b/ListableUI/Sources/Layout/Grid/GridListLayout.swift @@ -15,6 +15,7 @@ public extension LayoutDescription } } + public struct GridAppearance : ListLayoutAppearance { public var sizing : Sizing @@ -442,6 +443,7 @@ fileprivate extension GridAppearance.Sizing.ItemSize { } } + fileprivate extension Array { mutating func safeDropFirst(_ count : Int) -> [Element] diff --git a/ListableUI/Sources/Layout/List/DefaultListLayout.swift b/ListableUI/Sources/Layout/List/DefaultListLayout.swift index e0b6f642d..9d6a0586d 100644 --- a/ListableUI/Sources/Layout/List/DefaultListLayout.swift +++ b/ListableUI/Sources/Layout/List/DefaultListLayout.swift @@ -130,7 +130,11 @@ public struct ListAppearance : ListLayoutAppearance self.sizing = sizing self.layout = layout } - +} + + +extension ListAppearance +{ /// Sizing options for the list. public struct Sizing : Equatable { diff --git a/ListableUI/Sources/LayoutDirection.swift b/ListableUI/Sources/LayoutDirection.swift index 47971ece4..00fae724c 100644 --- a/ListableUI/Sources/LayoutDirection.swift +++ b/ListableUI/Sources/LayoutDirection.swift @@ -70,11 +70,11 @@ public enum LayoutDirection : Hashable } -public extension LayoutDirection +extension LayoutDirection { /// When writing a layout, use this method to return differing values based on /// the direction. The passed closures will only be evaluated if they are for the current direction. - func `switch`(vertical : () -> Value, horizontal : () -> Value) -> Value { + public func `switch`(vertical : () -> Value, horizontal : () -> Value) -> Value { switch self { case .vertical: return vertical() case .horizontal: return horizontal() @@ -83,7 +83,7 @@ public extension LayoutDirection /// When writing a layout, use this method to return differing values based on /// the direction. The passed autoclosures will only be evaluated if they are for the current direction. - func `switch`(vertical : @autoclosure () -> Value, horizontal : @autoclosure () -> Value) -> Value { + public func `switch`(vertical : @autoclosure () -> Value, horizontal : @autoclosure () -> Value) -> Value { switch self { case .vertical: return vertical() case .horizontal: return horizontal() @@ -92,7 +92,7 @@ public extension LayoutDirection } -public extension LayoutDirection +extension LayoutDirection { // // MARK: Creating & Reading Values @@ -100,7 +100,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **height** of the provided size. /// `.horizontal`: Returns the **width** of the provided size. - func height(for size : CGSize) -> CGFloat + public func height(for size : CGSize) -> CGFloat { switch self { case .vertical: return size.height @@ -110,7 +110,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **width** of the provided size. /// `.horizontal`: Returns the **height** of the provided size. - func width(for size : CGSize) -> CGFloat + public func width(for size : CGSize) -> CGFloat { switch self { case .vertical: return size.width @@ -120,7 +120,7 @@ public extension LayoutDirection /// `.vertical`: Returns a `CGPoint` made with `(x, y)`. /// `.horizontal`: Returns a `CGPoint` made with `(y, x)`. - func point(x : CGFloat, y : CGFloat) -> CGPoint + public func point(x : CGFloat, y : CGFloat) -> CGPoint { switch self { case .vertical: return CGPoint(x: x, y: y) @@ -130,7 +130,7 @@ public extension LayoutDirection /// `.vertical`: Returns the provided size. /// `.horizontal`: Returns a size created by swapping the width and height. - func size(for size : CGSize) -> CGSize + public func size(for size : CGSize) -> CGSize { switch self { case .vertical: return CGSize(width: size.width, height: size.height) @@ -140,7 +140,7 @@ public extension LayoutDirection /// `.vertical`: Returns a `CGSize` made with `(width, height)`. /// `.horizontal`: Returns a `CGSize` made with `(height, width)`. - func size(width : CGFloat, height : CGFloat) -> CGSize + public func size(width : CGFloat, height : CGFloat) -> CGSize { switch self { case .vertical: return CGSize(width: width, height: height) @@ -150,7 +150,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **maxY** of the frame. /// `.horizontal`: Returns the **maxX** of the frame. - func maxY(for frame : CGRect) -> CGFloat + public func maxY(for frame : CGRect) -> CGFloat { switch self { case .vertical: return frame.maxY @@ -160,7 +160,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **maxX** of the frame. /// `.horizontal`: Returns the **maxY** of the frame. - func maxX(for frame : CGRect) -> CGFloat + public func maxX(for frame : CGRect) -> CGFloat { switch self { case .vertical: return frame.maxX @@ -170,7 +170,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **x** of the point. /// `.horizontal`: Returns the **y** of the point. - func x(for point : CGPoint) -> CGFloat + public func x(for point : CGPoint) -> CGFloat { switch self { case .vertical: return point.x @@ -180,7 +180,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **y** of the point. /// `.horizontal`: Returns the **x** of the point. - func y(for point : CGPoint) -> CGFloat + public func y(for point : CGPoint) -> CGFloat { switch self { case .vertical: return point.y @@ -190,7 +190,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **top** of the insets. /// `.horizontal`: Returns the **left** of the insets. - func top(with insets : UIEdgeInsets) -> CGFloat + public func top(with insets : UIEdgeInsets) -> CGFloat { switch self { case .vertical: return insets.top @@ -200,7 +200,7 @@ public extension LayoutDirection /// `.vertical`: Returns the **bottom** of the insets. /// `.horizontal`: Returns the **right** of the insets. - func bottom(with insets : UIEdgeInsets) -> CGFloat + public func bottom(with insets : UIEdgeInsets) -> CGFloat { switch self { case .vertical: return insets.bottom diff --git a/ListableUI/Sources/ListProperties.swift b/ListableUI/Sources/ListProperties.swift index 38c37c2b6..57f442265 100644 --- a/ListableUI/Sources/ListProperties.swift +++ b/ListableUI/Sources/ListProperties.swift @@ -35,7 +35,7 @@ import Foundation /// In these cases, you can apply `ListProperties` to a `ListView` by calling one of the /// available `func configure(with:)` methods. Having a separate method which describes and provides /// all the properties to configure your `ListView` allows for a more singular flow of data through your application, -/// and eases in testibility. +/// and eases in testability. public struct ListProperties { // diff --git a/ListableUI/Sources/ListStateObserver.swift b/ListableUI/Sources/ListStateObserver.swift index a7870ced4..24ce845b8 100644 --- a/ListableUI/Sources/ListStateObserver.swift +++ b/ListableUI/Sources/ListStateObserver.swift @@ -62,12 +62,6 @@ public struct ListStateObserver { self.onDidScroll.append(callback) } - /// Parameters available for `OnDidScroll` callbacks. - public struct DidScroll { - public let actions : ListActions - public let positionInfo : ListScrollPositionInfo - } - private(set) var onDidScroll : [OnDidScroll] = [] // @@ -86,14 +80,6 @@ public struct ListStateObserver { self.onContentUpdated.append(callback) } - /// Parameters available for `OnContentUpdated` callbacks. - public struct ContentUpdated { - public let hadChanges : Bool - - public let actions : ListActions - public let positionInfo : ListScrollPositionInfo - } - private(set) var onContentUpdated : [OnContentUpdated] = [] // @@ -109,15 +95,6 @@ public struct ListStateObserver { self.onVisibilityChanged.append(callback) } - /// Parameters available for `OnVisibilityChanged` callbacks. - public struct VisibilityChanged { - public let actions : ListActions - public let positionInfo : ListScrollPositionInfo - - public let displayed : [AnyItem] - public let endedDisplay : [AnyItem] - } - private(set) var onVisibilityChanged : [OnVisibilityChanged] = [] // @@ -132,15 +109,6 @@ public struct ListStateObserver { self.onFrameChanged.append(callback) } - /// Parameters available for `OnFrameChanged` callbacks. - public struct FrameChanged { - public let actions : ListActions - public let positionInfo : ListScrollPositionInfo - - public let old : CGRect - public let new : CGRect - } - private(set) var onFrameChanged : [OnFrameChanged] = [] // @@ -155,15 +123,6 @@ public struct ListStateObserver { self.onSelectionChanged.append(callback) } - /// Parameters available for `OnSelectionChanged` callbacks. - public struct SelectionChanged { - public let actions : ListActions - public let positionInfo : ListScrollPositionInfo - - public let old : Set - public let new : Set - } - private(set) var onSelectionChanged : [OnSelectionChanged] = [] // @@ -193,3 +152,52 @@ public struct ListStateObserver { actions.listView = nil } } + + +extension ListStateObserver +{ + /// Parameters available for `OnDidScroll` callbacks. + public struct DidScroll { + public let actions : ListActions + public let positionInfo : ListScrollPositionInfo + } + + + /// Parameters available for `OnContentUpdated` callbacks. + public struct ContentUpdated { + public let hadChanges : Bool + + public let actions : ListActions + public let positionInfo : ListScrollPositionInfo + } + + + /// Parameters available for `OnVisibilityChanged` callbacks. + public struct VisibilityChanged { + public let actions : ListActions + public let positionInfo : ListScrollPositionInfo + + public let displayed : [AnyItem] + public let endedDisplay : [AnyItem] + } + + + /// Parameters available for `OnFrameChanged` callbacks. + public struct FrameChanged { + public let actions : ListActions + public let positionInfo : ListScrollPositionInfo + + public let old : CGRect + public let new : CGRect + } + + + /// Parameters available for `OnSelectionChanged` callbacks. + public struct SelectionChanged { + public let actions : ListActions + public let positionInfo : ListScrollPositionInfo + + public let old : Set + public let new : Set + } +} diff --git a/ListableUI/Sources/ListView/ListView.swift b/ListableUI/Sources/ListView/ListView.swift index f1d040d47..ac23f3841 100644 --- a/ListableUI/Sources/ListView/ListView.swift +++ b/ListableUI/Sources/ListView/ListView.swift @@ -498,7 +498,7 @@ public final class ListView : UIView, KeyboardObserverDelegate return } - let contentOffsetY = contentHeight - contentFrameHeight - self.collectionView.lst_adjustedContentInset.top + let contentOffsetY = contentHeight - contentFrameHeight - self.collectionView.adjustedContentInset.top let contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: contentOffsetY) animation.perform( diff --git a/ListableUI/Sources/RefreshControl.swift b/ListableUI/Sources/RefreshControl.swift index ecdf0db88..ccc40a806 100644 --- a/ListableUI/Sources/RefreshControl.swift +++ b/ListableUI/Sources/RefreshControl.swift @@ -12,13 +12,8 @@ public struct RefreshControl { public var isRefreshing : Bool - public enum Title : Equatable - { - case string(String) - case attributed(NSAttributedString) - } - public var title : Title? + public var tintColor : UIColor? public typealias OnRefresh = () -> () @@ -38,46 +33,14 @@ public struct RefreshControl self.onRefresh = onRefresh } - - internal final class PresentationState +} + + +extension RefreshControl +{ + public enum Title : Equatable { - public var model : RefreshControl - public var view : UIRefreshControl - - public init(_ model : RefreshControl) - { - self.model = model - self.view = UIRefreshControl() - - self.view.addTarget(self, action: #selector(refreshControlChanged), for: .valueChanged) - } - - func update(with control : RefreshControl) - { - self.model = control - - if let title = self.model.title { - switch title { - case .string(let string): self.view.attributedTitle = NSAttributedString(string: string) - case .attributed(let string): self.view.attributedTitle = string - } - } else { - self.view.attributedTitle = nil - } - - self.view.tintColor = self.model.tintColor - - if self.model.isRefreshing { - self.view.beginRefreshing() - } else { - self.view.endRefreshing() - } - } - - @objc func refreshControlChanged() - { - self.model.onRefresh() - } + case string(String) + case attributed(NSAttributedString) } - } diff --git a/ListableUI/Sources/ReorderingActions.swift b/ListableUI/Sources/ReorderingActions.swift index 24b1e3d50..929034188 100644 --- a/ListableUI/Sources/ReorderingActions.swift +++ b/ListableUI/Sources/ReorderingActions.swift @@ -6,15 +6,6 @@ // -protocol ReorderingActionsDelegate : AnyObject -{ - func beginInteractiveMovementFor(item : AnyPresentationItemState) -> Bool - func updateInteractiveMovementTargetPosition(with recognizer : UIPanGestureRecognizer) - func endInteractiveMovement() - func cancelInteractiveMovement() -} - - public final class ReorderingActions { public private(set) var isMoving : Bool @@ -62,3 +53,12 @@ public final class ReorderingActions self.delegate?.endInteractiveMovement() } } + + +protocol ReorderingActionsDelegate : AnyObject +{ + func beginInteractiveMovementFor(item : AnyPresentationItemState) -> Bool + func updateInteractiveMovementTargetPosition(with recognizer : UIPanGestureRecognizer) + func endInteractiveMovement() + func cancelInteractiveMovement() +} diff --git a/ListableUI/Sources/ScrollPosition.swift b/ListableUI/Sources/ScrollPosition.swift index 52b8ac0eb..586788ac0 100644 --- a/ListableUI/Sources/ScrollPosition.swift +++ b/ListableUI/Sources/ScrollPosition.swift @@ -32,7 +32,11 @@ public struct ScrollPosition : Equatable self.ifAlreadyVisible = ifAlreadyVisible self.offset = offset } - +} + + +extension ScrollPosition +{ /// How the item should be positioned within the list. public enum Position : Equatable { @@ -54,6 +58,7 @@ public struct ScrollPosition : Equatable } } + /// What action should be taken if an item is already partially visible within a list. public enum IfAlreadyVisible : Equatable { diff --git a/ListableUI/Sources/Section.swift b/ListableUI/Sources/Section.swift index 6d98c8b84..dfb06032f 100644 --- a/ListableUI/Sources/Section.swift +++ b/ListableUI/Sources/Section.swift @@ -182,9 +182,9 @@ public struct Section } -public extension Section +extension Section { - struct Layout : Equatable + public struct Layout : Equatable { public var width : CustomWidth @@ -198,7 +198,8 @@ public extension Section } } - struct Columns + + public struct Columns { public var count : Int public var spacing : CGFloat diff --git a/ListableUI/Sources/Sizing.swift b/ListableUI/Sources/Sizing.swift index dfd946611..eb5617e29 100644 --- a/ListableUI/Sources/Sizing.swift +++ b/ListableUI/Sources/Sizing.swift @@ -129,9 +129,9 @@ public enum Sizing : Hashable } -public extension Sizing +extension Sizing { - struct MeasureInfo + public struct MeasureInfo { var sizeConstraint : CGSize var defaultSize : CGSize @@ -148,7 +148,7 @@ public extension Sizing } } - struct Constraint : Hashable + public struct Constraint : Hashable { public var width : Axis public var height : Axis diff --git a/ListableUI/Sources/SwipeActionsConfiguration.swift b/ListableUI/Sources/SwipeActionsConfiguration.swift index f5b170832..656596ce8 100644 --- a/ListableUI/Sources/SwipeActionsConfiguration.swift +++ b/ListableUI/Sources/SwipeActionsConfiguration.swift @@ -28,6 +28,7 @@ public struct SwipeActionsConfiguration { } } + /// Create SwipeActions to define actions that can be performed in a SwipeActionsConfiguration. public struct SwipeAction { diff --git a/ListableUI/Sources/UIScrollView+Extensions.swift b/ListableUI/Sources/UIScrollView+Extensions.swift index 86c4cff3f..18205161d 100644 --- a/ListableUI/Sources/UIScrollView+Extensions.swift +++ b/ListableUI/Sources/UIScrollView+Extensions.swift @@ -12,15 +12,6 @@ extension UIScrollView { /// The frame of the collection view inset by the adjusted content inset, /// i.e., the visible frame of the content. var contentFrame: CGRect { - return self.bounds.inset(by: self.lst_adjustedContentInset) - } - - /// `adjustedContentInset` on iOS >= 11, `contentInset` otherwise. - var lst_adjustedContentInset: UIEdgeInsets { - if #available(iOS 11, *) { - return self.adjustedContentInset - } else { - return self.contentInset - } + return self.bounds.inset(by: self.adjustedContentInset) } }