From ca0e27349ff2cc5245e4c7500047f456d55e0e78 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 26 Nov 2021 22:14:30 +0900 Subject: [PATCH 1/9] Enable to set a bounding rect for FloatingPanelAdaptiveLayoutAnchor --- .../Samples/Samples.xcodeproj/project.pbxproj | 4 + .../Sources/Base.lproj/Main.storyboard | 47 ++++++++++- .../AdaptiveLayoutTestViewController.swift | 78 +++++++++++++++++++ Examples/Samples/Sources/Extensions.swift | 14 ++++ .../Samples/Sources/UseCases/UseCase.swift | 5 +- .../Sources/UseCases/UseCaseController.swift | 23 +++++- FloatingPanel.xcodeproj/project.pbxproj | 8 +- Sources/Layout.swift | 38 +++++++-- Sources/LayoutAnchoring.swift | 43 ++++++++-- ...eferences.swift => LayoutProperties.swift} | 31 ++++++++ 10 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift rename Sources/{LayoutReferences.swift => LayoutProperties.swift} (59%) diff --git a/Examples/Samples/Samples.xcodeproj/project.pbxproj b/Examples/Samples/Samples.xcodeproj/project.pbxproj index c4074630..90d177a1 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 */; }; @@ -78,6 +79,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 = ""; }; @@ -139,6 +141,7 @@ 5442E22725FC51E200A26F43 /* MultiPanelController.swift */, 5442E22B25FC521F00A26F43 /* SettingsViewController.swift */, 5442E22F25FC525200A26F43 /* TabBarViewController.swift */, + 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */, ); path = ContentViewControllers; sourceTree = ""; @@ -347,6 +350,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 */, diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index 65354893..92c4ddde 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 00000000..7972eedd --- /dev/null +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -0,0 +1,78 @@ +// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. + +import UIKit +import FloatingPanel + +final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + class PanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .full + + private weak var targetGuide: UILayoutGuide? + init(targetGuide: UILayoutGuide?) { + self.targetGuide = targetGuide + } + var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { + if #available(iOS 11.0, *), let targetGuide = targetGuide { + return [ + .full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0.0, + contentLayout: targetGuide, + referenceGuide: .superview, + boundingGuide: .superview), + .half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5, + contentLayout: targetGuide, + referenceGuide: .superview, + boundingGuide: .safeArea), + + ] + } else { + return [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 500, + edge: .bottom, + referenceGuide: .superview) + ] + } + } + } + + @IBOutlet weak var tableView: IntrinsicTableView! + let cellResuseID = "Cell" + + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellResuseID) + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellResuseID, for: indexPath) + cell.textLabel?.text = "\(indexPath.row)" + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 40 + } + + func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 50 + } +} + +class IntrinsicTableView: UITableView { + + override var contentSize:CGSize { + didSet { + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + self.layoutIfNeeded() + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } +} diff --git a/Examples/Samples/Sources/Extensions.swift b/Examples/Samples/Sources/Extensions.swift index 17e15816..d93b62a1 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 8d57bd88..eb7417ed 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 df3595d0..5a931938 100644 --- a/Examples/Samples/Sources/UseCases/UseCaseController.swift +++ b/Examples/Samples/Sources/UseCases/UseCaseController.swift @@ -218,7 +218,7 @@ extension UseCaseController { fpc.set(contentViewController: contentVC) addMain(panel: fpc) - case .showAdaptivePanel, .showAdaptivePanelWithCustomGuide: + case .showAdaptivePanel: let fpc = FloatingPanelController() fpc.isRemovalInteractionEnabled = true fpc.set(contentViewController: contentVC) @@ -234,6 +234,22 @@ extension UseCaseController { } 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 +299,7 @@ extension UseCaseController { } } - @objc + @objc private func handleSurface(tapGesture: UITapGestureRecognizer) { switch mainPanelVC.state { case .full: @@ -377,6 +393,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 2f88f64b..a844d66d 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 */; }; @@ -73,7 +73,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 = ""; }; @@ -141,7 +141,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 */, @@ -329,7 +329,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/Layout.swift b/Sources/Layout.swift index 81a955a9..5972252f 100644 --- a/Sources/Layout.swift +++ b/Sources/Layout.swift @@ -91,6 +91,7 @@ class LayoutAdapter { private(set) var attractionConstraint: NSLayoutConstraint? private var staticConstraint: NSLayoutConstraint? + private var boundingConstraint: NSLayoutConstraint? private var anchorStates: Set { return Set(layout.anchors.keys) @@ -330,12 +331,27 @@ class LayoutAdapter { if anchor.referenceGuide == .safeArea { referenceBoundsLength += position.inset(safeAreaInsets) } - return dimension - diff + let maxPosition: CGFloat = { + if let maxBounds = anchor.boundingGuide.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.boundingGuide.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 +645,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, boundingConstraint].compactMap{ $0 }) staticConstraint = nil + boundingConstraint = nil if vc.contentMode == .fitToBounds { surfaceView.containerOverflow = 0 @@ -654,7 +671,18 @@ class LayoutAdapter { constant = 0.0 } let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide) - staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant) + if let boundingLayoutGuide = anchor.boundingGuide.layoutGuide(vc) { + if anchor.isAbsolute { + boundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), + constant: anchor.offset) + } else { + boundingConstraint = 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 +701,7 @@ class LayoutAdapter { staticConstraint?.identifier = "FloatingPanel-static-width" } - NSLayoutConstraint.activate(constraint: staticConstraint) + NSLayoutConstraint.activate([staticConstraint, boundingConstraint].compactMap{ $0 }) surfaceView.containerOverflow = position.mainDimension(vc.view.bounds.size) } diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index c20ad87a..d74aecdc 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -148,33 +148,59 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// /// 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) { + @objc public init(absoluteOffset offset: CGFloat, + contentLayout: UILayoutGuide, + referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, + boundingGuide: FloatingPanelLayoutBoundingGuide = .none) { self.offset = offset self.contentLayoutGuide = contentLayout self.referenceGuide = referenceGuide + self.boundingGuide = boundingGuide 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. /// /// 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) { + @objc public init(fractionalOffset offset: CGFloat, + contentLayout: UILayoutGuide, + referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, + boundingGuide: FloatingPanelLayoutBoundingGuide = .none) { self.offset = offset self.contentLayoutGuide = contentLayout self.referenceGuide = referenceGuide + self.boundingGuide = boundingGuide 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 boundingGuide: FloatingPanelLayoutBoundingGuide } public extension FloatingPanelAdaptiveLayoutAnchor { func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] { + var constraints = [NSLayoutConstraint]() + + let boundingConstraint: NSLayoutConstraint + if let boundingLayoutGuide = boundingGuide.layoutGuide(vc) { + switch position { + case .top: + boundingConstraint = vc.surfaceView.bottomAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.bottomAnchor) + case .left: + boundingConstraint = vc.surfaceView.rightAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.rightAnchor) + case .bottom: + boundingConstraint = vc.surfaceView.topAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.topAnchor) + case .right: + boundingConstraint = vc.surfaceView.leftAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.leftAnchor) + } + constraints.append(boundingConstraint) + } + let layoutGuide = referenceGuide.layoutGuide(vc: vc) + let offsetConstraint: NSLayoutConstraint let offsetAnchor: NSLayoutDimension switch position { case .top: @@ -187,10 +213,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 59% rename from Sources/LayoutReferences.swift rename to Sources/LayoutProperties.swift index ff9f874a..47ec9f12 100644 --- a/Sources/LayoutReferences.swift +++ b/Sources/LayoutProperties.swift @@ -43,3 +43,34 @@ extension FloatingPanelLayoutReferenceGuide { } } } + +/// Constants that specify a layout guide to lay out a panel. +@objc public enum FloatingPanelLayoutBoundingGuide: Int { + case none = 0 + case superview = 1 + case safeArea = 2 +} + +extension FloatingPanelLayoutBoundingGuide { + 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 + } + } + +} From 8cca6eada48be1c54d456b4599255a497026cf05 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Nov 2021 09:23:29 +0900 Subject: [PATCH 2/9] Add the doc comments for Layout anchors --- Sources/LayoutAnchoring.swift | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index d74aecdc..3930ef83 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 @@ -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 @@ -148,6 +166,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// /// 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. + /// + /// - 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. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. + /// - boundingGuide: The bounding guid to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) @objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, @@ -163,6 +187,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// /// 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. + /// - contentLayout: The content layout guide to calculate the content size in 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. + /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel)``````` @objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, From 719e5575ef509ed1084f6d003119ec2d31e397da Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 30 Jul 2022 10:36:48 +0900 Subject: [PATCH 3/9] Remove double spaces --- Sources/LayoutAnchoring.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index 3930ef83..5cf110ac 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -18,7 +18,7 @@ import UIKit /// /// - 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. + /// - 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 @@ -34,8 +34,8 @@ import UIKit /// 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. + /// - 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 @@ -59,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) } From 910dc3eda7d3505ba5356e998c8d48dfea171c1d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 7 Jan 2023 09:24:43 +0900 Subject: [PATCH 4/9] Fix doc comments --- Sources/LayoutAnchoring.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index 5cf110ac..92f546a8 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -171,7 +171,7 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// - 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. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. - /// - boundingGuide: The bounding guid to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) + /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) @objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, @@ -192,7 +192,7 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// - 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. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. - /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel)``````` + /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) @objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, From 5d38aa0afa58c9e51625b56b7317faa97f3789b2 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 7 Jan 2023 09:38:22 +0900 Subject: [PATCH 5/9] Update the minimum deployment target of Samples app --- Examples/Samples/Samples.xcodeproj/project.pbxproj | 4 ++-- .../AdaptiveLayoutTestViewController.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Samples/Samples.xcodeproj/project.pbxproj b/Examples/Samples/Samples.xcodeproj/project.pbxproj index 625c38fc..f5c290f7 100644 --- a/Examples/Samples/Samples.xcodeproj/project.pbxproj +++ b/Examples/Samples/Samples.xcodeproj/project.pbxproj @@ -557,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", @@ -580,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/ContentViewControllers/AdaptiveLayoutTestViewController.swift b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift index 7972eedd..b92f4430 100644 --- a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -13,7 +13,7 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg self.targetGuide = targetGuide } var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { - if #available(iOS 11.0, *), let targetGuide = targetGuide { + if let targetGuide = targetGuide { return [ .full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0.0, contentLayout: targetGuide, From bdc1755f1f375ddbc9df65c6cb614ffe71626e57 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 7 Jan 2023 09:49:58 +0900 Subject: [PATCH 6/9] Refactor AdaptiveLayoutTestViewController --- .../AdaptiveLayoutTestViewController.swift | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift index b92f4430..9821382f 100644 --- a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -8,30 +8,27 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg let position: FloatingPanelPosition = .bottom let initialState: FloatingPanelState = .full - private weak var targetGuide: UILayoutGuide? - init(targetGuide: UILayoutGuide?) { + private unowned var targetGuide: UILayoutGuide + + init(targetGuide: UILayoutGuide) { self.targetGuide = targetGuide } + var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { - if let targetGuide = targetGuide { - return [ - .full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0.0, - contentLayout: targetGuide, - referenceGuide: .superview, - boundingGuide: .superview), - .half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5, - contentLayout: targetGuide, - referenceGuide: .superview, - boundingGuide: .safeArea), - - ] - } else { - return [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 500, - edge: .bottom, - referenceGuide: .superview) - ] - } + return [ + .full: FloatingPanelAdaptiveLayoutAnchor( + absoluteOffset: 0.0, + contentLayout: targetGuide, + referenceGuide: .superview, + boundingGuide: .superview + ), + .half: FloatingPanelAdaptiveLayoutAnchor( + fractionalOffset: 0.5, + contentLayout: targetGuide, + referenceGuide: .superview, + boundingGuide: .safeArea + ), + ] } } @@ -50,8 +47,18 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg return cell } + 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 { - return 40 + 40 } func numberOfSections(in tableView: UITableView) -> Int { @@ -67,12 +74,12 @@ class IntrinsicTableView: UITableView { override var contentSize:CGSize { didSet { - self.invalidateIntrinsicContentSize() + invalidateIntrinsicContentSize() } } override var intrinsicContentSize: CGSize { - self.layoutIfNeeded() + layoutIfNeeded() return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) } } From 04aa5b5e690a853ab86a2d7f653fe44321a452ce Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 7 Jan 2023 10:42:00 +0900 Subject: [PATCH 7/9] Make the FloatingPanelAdaptiveLayoutAnchor impl obvious * Removed incorrect constraints * Renamed FloatingPanelLayout{ => Content}BoundingGuide * Updated the doc comments --- .../AdaptiveLayoutTestViewController.swift | 4 +- .../ImageViewController.swift | 32 +++++----- .../Sources/UseCases/UseCaseController.swift | 10 +-- Sources/FloatingPanel.docc/FloatingPanel.md | 1 + Sources/Layout.swift | 20 +++--- Sources/LayoutAnchoring.swift | 61 +++++++++---------- Sources/LayoutProperties.swift | 8 +-- 7 files changed, 64 insertions(+), 72 deletions(-) diff --git a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift index 9821382f..6d4ad581 100644 --- a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -20,13 +20,13 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg absoluteOffset: 0.0, contentLayout: targetGuide, referenceGuide: .superview, - boundingGuide: .superview + contentBoundingGuide: .safeArea ), .half: FloatingPanelAdaptiveLayoutAnchor( fractionalOffset: 0.5, contentLayout: targetGuide, referenceGuide: .superview, - boundingGuide: .safeArea + contentBoundingGuide: .safeArea ), ] } diff --git a/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift b/Examples/Samples/Sources/ContentViewControllers/ImageViewController.swift index 3aa26352..503e29a0 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/UseCases/UseCaseController.swift b/Examples/Samples/Sources/UseCases/UseCaseController.swift index b39f908b..3d6f7a8d 100644 --- a/Examples/Samples/Sources/UseCases/UseCaseController.swift +++ b/Examples/Samples/Sources/UseCases/UseCaseController.swift @@ -224,13 +224,9 @@ extension UseCaseController { 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) diff --git a/Sources/FloatingPanel.docc/FloatingPanel.md b/Sources/FloatingPanel.docc/FloatingPanel.md index a1e08abd..abdfd47e 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 5972252f..3a6b10b9 100644 --- a/Sources/Layout.swift +++ b/Sources/Layout.swift @@ -91,7 +91,9 @@ class LayoutAdapter { private(set) var attractionConstraint: NSLayoutConstraint? private var staticConstraint: NSLayoutConstraint? - private var boundingConstraint: NSLayoutConstraint? + + /// A layout constraint to limit the content size on ``FloatingPanelAdaptiveLayoutAnchor``. + private var contentBoundingConstraint: NSLayoutConstraint? private var anchorStates: Set { return Set(layout.anchors.keys) @@ -332,7 +334,7 @@ class LayoutAdapter { referenceBoundsLength += position.inset(safeAreaInsets) } let maxPosition: CGFloat = { - if let maxBounds = anchor.boundingGuide.maxBounds(vc) { + if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) { return layout.position.mainLocation(maxBounds.origin) + layout.position.mainDimension(maxBounds.size) } else { @@ -345,7 +347,7 @@ class LayoutAdapter { referenceBoundsLength -= position.inset(safeAreaInsets) } let minPosition: CGFloat = { - if let maxBounds = anchor.boundingGuide.maxBounds(vc) { + if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) { return layout.position.mainLocation(maxBounds.origin) } else { return -(.infinity) @@ -645,9 +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([staticConstraint, boundingConstraint].compactMap{ $0 }) + NSLayoutConstraint.deactivate([staticConstraint, contentBoundingConstraint].compactMap{ $0 }) staticConstraint = nil - boundingConstraint = nil + contentBoundingConstraint = nil if vc.contentMode == .fitToBounds { surfaceView.containerOverflow = 0 @@ -671,12 +673,12 @@ class LayoutAdapter { constant = 0.0 } let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide) - if let boundingLayoutGuide = anchor.boundingGuide.layoutGuide(vc) { + if let boundingLayoutGuide = anchor.contentBoundingGuide.layoutGuide(vc) { if anchor.isAbsolute { - boundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), + contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), constant: anchor.offset) } else { - boundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), + contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide), multiplier: anchor.offset) } staticConstraint = surfaceAnchor.constraint(lessThanOrEqualTo: baseAnchor, constant: constant) @@ -701,7 +703,7 @@ class LayoutAdapter { staticConstraint?.identifier = "FloatingPanel-static-width" } - NSLayoutConstraint.activate([staticConstraint, boundingConstraint].compactMap{ $0 }) + 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 92f546a8..a7fd9aae 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -162,73 +162,70 @@ 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. /// + /// ``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. + /// /// - 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. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. - /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) - @objc public init(absoluteOffset offset: CGFloat, - contentLayout: UILayoutGuide, - referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, - boundingGuide: FloatingPanelLayoutBoundingGuide = .none) { + /// - 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.boundingGuide = boundingGuide + 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. /// + /// ``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. + /// /// - 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. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view. - /// - boundingGuide: The rectangular area to restrict a panel size in the main dimension(i.e. y axis for a bottom panel) - @objc public init(fractionalOffset offset: CGFloat, - contentLayout: UILayoutGuide, - referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea, - boundingGuide: FloatingPanelLayoutBoundingGuide = .none) { + /// - 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.boundingGuide = boundingGuide + self.contentBoundingGuide = contentBoundingGuide self.isAbsolute = false } let offset: CGFloat let isAbsolute: Bool let contentLayoutGuide: UILayoutGuide @objc public let referenceGuide: FloatingPanelLayoutReferenceGuide - @objc public let boundingGuide: FloatingPanelLayoutBoundingGuide + @objc public let contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide } public extension FloatingPanelAdaptiveLayoutAnchor { func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] { var constraints = [NSLayoutConstraint]() - let boundingConstraint: NSLayoutConstraint - if let boundingLayoutGuide = boundingGuide.layoutGuide(vc) { - switch position { - case .top: - boundingConstraint = vc.surfaceView.bottomAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.bottomAnchor) - case .left: - boundingConstraint = vc.surfaceView.rightAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.rightAnchor) - case .bottom: - boundingConstraint = vc.surfaceView.topAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.topAnchor) - case .right: - boundingConstraint = vc.surfaceView.leftAnchor.constraint(greaterThanOrEqualTo: boundingLayoutGuide.leftAnchor) - } - constraints.append(boundingConstraint) - } - let layoutGuide = referenceGuide.layoutGuide(vc: vc) let offsetConstraint: NSLayoutConstraint let offsetAnchor: NSLayoutDimension diff --git a/Sources/LayoutProperties.swift b/Sources/LayoutProperties.swift index 47ec9f12..ee463373 100644 --- a/Sources/LayoutProperties.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 layout guide to lay out a panel. @objc public enum FloatingPanelLayoutReferenceGuide: Int { case superview = 0 case safeArea = 1 @@ -44,14 +44,14 @@ extension FloatingPanelLayoutReferenceGuide { } } -/// Constants that specify a layout guide to lay out a panel. -@objc public enum FloatingPanelLayoutBoundingGuide: Int { +/// 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 FloatingPanelLayoutBoundingGuide { +extension FloatingPanelLayoutContentBoundingGuide { func layoutGuide(_ fpc: FloatingPanelController) -> LayoutGuideProvider? { switch self { case .superview: From 3221e76b2283eaa542fa28a1ee0eb229cb3866d9 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 15 Jan 2023 09:02:32 +0900 Subject: [PATCH 8/9] Clean up AdaptiveLayoutTestViewController --- .../AdaptiveLayoutTestViewController.swift | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift index 6d4ad581..a87a7fcf 100644 --- a/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift +++ b/Examples/Samples/Sources/ContentViewControllers/AdaptiveLayoutTestViewController.swift @@ -3,7 +3,7 @@ import UIKit import FloatingPanel -final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { +final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { class PanelLayout: FloatingPanelLayout { let position: FloatingPanelPosition = .bottom let initialState: FloatingPanelState = .full @@ -33,20 +33,32 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg } @IBOutlet weak var tableView: IntrinsicTableView! - let cellResuseID = "Cell" + private let cellID = "Cell" override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = UITableView.automaticDimension - tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellResuseID) + 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: cellResuseID, for: indexPath) + 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 @@ -60,14 +72,6 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDeleg func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 40 } - - func numberOfSections(in tableView: UITableView) -> Int { - 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - 50 - } } class IntrinsicTableView: UITableView { From 465218f8f9924f4abba4c8c624f7e4868eed56c4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 15 Jan 2023 09:11:32 +0900 Subject: [PATCH 9/9] Revise doc comments --- Sources/Layout.swift | 2 +- Sources/LayoutAnchoring.swift | 4 ++-- Sources/LayoutProperties.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Layout.swift b/Sources/Layout.swift index 3a6b10b9..78a6b292 100644 --- a/Sources/Layout.swift +++ b/Sources/Layout.swift @@ -92,7 +92,7 @@ class LayoutAdapter { private var staticConstraint: NSLayoutConstraint? - /// A layout constraint to limit the content size on ``FloatingPanelAdaptiveLayoutAnchor``. + /// A layout constraint to limit the content size in ``FloatingPanelAdaptiveLayoutAnchor``. private var contentBoundingConstraint: NSLayoutConstraint? private var anchorStates: Set { diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index a7fd9aae..17e1aa5d 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -167,7 +167,7 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// 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. /// - /// ``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. + /// ``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. @@ -194,7 +194,7 @@ public extension FloatingPanelIntrinsicLayoutAnchor { /// 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. /// - /// ``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. + /// ``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. diff --git a/Sources/LayoutProperties.swift b/Sources/LayoutProperties.swift index ee463373..8f2355c3 100644 --- a/Sources/LayoutProperties.swift +++ b/Sources/LayoutProperties.swift @@ -27,7 +27,7 @@ extension FloatingPanelReferenceEdge { } } -/// A representation to 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