diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce2af362..901d0e824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Fixed +- [Adjust calculated keyboard inset in both `setFrame` and `layoutSubviews`](https://github.com/kyleve/Listable/pull/200). This resolves issues that can occur if the list frame changes while the keyboard is visible. + ### Added - [Add support for `onInsert` , `onRemove`, `onMove`, `onUpdate`, on `Item`](https://github.com/kyleve/Listable/pull/196) to track when when items are added, removed, moved, or updated. Changed `onContentChanged` to `onContentUpdated` on `ListStateObserver`; it is always called during updates; you can check the `hadChanges` property. diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index a3ebdea27..2f57acdee 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0A07119324BA798400CDF65D /* ListStateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A07119224BA798400CDF65D /* ListStateViewController.swift */; }; 0A0E070423870A5700DDD27D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0A0E070323870A5700DDD27D /* README.md */; }; 0A49210424E5E11300D17038 /* AccordionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A49210324E5E11300D17038 /* AccordionViewController.swift */; }; + 0A5DC1A924F6FD4200DC7C14 /* ListAppearsAfterKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5DC1A824F6FD4200DC7C14 /* ListAppearsAfterKeyboardViewController.swift */; }; 0A793B5824E4B53500850139 /* ManualSelectionManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A793B5724E4B53500850139 /* ManualSelectionManagementViewController.swift */; }; 0A87BA652463567B0047C3B5 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 0A87BA642463567B0047C3B5 /* CHANGELOG.md */; }; 0AA4D9B9248064A300CF95A5 /* CustomLayoutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9A8248064A200CF95A5 /* CustomLayoutsViewController.swift */; }; @@ -47,6 +48,7 @@ 0A07119224BA798400CDF65D /* ListStateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStateViewController.swift; sourceTree = ""; }; 0A0E070323870A5700DDD27D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 0A49210324E5E11300D17038 /* AccordionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccordionViewController.swift; sourceTree = ""; }; + 0A5DC1A824F6FD4200DC7C14 /* ListAppearsAfterKeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAppearsAfterKeyboardViewController.swift; sourceTree = ""; }; 0A793B5724E4B53500850139 /* ManualSelectionManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualSelectionManagementViewController.swift; sourceTree = ""; }; 0A87BA642463567B0047C3B5 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; 0AA4D9A8248064A200CF95A5 /* CustomLayoutsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomLayoutsViewController.swift; sourceTree = ""; }; @@ -133,6 +135,7 @@ 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */, 0AA4D9B3248064A300CF95A5 /* ItemizationEditorViewController.swift */, 0AA4D9AB248064A300CF95A5 /* KeyboardTestingViewController.swift */, + 0A5DC1A824F6FD4200DC7C14 /* ListAppearsAfterKeyboardViewController.swift */, 0A07119224BA798400CDF65D /* ListStateViewController.swift */, 0AC2A1952489F93E00779459 /* PagedViewController.swift */, 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */, @@ -425,6 +428,7 @@ 0AA4D9C6248064A300CF95A5 /* SwipeActionsViewController.swift in Sources */, 0AA4D9BF248064A300CF95A5 /* WidthCustomizationViewController.swift in Sources */, 0AA4D9C8248064A300CF95A5 /* ReorderingViewController.swift in Sources */, + 0A5DC1A924F6FD4200DC7C14 /* ListAppearsAfterKeyboardViewController.swift in Sources */, 0A07119324BA798400CDF65D /* ListStateViewController.swift in Sources */, 0AA4D9C0248064A300CF95A5 /* InvoicesPaymentScheduleDemoViewController.swift in Sources */, 0AEB96E222FBCC1D00341DFF /* AppDelegate.swift in Sources */, diff --git a/Demo/Sources/Demos/Demo Screens/ListAppearsAfterKeyboardViewController.swift b/Demo/Sources/Demos/Demo Screens/ListAppearsAfterKeyboardViewController.swift new file mode 100644 index 000000000..092adb6e6 --- /dev/null +++ b/Demo/Sources/Demos/Demo Screens/ListAppearsAfterKeyboardViewController.swift @@ -0,0 +1,71 @@ +// +// ListAppearsAfterKeyboardViewController.swift +// Demo +// +// Created by Kyle Van Essen on 8/26/20. +// Copyright © 2020 Kyle Van Essen. All rights reserved. +// + +import UIKit +import BlueprintLists +import BlueprintUICommonControls + + +final class ListAppearsAfterKeyboardViewController : UIViewController { + + let blueprintView = BlueprintView() + + override func loadView() { + self.view = self.blueprintView + + self.blueprintView.element = self.element + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.rightBarButtonItems = [ + UIBarButtonItem(title: "Toggle List", style: .plain, target: self, action: #selector(toggleList)), + UIBarButtonItem(title: "Dismiss", style: .plain, target: self, action: #selector(dismissKeyboard)) + ] + } + + var showingList : Bool = false + + var element : Element { + EnvironmentReader { env in + Column { column in + column.horizontalAlignment = .fill + column.verticalUnderflow = .growProportionally + + column.add(growPriority: 0, shrinkPriority: 0, child: TextField(text: "") { + $0.placeholder = "Tap Into This Field To Show The Keyboard" + $0.textAlignment = .center + }) + + if self.showingList { + column.add(child: List { list in + + list.behavior.keyboardDismissMode = .none + + list("section") { section in + section += (1...20).map { index in + DemoItem(text: "Item \(index)") + } + } + }) + } + }.inset(by: env.safeAreaInsets) + } + } + + @objc func toggleList() { + self.showingList.toggle() + + self.blueprintView.element = self.element + } + + @objc func dismissKeyboard() { + self.view.endEditing(true) + } +} diff --git a/Demo/Sources/Demos/DemosRootViewController.swift b/Demo/Sources/Demos/DemosRootViewController.swift index 7923ebd39..9907d260b 100644 --- a/Demo/Sources/Demos/DemosRootViewController.swift +++ b/Demo/Sources/Demos/DemosRootViewController.swift @@ -83,12 +83,19 @@ public final class DemosRootViewController : ListViewController }) section += Item( - DemoItem(text: "Keyboard Testing"), + DemoItem(text: "Keyboard Inset (Full Screen List)"), selectionStyle: .selectable(), onSelect : { _ in self.push(KeyboardTestingViewController()) }) + section += Item( + DemoItem(text: "Keyboard Inset (Appears Later)"), + selectionStyle: .selectable(), + onSelect : { _ in + self.push(ListAppearsAfterKeyboardViewController()) + }) + section += Item( DemoItem(text: "Reordering (Experimental)"), selectionStyle: .selectable(), diff --git a/Listable/Sources/ListView/ListView.swift b/Listable/Sources/ListView/ListView.swift index 269ed1185..a46005ad0 100644 --- a/Listable/Sources/ListView/ListView.swift +++ b/Listable/Sources/ListView/ListView.swift @@ -500,6 +500,10 @@ public final class ListView : UIView */ self.collectionView.frame = self.bounds + guard oldValue != self.frame else { + return + } + /** Once the view actually has a size, we can provide content. @@ -516,15 +520,16 @@ public final class ListView : UIView self.updatePresentationState(for: .transitionedToBounds(isEmpty: true)) } - if oldValue != self.frame { - ListStateObserver.perform(self.stateObserver.onFrameChanged, "Frame Changed", with: self) { actions in - ListStateObserver.FrameChanged( - actions: actions, - positionInfo: self.scrollPositionInfo, - old: oldValue, - new: self.frame - ) - } + /// Our frame changed, update the keyboard inset in case the inset should now be different. + self.setContentInsetWithKeyboardFrame() + + ListStateObserver.perform(self.stateObserver.onFrameChanged, "Frame Changed", with: self) { actions in + ListStateObserver.FrameChanged( + actions: actions, + positionInfo: self.scrollPositionInfo, + old: oldValue, + new: self.frame + ) } } } @@ -558,6 +563,9 @@ public final class ListView : UIView super.layoutSubviews() self.collectionView.frame = self.bounds + + /// Our layout changed, update the keyboard inset in case the inset should now be different. + self.setContentInsetWithKeyboardFrame() } // @@ -868,6 +876,27 @@ public final class ListView : UIView } +public extension ListView +{ + /// + /// Call this method to force an immediate, synchronous re-render of the list + /// and its content when writing unit or snapshot tests. This avoids needing to + /// spin the runloop or needing to use test expectations to wait for content + /// to be rendered asynchronously. + /// + /// **WARNING**: You must **not** call this method outside of tests. Doing so will cause a fatal error. + /// + func testing_forceLayoutUpdateNow() + { + guard NSClassFromString("XCTestCase") != nil else { + fatalError("You must not call testing_forceLayoutUpdateNow outside of an XCTest environment.") + } + + self.collectionView.reloadData() + } +} + + extension ListView : ItemContentCoordinatorDelegate { func coordinatorUpdated(for : AnyItem, animated : Bool) @@ -960,19 +989,21 @@ extension ListView : KeyboardObserverDelegate return } - let inset : CGFloat - - switch self.behavior.keyboardAdjustmentMode { - case .none: inset = 0.0 - - case .adjustsWhenVisible: - switch frame { - case .nonOverlapping: inset = 0.0 + let inset : CGFloat = { + switch self.behavior.keyboardAdjustmentMode { + case .none: + return 0.0 - case .overlapping(let frame): - inset = (self.bounds.size.height - frame.origin.y) - self.lst_safeAreaInsets.bottom + case .adjustsWhenVisible: + switch frame { + case .nonOverlapping: + return 0.0 + + case .overlapping(let frame): + return (self.bounds.size.height - frame.origin.y) - self.lst_safeAreaInsets.bottom + } } - } + }() if self.collectionView.contentInset.bottom != inset { self.collectionView.contentInset.bottom = inset