diff --git a/BlueprintUILists/Sources/BlueprintHeaderFooterContent.swift b/BlueprintUILists/Sources/BlueprintHeaderFooterContent.swift index b28e258a1..75508bfa7 100644 --- a/BlueprintUILists/Sources/BlueprintHeaderFooterContent.swift +++ b/BlueprintUILists/Sources/BlueprintHeaderFooterContent.swift @@ -117,6 +117,15 @@ public extension BlueprintHeaderFooterContent views.content.element = self.elementRepresentation.wrapInBlueprintEnvironmentFrom(environment: info.environment) views.background.element = self.background?.wrapInBlueprintEnvironmentFrom(environment: info.environment) views.pressed.element = self.pressedBackground?.wrapInBlueprintEnvironmentFrom(environment: info.environment) + + /// `BlueprintView` does not update its content until the next layout cycle. + /// Force that layout cycle within this method if we're updating an already on-screen + /// `ItemContent`, to ensure that we inherit any animation blocks we may be within. + if reason == .wasUpdated { + views.content.layoutIfNeeded() + views.background.layoutIfNeeded() + views.pressed.layoutIfNeeded() + } } static func createReusableContentView(frame: CGRect) -> ContentView { diff --git a/BlueprintUILists/Sources/BlueprintItemContent.swift b/BlueprintUILists/Sources/BlueprintItemContent.swift index e097fdb56..a813b8a8b 100644 --- a/BlueprintUILists/Sources/BlueprintItemContent.swift +++ b/BlueprintUILists/Sources/BlueprintItemContent.swift @@ -128,6 +128,15 @@ public extension BlueprintItemContent views.content.element = self.element(with: info).wrapInBlueprintEnvironmentFrom(environment: info.environment) views.background.element = self.backgroundElement(with: info)?.wrapInBlueprintEnvironmentFrom(environment: info.environment) views.selectedBackground.element = self.selectedBackgroundElement(with: info)?.wrapInBlueprintEnvironmentFrom(environment: info.environment) + + /// `BlueprintView` does not update its content until the next layout cycle. + /// Force that layout cycle within this method if we're updating an already on-screen + /// `ItemContent`, to ensure that we inherit any animation blocks we may be within. + if reason == .wasUpdated { + views.content.layoutIfNeeded() + views.background.layoutIfNeeded() + views.selectedBackground.layoutIfNeeded() + } } /// Creates the `BlueprintView` used to render the content of the item. diff --git a/CHANGELOG.md b/CHANGELOG.md index e9eb4cd81..da4ffbe8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Changed +- [Updates to `ItemContentCoordinator`](https://github.com/kyleve/Listable/pull/274) to properly support animations in Blueprint-backed rows. This change also generalizes the contained animation type to `ViewAnimation`, for use in both scrolling and content updates. + ### Misc # Past Releases diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index cf5d199b4..258f1ede4 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 0AA4D9C8248064A300CF95A5 /* ReorderingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */; }; 0AA4D9C9248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */; }; 0AC2A1962489F93E00779459 /* PagedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC2A1952489F93E00779459 /* PagedViewController.swift */; }; + 0AC839A525EEAD110055CEF5 /* OnTapItemAnimationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */; }; 0ACF96D624A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */; }; 0AD6767A25423BE500A49315 /* MultiSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD6767925423BE500A49315 /* MultiSelectViewController.swift */; }; 0ADC3B3524907C80008DF2C0 /* XcodePreviewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADC3B3424907C80008DF2C0 /* XcodePreviewDemo.swift */; }; @@ -74,6 +75,7 @@ 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReorderingViewController.swift; sourceTree = ""; }; 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewBasicDemoViewController.swift; sourceTree = ""; }; 0AC2A1952489F93E00779459 /* PagedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedViewController.swift; sourceTree = ""; }; + 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnTapItemAnimationViewController.swift; sourceTree = ""; }; 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInsertAndRemoveAnimationsViewController.swift; sourceTree = ""; }; 0AD6767925423BE500A49315 /* MultiSelectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectViewController.swift; sourceTree = ""; }; 0ADC3B3424907C80008DF2C0 /* XcodePreviewDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodePreviewDemo.swift; sourceTree = ""; }; @@ -153,6 +155,7 @@ 2B8804642490844A003BB351 /* SpacingCustomizationViewController.swift */, 0AA4D9B5248064A300CF95A5 /* SwipeActionsViewController.swift */, 0AA4D9AE248064A300CF95A5 /* WidthCustomizationViewController.swift */, + 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */, ); path = "Demo Screens"; sourceTree = ""; @@ -443,6 +446,7 @@ 0A66420B254A317A007F6B2F /* AutoLayoutDemoViewController.swift in Sources */, 0AEB96E222FBCC1D00341DFF /* AppDelegate.swift in Sources */, 0A793B5824E4B53500850139 /* ManualSelectionManagementViewController.swift in Sources */, + 0AC839A525EEAD110055CEF5 /* OnTapItemAnimationViewController.swift in Sources */, 0AA4D9C9248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift in Sources */, 0AA4D9BC248064A300CF95A5 /* KeyboardTestingViewController.swift in Sources */, 0AA4D9C1248064A300CF95A5 /* CoordinatorViewController.swift in Sources */, diff --git a/Demo/Sources/Demos/Demo Screens/CoordinatorViewController.swift b/Demo/Sources/Demos/Demo Screens/CoordinatorViewController.swift index 937734d56..b1adc251b 100644 --- a/Demo/Sources/Demos/Demo Screens/CoordinatorViewController.swift +++ b/Demo/Sources/Demos/Demo Screens/CoordinatorViewController.swift @@ -116,9 +116,7 @@ fileprivate struct PodcastElement : BlueprintItemContent, Equatable let actions: CoordinatorActions let info: CoordinatorInfo - - var view : View? - + init(actions: CoordinatorActions, info: CoordinatorInfo) { self.actions = actions @@ -126,7 +124,7 @@ fileprivate struct PodcastElement : BlueprintItemContent, Equatable } func wasSelected() { - self.actions.update(animated: true) { + self.actions.update(animation: .default) { $0.content.showBottomBar = true } @@ -157,7 +155,7 @@ fileprivate struct PodcastElement : BlueprintItemContent, Equatable } func wasDeselected() { - self.actions.update(animated: true) { + self.actions.update(animation: .default) { $0.content.showBottomBar = false } } diff --git a/Demo/Sources/Demos/Demo Screens/OnTapItemAnimationViewController.swift b/Demo/Sources/Demos/Demo Screens/OnTapItemAnimationViewController.swift new file mode 100644 index 000000000..a8e6a06c2 --- /dev/null +++ b/Demo/Sources/Demos/Demo Screens/OnTapItemAnimationViewController.swift @@ -0,0 +1,112 @@ +// +// OnTapItemAnimationViewController.swift +// Demo +// +// Created by Kyle Van Essen on 3/2/21. +// Copyright © 2021 Kyle Van Essen. All rights reserved. +// + +import ListableUI +import BlueprintUI +import BlueprintUICommonControls +import BlueprintUILists + +final class OnTapItemAnimationViewController : ListViewController { + + override func configure(list: inout ListProperties) { + + list.layout = .demoLayout + + list("items") { section in + + section += items.map { item in + Item(item) { item in + item.selectionStyle = .tappable + } + } + + } + } +} + +fileprivate struct ItemRow : BlueprintItemContent, Equatable { + + var name : String + var price : String + var onTapText : String = "Added" + + var isShowingPrice : Bool = true + + var identifier: Identifier { + .init(name) + } + + func element(with info: ApplyItemContentInfo) -> Element { + + Row { row in + row.verticalAlignment = .center + + row.addFlexible(child: Label(text: self.name) { label in + label.font = .systemFont(ofSize: 18.0, weight: .semibold) + }) + + row.addFlexible(child: Overlay { overlay in + overlay.add( + Label(text: self.price) { label in + label.font = .systemFont(ofSize: 16.0, weight: .semibold) + } + .opacity(isShowingPrice ? 1 : 0) + ) + + overlay.add( + Label(text: self.onTapText) { label in + label.font = .systemFont(ofSize: 16.0, weight: .semibold) + } + .opacity(isShowingPrice ? 0 : 1) + ) + }) + } + .inset(uniform: 10.0) + .box(background: .white(0.95), corners: .rounded(radius: 10)) + } + + func makeCoordinator( + actions: CoordinatorActions, + info: CoordinatorInfo + ) -> OnTapCoordinator + { + OnTapCoordinator(actions: actions, info: info) + } +} + +fileprivate final class OnTapCoordinator : ItemContentCoordinator { + var actions: ItemRow.CoordinatorActions + var info: ItemRow.CoordinatorInfo + + init(actions: ItemRow.CoordinatorActions, info: ItemRow.CoordinatorInfo) { + self.actions = actions + self.info = info + } + + typealias ItemContentType = ItemRow + + func wasSelected() { + self.actions.update { item in + item.content.isShowingPrice = false + } + + self.actions.update(after: 1.0) { item in + item.content.isShowingPrice = true + } + } +} + + +fileprivate let items : [ItemRow] = [ + .init(name: "Coffee", price: "$4.00"), + .init(name: "Cold Brew", price: "$5.00"), + .init(name: "Espresso", price: "$6.00"), + .init(name: "Flat White", price: "$5.00"), + .init(name: "Iced Coffee", price: "$5.00"), + .init(name: "Latte", price: "$6.00") +] diff --git a/Demo/Sources/Demos/DemosRootViewController.swift b/Demo/Sources/Demos/DemosRootViewController.swift index fb5101160..f618077f5 100644 --- a/Demo/Sources/Demos/DemosRootViewController.swift +++ b/Demo/Sources/Demos/DemosRootViewController.swift @@ -131,13 +131,6 @@ public final class DemosRootViewController : ListViewController self.push(LocalizedCollationViewController()) }) - section += Item( - DemoItem(text: "Item Content Coordinator"), - selectionStyle: .selectable(), - onSelect : { _ in - self.push(CoordinatorViewController()) - }) - section += Item( DemoItem(text: "Item Insert & Remove Animations"), selectionStyle: .selectable(), @@ -168,6 +161,28 @@ public final class DemosRootViewController : ListViewController }) } + list("coordinator") { section in + + section.header = HeaderFooter( + DemoHeader(title: "Item Coordinator") + ) + + section += Item( + DemoItem(text: "Expand / Collapse Items"), + selectionStyle: .selectable(), + onSelect : { _ in + self.push(CoordinatorViewController()) + }) + + section += Item( + DemoItem(text: "Animating On Tap"), + selectionStyle: .selectable(), + onSelect : { _ in + self.push(OnTapItemAnimationViewController()) + }) + + } + list("layouts") { section in section.header = HeaderFooter( diff --git a/ListableUI/Sources/AutoScrollAction.swift b/ListableUI/Sources/AutoScrollAction.swift index 2cdb2e192..10f520199 100644 --- a/ListableUI/Sources/AutoScrollAction.swift +++ b/ListableUI/Sources/AutoScrollAction.swift @@ -56,7 +56,7 @@ public enum AutoScrollAction { _ destination : ScrollDestination? = nil, onInsertOf insertedIdentifier: AnyIdentifier, position: ScrollPosition, - animation: ScrollAnimation = .none, + animation: ViewAnimation = .none, shouldPerform : @escaping (ListScrollPositionInfo) -> Bool = { _ in true }, didPerform : @escaping (ListScrollPositionInfo) -> () = { _ in } ) -> AutoScrollAction @@ -115,7 +115,7 @@ extension AutoScrollAction /// ---- /// The action will only be animated if it is animated, **and** the list update itself is /// animated. Otherwise, no animation occurs. - public var animation : ScrollAnimation + public var animation : ViewAnimation /// An additional check you may provide to approve or reject the scroll action. public var shouldPerform : (ListScrollPositionInfo) -> Bool diff --git a/ListableUI/Sources/Internal/Presentation State/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/Presentation State/PresentationState.ItemState.swift index d81b13c9d..769c9af66 100644 --- a/ListableUI/Sources/Internal/Presentation State/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/Presentation State/PresentationState.ItemState.swift @@ -60,7 +60,7 @@ protocol AnyPresentationItemState : AnyObject protocol ItemContentCoordinatorDelegate : AnyObject { - func coordinatorUpdated(for item : AnyItem, animated : Bool) + func coordinatorUpdated(for item : AnyItem) } @@ -155,16 +155,17 @@ extension PresentationState weak var coordinatorDelegate = dependencies.coordinatorDelegate - self.coordination.actions.updateCallback = { [weak self, weak coordinatorDelegate] new, animated in + self.coordination.actions.updateCallback = { [weak self, weak coordinatorDelegate] new, animation in guard let self = self, let delegate = coordinatorDelegate else { return } self.setNew(item: new, reason: .updateFromItemCoordinator, updateCallbacks: UpdateCallbacks(.immediate)) - delegate.coordinatorUpdated(for: self.anyModel, animated: animated) - - self.applyToVisibleCell(with: dependencies.environmentProvider()) + animation.perform { + self.applyToVisibleCell(with: dependencies.environmentProvider()) + delegate.coordinatorUpdated(for: self.anyModel) + } } self.storage.didSetState = { [weak self] old, new in @@ -398,15 +399,10 @@ extension PresentationState } if old.visibleCell != new.visibleCell { - if let cell = new.visibleCell { - let contentView = cell.contentContainer.contentView - - coordinator.view = contentView - coordinator.willDisplay(with: contentView) + if new.visibleCell != nil { + coordinator.willDisplay() } else { - if let view = old.visibleCell?.contentContainer.contentView { - coordinator.didEndDisplay(with: view) - } + coordinator.didEndDisplay() } } } diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 499fa3eff..e31ebd589 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -279,7 +279,7 @@ public extension ItemContent where Coordinator == DefaultItemContentCoordinator< { func makeCoordinator(actions : ItemContentCoordinatorActions, info : ItemContentCoordinatorInfo) -> Coordinator { - DefaultItemContentCoordinator(actions: actions, info: info, view: nil) + DefaultItemContentCoordinator(actions: actions, info: info) } } diff --git a/ListableUI/Sources/Item/ItemContentCoordinator.swift b/ListableUI/Sources/Item/ItemContentCoordinator.swift index 9c14660f9..d6c1e97ae 100644 --- a/ListableUI/Sources/Item/ItemContentCoordinator.swift +++ b/ListableUI/Sources/Item/ItemContentCoordinator.swift @@ -57,6 +57,9 @@ public protocol ItemContentCoordinator : AnyObject /// The type of `ItemContent` associated with this coordinator. associatedtype ItemContentType : ItemContent + /// The item associated with the coordinator. + typealias Item = ListableUI.Item + // MARK: Actions & Info /// The available actions you can perform on the coordinated `Item`. Eg, updating it to a new value. @@ -68,38 +71,32 @@ public protocol ItemContentCoordinator : AnyObject // MARK: Instance Lifecycle /// Invoked on the coordinator when it is first created and configured. - func wasInserted(_ info : Item.OnInsert) + func wasInserted(_ info : Item.OnInsert) /// Invoked on the coordinator when its owned item is removed from the list due to /// the item, or its entire section, being removed from the list. /// /// Not invoked during deallocation of a list. - func wasRemoved(_ info : Item.OnRemove) + func wasRemoved(_ info : Item.OnRemove) /// Invoked on the coordinator when its owned item is moved inside a list due to its /// order changing. /// /// Not invoked when an item is manually re-ordered by a user. - func wasMoved(_ info : Item.OnMove) + func wasMoved(_ info : Item.OnMove) /// Invoked on the coordinator when an external update is pushed onto the owned `Item`. /// This happens when the developer updates the content of the list, and the item is /// reported as changed via its `isEquivalent(to:)` method. - func wasUpdated(_ info : Item.OnUpdate) + func wasUpdated(_ info : Item.OnUpdate) // MARK: Visibility & View Lifecycle - - /// The view type associated with the item. - typealias View = ItemContentType.ContentView - - /// The view, if any, currently used to display the item. - var view : View? { get set } /// Invoked when the list is about to begin displaying the item with the given view. - func willDisplay(with view : View) + func willDisplay() /// Invoked when the list is about to complete displaying the item with the given view. - func didEndDisplay(with view : View) + func didEndDisplay() // MARK: Selection & Highlight Lifecycle @@ -115,19 +112,19 @@ public extension ItemContentCoordinator { // MARK: Instance Lifecycle - func wasInserted(_ info : Item.OnInsert) {} + func wasInserted(_ info : Item.OnInsert) {} - func wasRemoved(_ info : Item.OnRemove) {} + func wasRemoved(_ info : Item.OnRemove) {} - func wasMoved(_ info : Item.OnMove) {} + func wasMoved(_ info : Item.OnMove) {} - func wasUpdated(_ info : Item.OnUpdate) {} + func wasUpdated(_ info : Item.OnUpdate) {} // MARK: Visibility Lifecycle - func willDisplay(with view : View) {} + func willDisplay() {} - func didEndDisplay(with view : View) {} + func didEndDisplay() {} // MARK: Selection & Highlight Lifecycle @@ -141,28 +138,41 @@ public extension ItemContentCoordinator public final class ItemContentCoordinatorActions { private let currentProvider : () -> Item - var updateCallback : (Item, Bool) -> () + var updateCallback : (Item, ViewAnimation) -> () - init(current : @escaping () -> Item, update : @escaping (Item, Bool) -> ()) + init(current : @escaping () -> Item, update : @escaping (Item, ViewAnimation) -> ()) { self.currentProvider = current self.updateCallback = update } - - /// Updates the item to the provided item. - public func update(animated: Bool = false, _ new : Item) - { - self.updateCallback(new, animated) - } - - /// Allows you to update the item passed into the update closure. - public func update(animated: Bool = false, _ update : (inout Item) -> ()) - { + + /// + /// Allows you to update the displayed item via the provided closure, with an optional + /// animation or delay. + /// + /// ``` + /// func wasSelected() { + /// self.update(animation: .animated(0.15), after: 1.0) { item in + /// item.content.myProperty = true + /// } + /// } + /// ``` + public func update( + animation: ViewAnimation = .default, + after delay: TimeInterval = 0, + update : (inout Item) -> () + ) { var new = self.currentProvider() update(&new) - self.update(animated: animated, new) + if delay > 0 { + Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { _ in + self.updateCallback(new, animation) + } + } else { + self.updateCallback(new, animation) + } } } @@ -198,15 +208,11 @@ public final class DefaultItemContentCoordinator : ItemCont public let actions : Content.CoordinatorActions public let info : Content.CoordinatorInfo - public var view : Content.ContentView? - internal init( actions: Content.CoordinatorActions, - info: Content.CoordinatorInfo, - view: DefaultItemContentCoordinator.View? + info: Content.CoordinatorInfo ) { self.actions = actions self.info = info - self.view = view } } diff --git a/ListableUI/Sources/ListActions.swift b/ListableUI/Sources/ListActions.swift index 938df2e56..545fd1286 100644 --- a/ListableUI/Sources/ListActions.swift +++ b/ListableUI/Sources/ListActions.swift @@ -93,7 +93,7 @@ public final class ListActions { public func scrollTo( item : AnyItem, position : ScrollPosition, - animation : ScrollAnimation = .none, + animation : ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -118,7 +118,7 @@ public final class ListActions { public func scrollTo( item : AnyIdentifier, position : ScrollPosition, - animation : ScrollAnimation = .none, + animation : ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -137,7 +137,7 @@ public final class ListActions { /// Scrolls to the very top of the list, which includes displaying the list header. @discardableResult public func scrollToTop( - animation : ScrollAnimation = .none, + animation : ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -154,7 +154,7 @@ public final class ListActions { /// Scrolls to the last item in the list. If the list contains no items, no action is performed. @discardableResult public func scrollToLastItem( - animation : ScrollAnimation = .none, + animation : ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { diff --git a/ListableUI/Sources/ListView/ListView.swift b/ListableUI/Sources/ListView/ListView.swift index 1d1a505dc..0eb62f547 100644 --- a/ListableUI/Sources/ListView/ListView.swift +++ b/ListableUI/Sources/ListView/ListView.swift @@ -390,7 +390,7 @@ public final class ListView : UIView, KeyboardObserverDelegate @discardableResult public func scrollTo( item : AnyItem, position : ScrollPosition, - animation: ScrollAnimation = .none, + animation: ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -411,7 +411,7 @@ public final class ListView : UIView, KeyboardObserverDelegate public func scrollTo( item : AnyIdentifier, position : ScrollPosition, - animation: ScrollAnimation = .none, + animation: ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -451,7 +451,7 @@ public final class ListView : UIView, KeyboardObserverDelegate /// Scrolls to the very top of the list, which includes displaying the list header. @discardableResult public func scrollToTop( - animation: ScrollAnimation = .none, + animation: ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -471,7 +471,7 @@ public final class ListView : UIView, KeyboardObserverDelegate /// Scrolls to the last item in the list. If the list contains no items, no action is performed. @discardableResult public func scrollToLastItem( - animation: ScrollAnimation = .none, + animation: ViewAnimation = .none, completion : @escaping ScrollCompletion = { _ in } ) -> Bool { @@ -1042,16 +1042,10 @@ public extension ListView extension ListView : ItemContentCoordinatorDelegate { - func coordinatorUpdated(for : AnyItem, animated : Bool) + func coordinatorUpdated(for : AnyItem) { - if animated { - UIView.animate(withDuration: 0.25) { - self.collectionViewLayout.setNeedsRelayout() - self.collectionView.layoutIfNeeded() - } - } else { - self.collectionViewLayout.setNeedsRelayout() - } + self.collectionViewLayout.setNeedsRelayout() + self.collectionView.layoutIfNeeded() } } diff --git a/ListableUI/Sources/ScrollAnimation.swift b/ListableUI/Sources/ViewAnimation.swift similarity index 66% rename from ListableUI/Sources/ScrollAnimation.swift rename to ListableUI/Sources/ViewAnimation.swift index f54b73aa3..94c3a76fa 100644 --- a/ListableUI/Sources/ScrollAnimation.swift +++ b/ListableUI/Sources/ViewAnimation.swift @@ -1,5 +1,5 @@ // -// ScrollAnimation.swift +// ViewAnimation.swift // ListableUI // // Created by Kyle Van Essen on 10/29/20. @@ -8,23 +8,26 @@ import Foundation -/// Specifies the kind of animation to use when scrolling a list view. -public enum ScrollAnimation { +/// Specifies the kind of animation to use when updating various parts of a list, +/// such as updating an item or scrolling to a given position. +public enum ViewAnimation { /// No animation is performed. case none - /// A default animation is performed. This is the same as `.custom()`. - case `default` + /// A default animation is performed. This is the same as `.animated()`. + public static var `default` : Self = .animated() - /// A custom animation is performed. + /// A `UIView.animate(...)` animation is performed. /// The default parameters are 0.25 seconds and `.curveEaseInOut` animation curve. - case custom(duration : TimeInterval = 0.25, options : Set = .default) + case animated(TimeInterval = 0.25, options : Set = .default) - case spring(duration : TimeInterval = 0.25, timing : UISpringTimingParameters = .init()) + /// A spring based animation is performed. + /// The default parameters are 0.25 seconds and `UISpringTimingParameters()` timing. + case spring(TimeInterval = 0.25, timing : UISpringTimingParameters = .init()) /// Ands the animation with the provided bool, returning the animation if true, and `.none` if false. - public func and(with animated : Bool) -> ScrollAnimation { + public func and(with animated : Bool) -> ViewAnimation { if animated { return self } else { @@ -32,21 +35,17 @@ public enum ScrollAnimation { } } - /// Performs the provided animations for the `ScrollAnimation`. - public func perform(animations : @escaping () -> (), completion : @escaping (Bool) -> () = { _ in }) - { + /// Performs the provided animations for the `ViewAnimation`. + public func perform( + animations : @escaping () -> (), + completion : @escaping (Bool) -> () = { _ in } + ) { switch self { case .none: animations() completion(true) - - case .default: - Self.custom().perform( - animations: animations, - completion: completion - ) - - case .custom(let duration, let options): + + case .animated(let duration, let options): UIView.animate( withDuration: duration, delay: 0.0, @@ -74,9 +73,9 @@ public enum ScrollAnimation { } -extension ScrollAnimation { +extension ViewAnimation { - /// The animations options available for the `ScrollAnimation`. + /// The animations options available for the `ViewAnimation`. public enum AnimationOptions : Hashable { case curveEaseInOut case curveEaseIn @@ -86,7 +85,7 @@ extension ScrollAnimation { } -extension Set where Element == ScrollAnimation.AnimationOptions { +extension Set where Element == ViewAnimation.AnimationOptions { public static var `default` : Self {[ .curveEaseInOut diff --git a/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift b/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift index c19433d6d..ea269f30d 100644 --- a/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift +++ b/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift @@ -388,7 +388,7 @@ fileprivate struct TestContent : ItemContent, Equatable self.wasInserted_calls.append(()) } - var wasUpdated_calls = [Item.OnUpdate]() + var wasUpdated_calls = [Item.OnUpdate]() func wasUpdated(_ info : Item.OnUpdate) { @@ -402,30 +402,18 @@ fileprivate struct TestContent : ItemContent, Equatable self.wasRemoved_calls.append(()) } - // MARK: ItemElementCoordinator - Visibility & View Lifecycle - - typealias View = ItemContentType.ContentView - - var view_didSet_calls = [View?]() - - var view : View? { - didSet { - self.view_didSet_calls.append(self.view) - } - } - - var willDisplay_calls = [View]() + var willDisplay_calls = [Void]() - func willDisplay(with view : View) + func willDisplay() { - self.willDisplay_calls.append(view) + self.willDisplay_calls.append(()) } - var didEndDisplay_calls = [View]() + var didEndDisplay_calls = [Void]() - func didEndDisplay(with view : View) + func didEndDisplay() { - self.didEndDisplay_calls.append(view) + self.didEndDisplay_calls.append(()) } // MARK: ItemElementCoordinator - Selection & Highlight Lifecycle @@ -452,7 +440,7 @@ fileprivate class ItemContentCoordinatorDelegateMock : ItemContentCoordinatorDel { var coordinatorUpdated_calls = [AnyItem]() - func coordinatorUpdated(for item: AnyItem, animated : Bool) + func coordinatorUpdated(for item: AnyItem) { self.coordinatorUpdated_calls.append(item) } diff --git a/ListableUI/Tests/Item/ItemContentCoordinatorTests.swift b/ListableUI/Tests/Item/ItemContentCoordinatorTests.swift index 4d1d30f89..7bfa1ddf6 100644 --- a/ListableUI/Tests/Item/ItemContentCoordinatorTests.swift +++ b/ListableUI/Tests/Item/ItemContentCoordinatorTests.swift @@ -23,16 +23,6 @@ class ItemContentCoordinatorActionsTests : XCTestCase callbackCount += 1 }) - self.testcase("Setter based update") { - - var updated = item - updated.content.value = "update1" - actions.update(updated) - - XCTAssertEqual(item.content.value, "update1") - XCTAssertEqual(callbackCount, 1) - } - self.testcase("Closure based update") { actions.update { diff --git a/Podfile.lock b/Podfile.lock index 0f371797d..4a8db3e3a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - - BlueprintUI (0.20.0) - - BlueprintUICommonControls (0.20.0): + - BlueprintUI (0.21.0) + - BlueprintUICommonControls (0.21.0): - BlueprintUI - BlueprintUILists (0.16.0): - BlueprintUI @@ -43,8 +43,8 @@ EXTERNAL SOURCES: :path: Internal Pods/Snapshot/Snapshot.podspec SPEC CHECKSUMS: - BlueprintUI: d26766f3e006d1f9348cba6a7f15efc64da74cb3 - BlueprintUICommonControls: b7b6a10581203f4bd6283c9d2a9b810d513d804b + BlueprintUI: cfa638ec8c3ce1c9f3b3c8740fb56c22401562e5 + BlueprintUICommonControls: 647175c08fbf31ddd849723062715d3c64d1770d BlueprintUILists: 792f40199bbf76318070ff9cc6b57e04ff7e22ea EnglishDictionary: f03968b9382ddc5c8dd63535efbf783c6cd45f1c ListableUI: 827ec9caab46cc1b95817e98d44c8637d2a93f54