Files
FloatingPanel/Framework/Sources/FloatingPanelLayout.swift
T
Shin Yamamoto 66a8ca36e4 Fix FloatingPanelIntrinsicLayout
- `.full` position's height must be the intrinsic height.
- Work it for a safe area bottom anchor
- Remove FloatingPanelIntrinsicLayout.contentViewController because it
isn't actually needed
2018-12-07 14:37:48 +09:00

381 lines
14 KiB
Swift

//
// Created by Shin Yamamoto on 2018/09/27.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
/// FloatingPanelIntrinsicLayout
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for full position because the inset is determined automatically.
/// You can customize insets only for half, tip and hidden positions
/// on FloatingPanelIntrinsicLayout.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns all position exepct for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
var topInteractionBuffer: CGFloat { get }
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each positiion.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
/// You must not include any Y-axis and height layout constraints of the surface view
/// because their constraints will be configured by the floating panel controller.
/// By default, the width of a surface view fits a safe area.
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Returns a CGFloat value to determine the backdrop view's alpha for a position.
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
}
public extension FloatingPanelLayout {
var topInteractionBuffer: CGFloat { return 6.0 }
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set([.full, .half, .tip])
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .half
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
}
class FloatingPanelLayoutAdapter {
weak var vc: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
var layout: FloatingPanelLayout
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
if oldValue != safeAreaInsets {
updateHeight()
}
}
}
private var heightBuffer: CGFloat = 88.0 // For bounce
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var heightConstraints: [NSLayoutConstraint] = []
private var fullInset: CGFloat {
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
}
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topY: CGFloat {
if supportedPositions.contains(.full) {
if layout is FloatingPanelIntrinsicLayout {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + fullInset)
} else {
return (safeAreaInsets.top + fullInset)
}
} else {
return middleY
}
}
var middleY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
}
var bottomY: CGFloat {
if supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
}
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: safeAreaInsets.bottom,
right: 0.0)
}
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
return topY
case .half:
return middleY
case .tip:
return bottomY
case .hidden:
return hiddenY
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func updateIntrinsicHeight() {
let fittingSize = UIView.layoutFittingCompressedSize
intrinsicHeight = surfaceView.contentView.systemLayoutSizeFitting(fittingSize).height
}
func prepareLayout(in vc: UIViewController) {
self.vc = vc
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
} else {
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: fullInset),
]
}
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: -hiddenInset),
]
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
guard let vc = vc else { return }
defer {
UIView.performWithoutAnimation {
surfaceView.superview!.layoutIfNeeded()
}
}
NSLayoutConstraint.deactivate(heightConstraints)
let height: CGFloat
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
height = intrinsicHeight + safeAreaInsets.bottom
} else {
// Must use the`vc` height, not the screen height because safe area insets
// of `vc` are relative values. For example, a view controller in
// Navigation controller's safe area insets and frame can be changed whether
// the navigation bar is translucent or not.
height = vc.view.bounds.height - (safeAreaInsets.top + fullInset)
}
heightConstraints = [
surfaceView.heightAnchor.constraint(equalToConstant: height)
]
NSLayoutConstraint.activate(heightConstraints)
surfaceView.set(bottomOverflow: heightBuffer + layout.topInteractionBuffer)
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
NSLayoutConstraint.activate(fullConstraints)
}
}
func activateLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
NSLayoutConstraint.activate(fixedConstraints)
if supportedPositions.union([.hidden]).contains(state) == false {
state = layout.initialPosition
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
switch state {
case .full:
NSLayoutConstraint.deactivate(halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.deactivate(fullConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
}
}
func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
}
}
func checkLayoutConsistance() {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}
}
}