From 2205d1186dea884bdee5fc5e1efe4751e20ddd1f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 15 Jan 2023 10:30:25 +0900 Subject: [PATCH] Enable to restrict the content size in FloatingPanelAdaptiveLayoutAnchor (#518) * Introduce FloatingPanelLayoutContentBoundingGuide property for FloatingPanelAdaptiveLayoutAnchor * Revise doc comments * Clean up code * Update the minimum deployment target of Samples app to iOS 11 --- .../Samples/Samples.xcodeproj/project.pbxproj | 8 +- .../Sources/Base.lproj/Main.storyboard | 47 +++++++++- .../AdaptiveLayoutTestViewController.swift | 89 +++++++++++++++++++ .../ImageViewController.swift | 32 +++---- Examples/Samples/Sources/Extensions.swift | 14 +++ .../Samples/Sources/UseCases/UseCase.swift | 5 +- .../Sources/UseCases/UseCaseController.swift | 33 +++++-- FloatingPanel.xcodeproj/project.pbxproj | 8 +- Sources/FloatingPanel.docc/FloatingPanel.md | 1 + Sources/Layout.swift | 40 +++++++-- Sources/LayoutAnchoring.swift | 76 +++++++++++++--- ...eferences.swift => LayoutProperties.swift} | 33 ++++++- 12 files changed, 335 insertions(+), 51 deletions(-) create mode 100644 Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift rename Sources/{LayoutReferences.swift => LayoutProperties.swift} (54%) diff --git a/Examples/Samples/Samples.xcodeproj/project.pbxproj b/Examples/Samples/Samples.xcodeproj/project.pbxproj index 54bca1c..f5c290f 100644 --- a/Examples/Samples/Samples.xcodeproj/project.pbxproj +++ b/Examples/Samples/Samples.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; }; 5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; }; 54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; }; + 544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */; }; 545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; }; 545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; }; 545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; }; @@ -79,6 +80,7 @@ 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = ""; }; 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = ""; }; 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = ""; }; + 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveLayoutTestViewController.swift; sourceTree = ""; }; 545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 545DB9EF21511E6300CA77B8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; @@ -142,6 +144,7 @@ 5442E22725FC51E200A26F43 /* MultiPanelController.swift */, 5442E22B25FC521F00A26F43 /* SettingsViewController.swift */, 5442E22F25FC525200A26F43 /* TabBarViewController.swift */, + 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */, ); path = ContentViewControllers; sourceTree = ""; @@ -359,6 +362,7 @@ 54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */, 54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */, 54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */, + 544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */, 5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */, 546341AC25C6426500CA0596 /* CustomState.swift in Sources */, 5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */, @@ -553,7 +557,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Sources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -576,7 +580,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Sources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index a563582..f13f16b 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -39,7 +39,7 @@ - + @@ -782,6 +782,51 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift new file mode 100644 index 0000000..a87a7fc --- /dev/null +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -0,0 +1,89 @@ +// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. + +import UIKit +import FloatingPanel + +final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + class PanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .full + + private unowned var targetGuide: UILayoutGuide + + init(targetGuide: UILayoutGuide) { + self.targetGuide = targetGuide + } + + var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelAdaptiveLayoutAnchor( + absoluteOffset: 0.0, + contentLayout: targetGuide, + referenceGuide: .superview, + contentBoundingGuide: .safeArea + ), + .half: FloatingPanelAdaptiveLayoutAnchor( + fractionalOffset: 0.5, + contentLayout: targetGuide, + referenceGuide: .superview, + contentBoundingGuide: .safeArea + ), + ] + } + } + + @IBOutlet weak var tableView: IntrinsicTableView! + private let cellID = "Cell" + + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) + } + + // MARK: - UITableViewDataSource + + func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 50 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) + cell.textLabel?.text = "\(indexPath.row)" + return cell + } + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .orange + return headerView + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + 44.0 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 40 + } +} + +class IntrinsicTableView: UITableView { + + override var contentSize:CGSize { + didSet { + invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + layoutIfNeeded() + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } +} diff --git a/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift b/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift index 3aa2635..503e29a 100644 --- a/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift +++ b/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift @@ -5,29 +5,25 @@ import FloatingPanel final class ImageViewController: UIViewController { class PanelLayout: FloatingPanelLayout { - weak var targetGuide: UILayoutGuide? - init(targetGuide: UILayoutGuide?) { + private unowned var targetGuide: UILayoutGuide + init(targetGuide: UILayoutGuide) { self.targetGuide = targetGuide } let position: FloatingPanelPosition = .bottom let initialState: FloatingPanelState = .full var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { - if #available(iOS 11.0, *), let targetGuide = targetGuide { - return [ - .full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0, - contentLayout: targetGuide, - referenceGuide: .superview), - .half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5, - contentLayout: targetGuide, - referenceGuide: .superview) - ] - } else { - return [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 500, - edge: .bottom, - referenceGuide: .superview) - ] - } + return [ + .full: FloatingPanelAdaptiveLayoutAnchor( + absoluteOffset: 0, + contentLayout: targetGuide, + referenceGuide: .superview + ), + .half: FloatingPanelAdaptiveLayoutAnchor( + fractionalOffset: 0.5, + contentLayout: targetGuide, + referenceGuide: .superview + ) + ] } } diff --git a/Examples/Samples/Sources/Extensions.swift b/Examples/Samples/Sources/Extensions.swift index 17e1581..d93b62a 100644 --- a/Examples/Samples/Sources/Extensions.swift +++ b/Examples/Samples/Sources/Extensions.swift @@ -2,6 +2,20 @@ import UIKit +extension UIView { + func makeBoundsLayoutGuide() -> UILayoutGuide { + let guide = UILayoutGuide() + addLayoutGuide(guide) + NSLayoutConstraint.activate([ + guide.topAnchor.constraint(equalTo: topAnchor), + guide.leftAnchor.constraint(equalTo: leftAnchor), + guide.bottomAnchor.constraint(equalTo: bottomAnchor), + guide.rightAnchor.constraint(equalTo: rightAnchor), + ]) + return guide + } +} + protocol LayoutGuideProvider { var topAnchor: NSLayoutYAxisAnchor { get } var bottomAnchor: NSLayoutYAxisAnchor { get } diff --git a/Examples/Samples/Sources/UseCases/UseCase.swift b/Examples/Samples/Sources/UseCases/UseCase.swift index 8d57bd8..eb7417e 100644 --- a/Examples/Samples/Sources/UseCases/UseCase.swift +++ b/Examples/Samples/Sources/UseCases/UseCase.swift @@ -50,7 +50,9 @@ extension UseCase { case .showCustomStatePanel: return "Show Panel with Custom state" } } +} +extension UseCase { private enum Content { case storyboard(String) case viewController(UIViewController) @@ -76,7 +78,7 @@ extension UseCase { case .showNavigationController: return .storyboard("RootNavigationController") // Storyboard only case .showTopPositionedPanel: return .viewController(DebugTableViewController()) case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self)) - case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: ImageViewController.self)) + case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self)) case .showCustomStatePanel: return .viewController(DebugTableViewController()) } } @@ -86,6 +88,7 @@ extension UseCase { case .storyboard(let id): return storyboard.instantiateViewController(withIdentifier: id) case .viewController(let vc): + vc.loadViewIfNeeded() return vc } } diff --git a/Examples/Samples/Sources/UseCases/UseCaseController.swift b/Examples/Samples/Sources/UseCases/UseCaseController.swift index dac33d4..3d6f7a8 100644 --- a/Examples/Samples/Sources/UseCases/UseCaseController.swift +++ b/Examples/Samples/Sources/UseCases/UseCaseController.swift @@ -218,22 +218,34 @@ extension UseCaseController { fpc.set(contentViewController: contentVC) addMain(panel: fpc) - case .showAdaptivePanel, .showAdaptivePanelWithCustomGuide: + case .showAdaptivePanel: let fpc = FloatingPanelController() fpc.isRemovalInteractionEnabled = true fpc.set(contentViewController: contentVC) fpc.ext_trackScrollView(in: contentVC) if case let contentVC as ImageViewController = contentVC { - if #available(iOS 11.0, *) { - let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage - let layoutGuide = contentVC.layoutGuideFor(mode: mode) - fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide) - } else { - fpc.layout = ImageViewController.PanelLayout(targetGuide: nil) - } + let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage + let layoutGuide = contentVC.layoutGuideFor(mode: mode) + fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide) } addMain(panel: fpc) + case .showAdaptivePanelWithCustomGuide: + let fpc = FloatingPanelController() + fpc.isRemovalInteractionEnabled = true + fpc.contentInsetAdjustmentBehavior = .always + fpc.surfaceView.appearance = { + let appearance = SurfaceAppearance() + appearance.cornerRadius = 6.0 + return appearance + }() + + + fpc.set(contentViewController: contentVC) + fpc.ext_trackScrollView(in: contentVC) + fpc.layout = AdaptiveLayoutTestViewController.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide()) + addMain(panel: fpc) + case .showCustomStatePanel: let fpc = FloatingPanelController() fpc.delegate = self @@ -283,7 +295,7 @@ extension UseCaseController { } } - @objc + @objc private func handleSurface(tapGesture: UITapGestureRecognizer) { switch mainPanelVC.state { case .full: @@ -377,6 +389,9 @@ private extension FloatingPanelController { case let contentVC as ImageViewController: track(scrollView: contentVC.scrollView) + case let contentVC as AdaptiveLayoutTestViewController: + track(scrollView: contentVC.tableView) + default: break } diff --git a/FloatingPanel.xcodeproj/project.pbxproj b/FloatingPanel.xcodeproj/project.pbxproj index 877755a..228984b 100644 --- a/FloatingPanel.xcodeproj/project.pbxproj +++ b/FloatingPanel.xcodeproj/project.pbxproj @@ -22,7 +22,7 @@ 5469F4AE24B30D7E00537F8A /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AD24B30D7E00537F8A /* State.swift */; }; 5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; }; 5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; }; - 5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutReferences.swift */; }; + 5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutProperties.swift */; }; 549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; }; 549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; }; 54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; }; @@ -74,7 +74,7 @@ 5469F4AD24B30D7E00537F8A /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = ""; }; 5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = ""; }; - 5469F4B324B30F3500537F8A /* LayoutReferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutReferences.swift; sourceTree = ""; }; + 5469F4B324B30F3500537F8A /* LayoutProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProperties.swift; sourceTree = ""; }; 549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = ""; }; 549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = ""; }; 54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = ""; }; @@ -145,7 +145,7 @@ 5469F4B124B30F1100537F8A /* Position.swift */, 54CFBFC4215CD09C006B5735 /* Core.swift */, 54CFBFC2215CD045006B5735 /* Layout.swift */, - 5469F4B324B30F3500537F8A /* LayoutReferences.swift */, + 5469F4B324B30F3500537F8A /* LayoutProperties.swift */, 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */, 5450EEE321646DF500135936 /* Behavior.swift */, 54352E9721A521CA00CBCA08 /* PassthroughView.swift */, @@ -341,7 +341,7 @@ 5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */, 54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */, 54CFBFC3215CD045006B5735 /* Layout.swift in Sources */, - 5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */, + 5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */, 54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */, 54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */, 54CFBFC5215CD09C006B5735 /* Core.swift in Sources */, diff --git a/Sources/FloatingPanel.docc/FloatingPanel.md b/Sources/FloatingPanel.docc/FloatingPanel.md index a1e08ab..abdfd47 100644 --- a/Sources/FloatingPanel.docc/FloatingPanel.md +++ b/Sources/FloatingPanel.docc/FloatingPanel.md @@ -44,6 +44,7 @@ The new interface displays the related contents and utilities in parallel as a u - ``FloatingPanelPosition`` - ``FloatingPanelReferenceEdge`` - ``FloatingPanelLayoutReferenceGuide`` +- ``FloatingPanelLayoutContentBoundingGuide`` ### Behaviors diff --git a/Sources/Layout.swift b/Sources/Layout.swift index 81a955a..78a6b29 100644 --- a/Sources/Layout.swift +++ b/Sources/Layout.swift @@ -92,6 +92,9 @@ class LayoutAdapter { private var staticConstraint: NSLayoutConstraint? + /// A layout constraint to limit the content size in ``FloatingPanelAdaptiveLayoutAnchor``. + private var contentBoundingConstraint: NSLayoutConstraint? + private var anchorStates: Set { return Set(layout.anchors.keys) } @@ -330,12 +333,27 @@ class LayoutAdapter { if anchor.referenceGuide == .safeArea { referenceBoundsLength += position.inset(safeAreaInsets) } - return dimension - diff + let maxPosition: CGFloat = { + if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) { + return layout.position.mainLocation(maxBounds.origin) + + layout.position.mainDimension(maxBounds.size) + } else { + return .infinity + } + }() + return min(dimension - diff, maxPosition) case .bottom, .right: if anchor.referenceGuide == .safeArea { referenceBoundsLength -= position.inset(safeAreaInsets) } - return referenceBoundsLength - dimension + diff + let minPosition: CGFloat = { + if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) { + return layout.position.mainLocation(maxBounds.origin) + } else { + return -(.infinity) + } + }() + return max(referenceBoundsLength - dimension + diff, minPosition) } case let anchor as FloatingPanelLayoutAnchor: let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds @@ -629,8 +647,9 @@ class LayoutAdapter { // The method is separated from prepareLayout(to:) for the rotation support // It must be called in FloatingPanelController.traitCollectionDidChange(_:) func updateStaticConstraint() { - NSLayoutConstraint.deactivate(constraint: staticConstraint) + NSLayoutConstraint.deactivate([staticConstraint, contentBoundingConstraint].compactMap{ $0 }) staticConstraint = nil + contentBoundingConstraint = nil if vc.contentMode == .fitToBounds { surfaceView.containerOverflow = 0 @@ -654,7 +673,18 @@ class LayoutAdapter { constant = 0.0 } let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide) - staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant) + if let boundingLayoutGuide = anchor.contentBoundingGuide.layoutGuide(vc) { + if anchor.isAbsolute { + contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), + constant: anchor.offset) + } else { + contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), + multiplier: anchor.offset) + } + staticConstraint = surfaceAnchor.constraint(lessThanOrEqualTo: baseAnchor, constant: constant) + } else { + staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant) + } default: switch position { case .top, .left: @@ -673,7 +703,7 @@ class LayoutAdapter { staticConstraint?.identifier = "FloatingPanel-static-width" } - NSLayoutConstraint.activate(constraint: staticConstraint) + NSLayoutConstraint.activate([staticConstraint, contentBoundingConstraint].compactMap{ $0 }) surfaceView.containerOverflow = position.mainDimension(vc.view.bounds.size) } diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index c20ad87..17e1aa5 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -15,6 +15,11 @@ import UIKit /// /// The inset is an amount to inset a panel from an edge of the reference guide. The edge refers to a panel /// positioning. + /// + /// - Parameters: + /// - absoluteOffset: An absolute offset to attach the panel from the edge. + /// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset. + /// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. @objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) { self.inset = absoluteInset self.referenceGuide = referenceGuide @@ -27,6 +32,11 @@ import UIKit /// The inset is an amount to inset a panel from the edge of the specified reference guide. The value is /// a floating-point number in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and /// 1.0 represents a distance to the opposite edge. + /// + /// - Parameters: + /// - fractionalOffset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge. + /// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset. + /// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. @objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) { self.inset = fractionalInset self.referenceGuide = referenceGuide @@ -49,7 +59,7 @@ public extension FloatingPanelLayoutAnchor { case .left: return layoutConstraints(layoutGuide, for: vc.surfaceView.rightAnchor) case .bottom: - return layoutConstraints(layoutGuide, for: vc.surfaceView.topAnchor) + return layoutConstraints(layoutGuide, for: vc.surfaceView.topAnchor) case .right: return layoutConstraints(layoutGuide, for: vc.surfaceView.leftAnchor) } @@ -101,6 +111,10 @@ public extension FloatingPanelLayoutAnchor { /// /// The offset is an amount to offset a position of panel that displays the entire content from an edge of /// the reference guide. The edge refers to a panel positioning. + /// + /// - Parameters: + /// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel. + /// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. @objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) { self.offset = offset self.referenceGuide = referenceGuide @@ -111,6 +125,10 @@ public extension FloatingPanelLayoutAnchor { /// /// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content /// is displayed and 0.5 represents the half of content is displayed. + /// + /// - Parameters: + /// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel. + /// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. @objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) { self.offset = offset self.referenceGuide = referenceGuide @@ -144,37 +162,72 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// An object that defines how to settles a panel with a layout guide of a content view. @objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ { - /// Returns a layout anchor with the specified offset by an absolute value, layout guide to display content and reference guide for a panel. + /// Returns a layout anchor with the specified offset by an absolute value to display a panel with its intrinsic content size. /// /// The offset is an amount to offset a position of panel that displays the entire content of the specified guide from an edge of /// the reference guide. The edge refers to a panel positioning. - @objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) { + /// + /// ``contentBoundingGuide`` restricts the content size which a panel displays. For example, given ``referenceGuide`` is `.superview` and ``contentBoundingGuide`` is `.safeArea` for a bottom positioned panel, the panel content is laid out inside the superview of the view of FloatingPanelController(not its safe area), but its content size is limited to its safe area size. Normally both of ``referenceGuide`` and ``contentBoundingGuide`` are specified with the same rectangle area. + /// + /// - Parameters: + /// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel. + /// - contentLayout: The content layout guide to calculate the content size in the panel. + /// - referenceGuide: The rectangular area to lay out the content of a panel. If it's set to `.safeArea`, the panel content displays inside the safe area of its ``FloatingPanelController``'s view. This argument doesn't limit its content size. + /// - contentBoundingGuide: The rectangular area to restrict the content size of a panel in the main dimension(i.e. y axis is the main dimension for a bottom panel). + /// + /// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content. + @objc public init( + absoluteOffset offset: CGFloat, + contentLayout: UILayoutGuide, + referenceGuide: FloatingPanelLayoutReferenceGuide, + contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none + ) { self.offset = offset self.contentLayoutGuide = contentLayout self.referenceGuide = referenceGuide + self.contentBoundingGuide = contentBoundingGuide self.isAbsolute = true - } - /// Returns a layout anchor with the specified offset by a fractional value, layout guide to display content and reference guide for a panel. + /// Returns a layout anchor with the specified offset by a fractional value to display a panel with its intrinsic content size. /// /// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content /// is displayed and 0.5 represents the half of content is displayed. - @objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) { + /// + /// ``contentBoundingGuide`` restricts the content size which a panel displays. For example, given ``referenceGuide`` is `.superview` and ``contentBoundingGuide`` is `.safeArea` for a bottom positioned panel, the panel content is laid out inside the superview of the view of FloatingPanelController(not its safe area), but its content size is limited to its safe area size. Normally both of ``referenceGuide`` and ``contentBoundingGuide`` are specified with the same rectangle area. + /// + /// - Parameters: + /// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel. + /// - contentLayout: The content layout guide to calculate the content size in the panel. + /// - referenceGuide: The rectangular area to lay out the content of a panel. If it's set to `.safeArea`, the panel content displays inside the safe area of its ``FloatingPanelController``'s view. This argument doesn't limit its content size. + /// - contentBoundingGuide: The rectangular area to restrict the content size of a panel in the main dimension(i.e. y axis is the main dimension for a bottom panel). + /// + /// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content. + @objc public init( + fractionalOffset offset: CGFloat, + contentLayout: UILayoutGuide, + referenceGuide: FloatingPanelLayoutReferenceGuide, + contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none + ) { self.offset = offset self.contentLayoutGuide = contentLayout self.referenceGuide = referenceGuide + self.contentBoundingGuide = contentBoundingGuide self.isAbsolute = false } - fileprivate let offset: CGFloat - fileprivate let isAbsolute: Bool + let offset: CGFloat + let isAbsolute: Bool let contentLayoutGuide: UILayoutGuide @objc public let referenceGuide: FloatingPanelLayoutReferenceGuide + @objc public let contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide } public extension FloatingPanelAdaptiveLayoutAnchor { func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] { + var constraints = [NSLayoutConstraint]() + let layoutGuide = referenceGuide.layoutGuide(vc: vc) + let offsetConstraint: NSLayoutConstraint let offsetAnchor: NSLayoutDimension switch position { case .top: @@ -187,10 +240,13 @@ public extension FloatingPanelAdaptiveLayoutAnchor { offsetAnchor = vc.surfaceView.leftAnchor.anchorWithOffset(to: layoutGuide.rightAnchor) } if isAbsolute { - return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)] + offsetConstraint = offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset) } else { - return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))] + offsetConstraint = offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset)) } + constraints.append(offsetConstraint) + + return constraints } } diff --git a/Sources/LayoutReferences.swift b/Sources/LayoutProperties.swift similarity index 54% rename from Sources/LayoutReferences.swift rename to Sources/LayoutProperties.swift index ff9f874..8f2355c 100644 --- a/Sources/LayoutReferences.swift +++ b/Sources/LayoutProperties.swift @@ -27,7 +27,7 @@ extension FloatingPanelReferenceEdge { } } -/// Constants that specify a layout guide to lay out a panel. +/// A representation to specify a rectangular area to lay out a panel. @objc public enum FloatingPanelLayoutReferenceGuide: Int { case superview = 0 case safeArea = 1 @@ -43,3 +43,34 @@ extension FloatingPanelLayoutReferenceGuide { } } } + +/// A representation to specify a bounding box which limit the content size of a panel. +@objc public enum FloatingPanelLayoutContentBoundingGuide: Int { + case none = 0 + case superview = 1 + case safeArea = 2 +} + +extension FloatingPanelLayoutContentBoundingGuide { + func layoutGuide(_ fpc: FloatingPanelController) -> LayoutGuideProvider? { + switch self { + case .superview: + return fpc.view + case .safeArea: + return fpc.fp_safeAreaLayoutGuide + case .none: + return nil + } + } + func maxBounds(_ fpc: FloatingPanelController) -> CGRect? { + switch self { + case .superview: + return fpc.view.bounds + case .safeArea: + return fpc.view.bounds.inset(by: fpc.fp_safeAreaInsets) + case .none: + return nil + } + } + +}