This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
859 lines
41 KiB
Swift
859 lines
41 KiB
Swift
@objc public enum NavigationBarDisplayType: Int {
|
|
case backVisible
|
|
case largeTitle
|
|
case centeredLargeTitle // If left, title, and right bar button items exist, center title. Otherwise, revert to `largeTitle` behavior.
|
|
case modal
|
|
case hidden
|
|
}
|
|
|
|
|
|
// This class works in concert w/ `NavigationBarHider` to implement our custom Navigation Bar.
|
|
// This `NavigationBar` class holds the properties and subviews, and `NavigationBarHider` implements the hiding logic when `isInteractiveHidingEnabled` is true.
|
|
|
|
/*
|
|
Navigation bars can have a top spacing, a title, an UnderBarView, and an ExtendedBarView.
|
|
Example of the SavedViewController: The title is "Saved", the underNavigationBarView is the "All articles / Reading lists" picker, and the extendedNavigationBarView is the search bar (when on "all articles") or "+ Create a new list" buton (when on "reading lists").
|
|
|
|
_Main subviews_
|
|
var barTopSpacing: CGFloat
|
|
var title: String?
|
|
func addExtendedNavigationBarView(_ view: UIView)
|
|
func removeExtendedNavigationBarView()
|
|
func addUnderNavigationBarView(_ view: UIView, shouldIgnoreSafeArea: Bool)
|
|
func removeUnderNavigationBarView()
|
|
var topSpacingPercentHidden, navigationBarPercentHidden, underBarViewPercentHidden, extendedViewPercentHidden // Four CGFloats, each setting the percent hidden for the component
|
|
|
|
_Main properties_
|
|
var displayType: NavigationBarDisplayType // see enum above for options
|
|
var isBarHidingEnabled: Bool // Does the NavigationBar scroll away.
|
|
var isUnderBarViewHidingEnabled: Bool // Does the UnderBarView scroll away.
|
|
var isUnderBarFadingEnabled: Bool // Fade out UnderBarView as it hides
|
|
var isExtendedViewHidingEnabled: Bool // Does the extendedView scroll away.
|
|
var isExtendedViewFadingEnabled: Bool // fade out extended view as it hides
|
|
var underBarViewPercentHiddenForShowingTitle: CGFloat // amount of fading before showing title in bar
|
|
var isAdjustingHidingFromContentInsetChangesEnabled: Bool
|
|
var isShadowHidingEnabled: Bool // turn on/off shadow alpha adjusment
|
|
var isTitleShrinkingEnabled: Bool // turn on/off title shrinking
|
|
var isInteractiveHidingEnabled: Bool // turn on/off any interactive adjustment of bar or view visibility
|
|
var isTopSpacingHidingEnabled: Bool
|
|
var shouldTransformUnderBarViewWithBar: Bool = false // hide/show underbar view when bar is hidden/shown
|
|
var delegate: UIViewController? // Set by WMFViewController when this NavBar is initially set up
|
|
|
|
_Detailed styling_
|
|
var backgroundAlpha: CGFloat
|
|
var isShadowBelowUnderBarView: Bool
|
|
var isShadowShowing: Bool
|
|
var shadowAlpha: CGFloat
|
|
var shadowColorKeyPath: KeyPath<Theme, UIColor>
|
|
|
|
_Used only by FakeProgressController_
|
|
func setProgress(_ progress: Float, animated: Bool)
|
|
func setProgressHidden(_ hidden: Bool, animated: Bool)
|
|
var progress: Float
|
|
|
|
_Related to tap targets_
|
|
var allowsUnderbarHitsFallThrough: Bool //if true, this only considers underBarView's subviews for hitTest, not self. Use if you need underlying view controller's scroll view to capture scrolling.
|
|
var allowsExtendedHitsFallThrough: Bool //if true, this only considers extendedView's subviews for hitTest, not self. Use if you need underlying view controller's scroll view to capture scrolling.
|
|
|
|
_See comments near `updateHackyConstraint` for details_
|
|
var needsUnderBarHack: Bool
|
|
func updateHackyConstraint()
|
|
|
|
_Read by other classes to help with their layouts_
|
|
var visibleHeight: CGFloat
|
|
var hiddenHeight: CGFloat
|
|
*/
|
|
@objc(WMFNavigationBar)
|
|
public class NavigationBar: SetupView, FakeProgressReceiving, FakeProgressDelegate {
|
|
fileprivate let statusBarUnderlay: UIView = UIView()
|
|
fileprivate let titleBar: UIToolbar = UIToolbar()
|
|
fileprivate let bar: UINavigationBar = UINavigationBar()
|
|
fileprivate let underBarView: UIView = UIView() // this is always visible below the navigation bar
|
|
fileprivate let extendedView: UIView = UIView()
|
|
fileprivate let shadow: UIView = UIView()
|
|
fileprivate let progressView: UIProgressView = UIProgressView()
|
|
fileprivate let backgroundView: UIView = UIView()
|
|
public var underBarViewPercentHiddenForShowingTitle: CGFloat?
|
|
public var title: String?
|
|
|
|
convenience public init() {
|
|
self.init(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
assert(frame.size != .zero, "Non-zero frame size required to prevent iOS 13 constraint breakage")
|
|
titleBar.frame = bounds
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public var isAdjustingHidingFromContentInsetChangesEnabled: Bool = true
|
|
|
|
public var isShadowHidingEnabled: Bool = false // turn on/off shadow alpha adjusment
|
|
public var isTitleShrinkingEnabled: Bool = false
|
|
public var isInteractiveHidingEnabled: Bool = true // turn on/off any interactive adjustment of bar or view visibility
|
|
@objc public var isShadowBelowUnderBarView: Bool = false {
|
|
didSet {
|
|
updateShadowConstraints()
|
|
}
|
|
}
|
|
public var isShadowShowing: Bool = true {
|
|
didSet {
|
|
updateShadowHeightConstraintConstant()
|
|
}
|
|
}
|
|
|
|
@objc public var isTopSpacingHidingEnabled: Bool = true
|
|
@objc public var isBarHidingEnabled: Bool = true
|
|
@objc public var isUnderBarViewHidingEnabled: Bool = false
|
|
public var isUnderBarFadingEnabled: Bool = true
|
|
@objc public var isExtendedViewHidingEnabled: Bool = false
|
|
@objc public var isExtendedViewFadingEnabled: Bool = true // fade out extended view as it hides
|
|
public var shouldTransformUnderBarViewWithBar: Bool = false // hide/show underbar view when bar is hidden/shown // TODO: change this stupid name
|
|
public var allowsUnderbarHitsFallThrough: Bool = false // if true, this only considers underBarView's subviews for hitTest, not self. Use if you need underlying view controller's scroll view to capture scrolling.
|
|
public var allowsExtendedHitsFallThrough: Bool = false // if true, this only considers extendedView's subviews for hitTest, not self. Use if you need underlying view controller's scroll view to capture scrolling.
|
|
|
|
private var theme = Theme.standard
|
|
|
|
public var shadowColorKeyPath: KeyPath<Theme, UIColor> = \Theme.colors.chromeShadow
|
|
|
|
/// back button presses will be forwarded to this nav controller
|
|
@objc public weak var delegate: UIViewController? {
|
|
didSet {
|
|
updateNavigationItems()
|
|
}
|
|
}
|
|
|
|
private var _displayType: NavigationBarDisplayType = .backVisible
|
|
@objc public var displayType: NavigationBarDisplayType {
|
|
get {
|
|
return _displayType
|
|
}
|
|
set {
|
|
guard newValue != _displayType else {
|
|
return
|
|
}
|
|
_displayType = newValue
|
|
isTitleShrinkingEnabled = _displayType == .largeTitle || _displayType == .centeredLargeTitle
|
|
updateTitleBarConstraints()
|
|
updateNavigationItems()
|
|
updateAccessibilityElements()
|
|
}
|
|
}
|
|
|
|
private func updateAccessibilityElements() {
|
|
let titleElement = (displayType == .largeTitle || displayType == .centeredLargeTitle) ? titleBar : bar
|
|
accessibilityElements = [titleElement, underBarView, extendedView]
|
|
}
|
|
|
|
@objc public func updateNavigationItems() {
|
|
var items: [UINavigationItem] = []
|
|
if displayType == .backVisible, let vc = delegate, let nc = vc.navigationController {
|
|
nc.viewControllers.forEach({ items.append($0.navigationItem) })
|
|
} else if let item = delegate?.navigationItem {
|
|
items.append(item)
|
|
}
|
|
|
|
if (displayType == .largeTitle || displayType == .centeredLargeTitle), let navigationItem = items.last {
|
|
configureTitleBar(with: navigationItem, centerTitle: displayType == .centeredLargeTitle)
|
|
} else {
|
|
bar.setItems([], animated: false)
|
|
bar.setItems(items, animated: false)
|
|
}
|
|
apply(theme: theme)
|
|
}
|
|
|
|
private var cachedTitleViewItem: UIBarButtonItem?
|
|
private var titleView: UIView?
|
|
|
|
private func configureTitleBar(with navigationItem: UINavigationItem, centerTitle: Bool) {
|
|
var titleBarItems: [UIBarButtonItem] = []
|
|
titleView = nil
|
|
|
|
var extractedTitleBarButtonItem: UIBarButtonItem?
|
|
var extractedLeftBarButtonItem: UIBarButtonItem?
|
|
var extractedRightBarButtonItem: UIBarButtonItem?
|
|
|
|
if let titleView = navigationItem.titleView {
|
|
if let cachedTitleViewItem = cachedTitleViewItem {
|
|
extractedTitleBarButtonItem = cachedTitleViewItem
|
|
titleBarItems.append(cachedTitleViewItem)
|
|
} else {
|
|
let titleItem = UIBarButtonItem(customView: titleView)
|
|
extractedTitleBarButtonItem = titleItem
|
|
titleBarItems.append(titleItem)
|
|
cachedTitleViewItem = titleItem
|
|
}
|
|
} else if let title = navigationItem.title {
|
|
let navigationTitleLabel = UILabel()
|
|
navigationTitleLabel.text = title
|
|
navigationTitleLabel.sizeToFit()
|
|
navigationTitleLabel.font = UIFont.wmf_font(.boldTitle1)
|
|
titleView = navigationTitleLabel
|
|
let titleItem = UIBarButtonItem(customView: navigationTitleLabel)
|
|
extractedTitleBarButtonItem = titleItem
|
|
titleBarItems.append(titleItem)
|
|
}
|
|
|
|
titleBarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil))
|
|
|
|
if let item = navigationItem.leftBarButtonItem {
|
|
let leftBarButtonItem = barButtonItem(from: item)
|
|
extractedLeftBarButtonItem = leftBarButtonItem
|
|
titleBarItems.append(leftBarButtonItem)
|
|
}
|
|
|
|
if let item = navigationItem.rightBarButtonItem {
|
|
let rightBarButtonItem = barButtonItem(from: item)
|
|
extractedRightBarButtonItem = rightBarButtonItem
|
|
titleBarItems.append(rightBarButtonItem)
|
|
}
|
|
|
|
// The default `largeTitle` behavior left aligns the title view, which isn't desirable for displaying the Notifications Center bar button in the Explore feed.
|
|
// Center the title element with appropriate flexible space between left and right bar button items, if they exist.
|
|
if centerTitle {
|
|
titleBarItems = []
|
|
|
|
if let extractedLeftBarButtonItem = extractedLeftBarButtonItem {
|
|
titleBarItems.append(extractedLeftBarButtonItem)
|
|
}
|
|
|
|
titleBarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil))
|
|
|
|
if let extractedTitleBarButtonItem = extractedTitleBarButtonItem {
|
|
titleBarItems.append(extractedTitleBarButtonItem)
|
|
}
|
|
|
|
titleBarItems.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil))
|
|
|
|
if let extractedRightBarButtonItem = extractedRightBarButtonItem {
|
|
titleBarItems.append(extractedRightBarButtonItem)
|
|
}
|
|
}
|
|
|
|
titleBar.setItems(titleBarItems, animated: false)
|
|
}
|
|
|
|
// HAX: barButtonItem that we're getting from the navigationItem will not be shown on iOS 11 so we need to recreate it
|
|
private func barButtonItem(from item: UIBarButtonItem) -> UIBarButtonItem {
|
|
let barButtonItem: UIBarButtonItem
|
|
if let title = item.title {
|
|
barButtonItem = UIBarButtonItem(title: title, style: item.style, target: item.target, action: item.action)
|
|
} else if let systemBarButton = item as? SystemBarButton, let systemItem = systemBarButton.systemItem {
|
|
barButtonItem = SystemBarButton(with: systemItem, target: systemBarButton.target, action: systemBarButton.action)
|
|
} else if let customView = item.customView {
|
|
if let customViewData = try? NSKeyedArchiver.archivedData(withRootObject: customView, requiringSecureCoding: false),
|
|
let copiedView = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIView.self, from: customViewData) {
|
|
if let button = customView as? UIButton, let copiedButton = copiedView as? UIButton {
|
|
for target in button.allTargets {
|
|
guard let actions = button.actions(forTarget: target, forControlEvent: .touchUpInside) else {
|
|
continue
|
|
}
|
|
for action in actions {
|
|
copiedButton.addTarget(target, action: Selector(action), for: .touchUpInside)
|
|
}
|
|
}
|
|
}
|
|
barButtonItem = UIBarButtonItem(customView: copiedView)
|
|
} else {
|
|
assert(false, "unable to copy custom view")
|
|
barButtonItem = item
|
|
}
|
|
} else if let image = item.image {
|
|
barButtonItem = UIBarButtonItem(image: image, landscapeImagePhone: item.landscapeImagePhone, style: item.style, target: item.target, action: item.action)
|
|
} else {
|
|
assert(false, "barButtonItem must have title OR be of type SystemBarButton OR have image OR have custom view")
|
|
barButtonItem = item
|
|
}
|
|
barButtonItem.isEnabled = item.isEnabled
|
|
barButtonItem.isAccessibilityElement = item.isAccessibilityElement
|
|
barButtonItem.accessibilityLabel = item.accessibilityLabel
|
|
return barButtonItem
|
|
}
|
|
|
|
private var titleBarHeightConstraint: NSLayoutConstraint!
|
|
|
|
private var underBarViewHeightConstraint: NSLayoutConstraint!
|
|
|
|
private var shadowTopUnderBarViewBottomConstraint: NSLayoutConstraint!
|
|
private var shadowTopExtendedViewBottomConstraint: NSLayoutConstraint!
|
|
|
|
private var shadowHeightConstraint: NSLayoutConstraint!
|
|
private var extendedViewHeightConstraint: NSLayoutConstraint!
|
|
|
|
private lazy var safeAreaUnderBarConstraints = [
|
|
safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: underBarView.leadingAnchor),
|
|
safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: underBarView.trailingAnchor)
|
|
]
|
|
|
|
private lazy var fullWidthUnderBarConstraints = [
|
|
leadingAnchor.constraint(equalTo: underBarView.leadingAnchor),
|
|
trailingAnchor.constraint(equalTo: underBarView.trailingAnchor)
|
|
]
|
|
|
|
private var titleBarTopConstraint: NSLayoutConstraint!
|
|
private var barTopConstraint: NSLayoutConstraint!
|
|
public var barTopSpacing: CGFloat = 0 {
|
|
didSet {
|
|
titleBarTopConstraint.constant = barTopSpacing
|
|
barTopConstraint.constant = barTopSpacing
|
|
setNeedsLayout()
|
|
}
|
|
}
|
|
|
|
private var underBarViewTopBarBottomConstraint: NSLayoutConstraint!
|
|
private var underBarViewTopTitleBarBottomConstraint: NSLayoutConstraint!
|
|
private var underBarViewTopBottomConstraint: NSLayoutConstraint!
|
|
|
|
/// See `updateHackyConstraint` for details
|
|
public var needsUnderBarHack: Bool = false {
|
|
didSet {
|
|
underBarViewTopBarBottomConstraint.constant = (needsUnderBarHack ? -12 : 0)
|
|
}
|
|
}
|
|
|
|
override open func setup() {
|
|
super.setup()
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
|
statusBarUnderlay.translatesAutoresizingMaskIntoConstraints = false
|
|
bar.translatesAutoresizingMaskIntoConstraints = false
|
|
underBarView.translatesAutoresizingMaskIntoConstraints = false
|
|
extendedView.translatesAutoresizingMaskIntoConstraints = false
|
|
progressView.translatesAutoresizingMaskIntoConstraints = false
|
|
progressView.alpha = 0
|
|
shadow.translatesAutoresizingMaskIntoConstraints = false
|
|
titleBar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(backgroundView)
|
|
addSubview(extendedView)
|
|
addSubview(underBarView)
|
|
addSubview(bar)
|
|
addSubview(titleBar)
|
|
addSubview(progressView)
|
|
addSubview(statusBarUnderlay)
|
|
addSubview(shadow)
|
|
|
|
updateAccessibilityElements()
|
|
|
|
bar.delegate = self
|
|
|
|
shadowHeightConstraint = shadow.heightAnchor.constraint(equalToConstant: 0.5)
|
|
shadowHeightConstraint.priority = .defaultHigh
|
|
shadow.addConstraint(shadowHeightConstraint)
|
|
|
|
var updatedConstraints: [NSLayoutConstraint] = []
|
|
|
|
let statusBarUnderlayTopConstraint = topAnchor.constraint(equalTo: statusBarUnderlay.topAnchor)
|
|
updatedConstraints.append(statusBarUnderlayTopConstraint)
|
|
|
|
let statusBarUnderlayBottomConstraint = safeAreaLayoutGuide.topAnchor.constraint(equalTo: statusBarUnderlay.bottomAnchor)
|
|
updatedConstraints.append(statusBarUnderlayBottomConstraint)
|
|
|
|
let statusBarUnderlayLeadingConstraint = leadingAnchor.constraint(equalTo: statusBarUnderlay.leadingAnchor)
|
|
updatedConstraints.append(statusBarUnderlayLeadingConstraint)
|
|
let statusBarUnderlayTrailingConstraint = trailingAnchor.constraint(equalTo: statusBarUnderlay.trailingAnchor)
|
|
updatedConstraints.append(statusBarUnderlayTrailingConstraint)
|
|
|
|
titleBarHeightConstraint = titleBar.heightAnchor.constraint(equalToConstant: 44)
|
|
titleBarHeightConstraint.priority = UILayoutPriority(rawValue: 999)
|
|
titleBar.addConstraint(titleBarHeightConstraint)
|
|
|
|
titleBarTopConstraint = titleBar.topAnchor.constraint(equalTo: statusBarUnderlay.bottomAnchor, constant: barTopSpacing)
|
|
let titleBarLeadingConstraint = leadingAnchor.constraint(equalTo: titleBar.leadingAnchor)
|
|
let titleBarTrailingConstraint = trailingAnchor.constraint(equalTo: titleBar.trailingAnchor)
|
|
|
|
barTopConstraint = bar.topAnchor.constraint(equalTo: statusBarUnderlay.bottomAnchor, constant: barTopSpacing)
|
|
let barLeadingConstraint = leadingAnchor.constraint(equalTo: bar.leadingAnchor)
|
|
let barTrailingConstraint = trailingAnchor.constraint(equalTo: bar.trailingAnchor)
|
|
|
|
underBarViewHeightConstraint = underBarView.heightAnchor.constraint(equalToConstant: 0)
|
|
underBarView.addConstraint(underBarViewHeightConstraint)
|
|
|
|
/// See `updateHackyConstraint` for explanation of the constant on the next line.
|
|
underBarViewTopBarBottomConstraint = bar.bottomAnchor.constraint(equalTo: underBarView.topAnchor, constant: needsUnderBarHack ? -12 : 0)
|
|
underBarViewTopTitleBarBottomConstraint = titleBar.bottomAnchor.constraint(equalTo: underBarView.topAnchor)
|
|
underBarViewTopBottomConstraint = topAnchor.constraint(equalTo: underBarView.topAnchor)
|
|
|
|
extendedViewHeightConstraint = extendedView.heightAnchor.constraint(equalToConstant: 0)
|
|
extendedView.addConstraint(extendedViewHeightConstraint)
|
|
|
|
let extendedViewTopConstraint = underBarView.bottomAnchor.constraint(equalTo: extendedView.topAnchor)
|
|
let extendedViewLeadingConstraint = leadingAnchor.constraint(equalTo: extendedView.leadingAnchor)
|
|
let extendedViewTrailingConstraint = trailingAnchor.constraint(equalTo: extendedView.trailingAnchor)
|
|
let extendedViewBottomConstraint = extendedView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
|
let backgroundViewTopConstraint = topAnchor.constraint(equalTo: backgroundView.topAnchor)
|
|
let backgroundViewLeadingConstraint = leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor)
|
|
let backgroundViewTrailingConstraint = trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor)
|
|
let backgroundViewBottomConstraint = extendedView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor)
|
|
|
|
let progressViewBottomConstraint = shadow.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 1)
|
|
let progressViewLeadingConstraint = leadingAnchor.constraint(equalTo: progressView.leadingAnchor)
|
|
let progressViewTrailingConstraint = trailingAnchor.constraint(equalTo: progressView.trailingAnchor)
|
|
|
|
let shadowLeadingConstraint = leadingAnchor.constraint(equalTo: shadow.leadingAnchor)
|
|
let shadowTrailingConstraint = trailingAnchor.constraint(equalTo: shadow.trailingAnchor)
|
|
|
|
shadowTopExtendedViewBottomConstraint = extendedView.bottomAnchor.constraint(equalTo: shadow.topAnchor)
|
|
shadowTopUnderBarViewBottomConstraint = underBarView.bottomAnchor.constraint(equalTo: shadow.topAnchor)
|
|
|
|
updatedConstraints.append(contentsOf: [titleBarTopConstraint, titleBarLeadingConstraint, titleBarTrailingConstraint, underBarViewTopTitleBarBottomConstraint, barTopConstraint, barLeadingConstraint, barTrailingConstraint, underBarViewTopBarBottomConstraint, underBarViewTopBottomConstraint, extendedViewTopConstraint, extendedViewLeadingConstraint, extendedViewTrailingConstraint, extendedViewBottomConstraint, backgroundViewTopConstraint, backgroundViewLeadingConstraint, backgroundViewTrailingConstraint, backgroundViewBottomConstraint, progressViewBottomConstraint, progressViewLeadingConstraint, progressViewTrailingConstraint, shadowTopUnderBarViewBottomConstraint, shadowTopExtendedViewBottomConstraint, shadowLeadingConstraint, shadowTrailingConstraint])
|
|
updatedConstraints.append(contentsOf: safeAreaUnderBarConstraints)
|
|
addConstraints(updatedConstraints)
|
|
|
|
updateTitleBarConstraints()
|
|
updateShadowConstraints()
|
|
updateShadowHeightConstraintConstant()
|
|
|
|
setNavigationBarPercentHidden(0, underBarViewPercentHidden: 0, extendedViewPercentHidden: 0, topSpacingPercentHidden: 0, animated: false)
|
|
}
|
|
|
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
updateShadowHeightConstraintConstant()
|
|
}
|
|
|
|
/// This function covers for a layout bug in iOS: When presenting a `pageSheet`, the navigation bar doesn't size appropriately. The top of the `underBarView` is appropriately pinned to the bottom of the navigation bar. The navigation bar's content view has a larger height than the actual navigation bar used in the constraint, however, and that causes the weird view: https://github.com/wikimedia/wikipedia-ios/pull/3683#issuecomment-693732339
|
|
/// This is an issue that others experience as well: https://developer.apple.com/forums/thread/121861 , https://stackoverflow.com/questions/57784596/how-to-prevent-gap-between-uinavigationbar-and-view-in-ios-13 , and https://stackoverflow.com/questions/58296535/ios-13-new-pagesheet-formsheet-navigationbar-height .
|
|
/// The layout bug will clear itself if you rotate the screen to landscape, then rotate it back to portrait. This allows it to also look good when the screen loads.
|
|
/// From the links above, others have fixed this by forcing a layout pass on the navigation bar. Unfortunately that doesn't fix it in our case. (Perhaps because our nav bar is on the View Controller and not the Navigation Controller?)
|
|
/// Hopefully some day this and `needsUnderBarHack` can be removed. To test if iOS has fixed it: Set `needsUnderBarHack` to always return `false`, open a modal w/ a presentation style of `pageSheet` and an underbar view (ex: `ArticleAsLivingDocViewController`, and ensure the top of the underbar view is not hidden behind the nav bar.
|
|
public func updateHackyConstraint() {
|
|
if needsUnderBarHack && underBarViewTopBarBottomConstraint.constant != 0 {
|
|
underBarViewTopBarBottomConstraint.constant = 0
|
|
}
|
|
}
|
|
|
|
private func updateShadowHeightConstraintConstant() {
|
|
guard traitCollection.displayScale > 0 else {
|
|
return
|
|
}
|
|
|
|
if !isShadowShowing {
|
|
shadowHeightConstraint.constant = 0
|
|
} else {
|
|
shadowHeightConstraint.constant = 1.0 / traitCollection.displayScale
|
|
}
|
|
}
|
|
|
|
|
|
fileprivate var _topSpacingPercentHidden: CGFloat = 0
|
|
@objc public var topSpacingPercentHidden: CGFloat {
|
|
get {
|
|
return _topSpacingPercentHidden
|
|
}
|
|
set {
|
|
setNavigationBarPercentHidden(_navigationBarPercentHidden, underBarViewPercentHidden: _underBarViewPercentHidden, extendedViewPercentHidden: _extendedViewPercentHidden, topSpacingPercentHidden: newValue, animated: false)
|
|
}
|
|
}
|
|
|
|
fileprivate var _navigationBarPercentHidden: CGFloat = 0
|
|
@objc public var navigationBarPercentHidden: CGFloat {
|
|
get {
|
|
return _navigationBarPercentHidden
|
|
}
|
|
set {
|
|
setNavigationBarPercentHidden(newValue, underBarViewPercentHidden: _underBarViewPercentHidden, extendedViewPercentHidden: _extendedViewPercentHidden, topSpacingPercentHidden: _topSpacingPercentHidden, animated: false)
|
|
}
|
|
}
|
|
|
|
private var _underBarViewPercentHidden: CGFloat = 0
|
|
@objc public var underBarViewPercentHidden: CGFloat {
|
|
get {
|
|
return _underBarViewPercentHidden
|
|
}
|
|
set {
|
|
setNavigationBarPercentHidden(_navigationBarPercentHidden, underBarViewPercentHidden: newValue, extendedViewPercentHidden: _extendedViewPercentHidden, topSpacingPercentHidden: _topSpacingPercentHidden, animated: false)
|
|
}
|
|
}
|
|
|
|
fileprivate var _extendedViewPercentHidden: CGFloat = 0
|
|
@objc public var extendedViewPercentHidden: CGFloat {
|
|
get {
|
|
return _extendedViewPercentHidden
|
|
}
|
|
set {
|
|
setNavigationBarPercentHidden(_navigationBarPercentHidden, underBarViewPercentHidden: _underBarViewPercentHidden, extendedViewPercentHidden: newValue, topSpacingPercentHidden: _topSpacingPercentHidden, animated: false)
|
|
}
|
|
}
|
|
|
|
private var shouldUnderBarIgnoreSafeArea: Bool = false {
|
|
didSet {
|
|
guard shouldUnderBarIgnoreSafeArea != oldValue else {
|
|
return
|
|
}
|
|
|
|
NSLayoutConstraint.deactivate(shouldUnderBarIgnoreSafeArea ? safeAreaUnderBarConstraints : fullWidthUnderBarConstraints)
|
|
NSLayoutConstraint.activate(shouldUnderBarIgnoreSafeArea ? fullWidthUnderBarConstraints : safeAreaUnderBarConstraints)
|
|
}
|
|
}
|
|
|
|
@objc dynamic public var visibleHeight: CGFloat = 0
|
|
@objc public var hiddenHeight: CGFloat = 0
|
|
|
|
public var shadowAlpha: CGFloat {
|
|
get {
|
|
return shadow.alpha
|
|
}
|
|
set {
|
|
shadow.alpha = newValue
|
|
}
|
|
}
|
|
|
|
@objc public func setNavigationBarPercentHidden(_ navigationBarPercentHidden: CGFloat, underBarViewPercentHidden: CGFloat, extendedViewPercentHidden: CGFloat, topSpacingPercentHidden: CGFloat, shadowAlpha: CGFloat = -1, animated: Bool, additionalAnimations: (() -> Void)? = nil) {
|
|
if animated {
|
|
layoutIfNeeded()
|
|
}
|
|
|
|
if isTopSpacingHidingEnabled {
|
|
_topSpacingPercentHidden = topSpacingPercentHidden
|
|
}
|
|
|
|
if isBarHidingEnabled {
|
|
_navigationBarPercentHidden = navigationBarPercentHidden
|
|
}
|
|
|
|
if isUnderBarViewHidingEnabled {
|
|
_underBarViewPercentHidden = underBarViewPercentHidden
|
|
}
|
|
|
|
if isExtendedViewHidingEnabled {
|
|
_extendedViewPercentHidden = extendedViewPercentHidden
|
|
}
|
|
|
|
setNeedsLayout()
|
|
// DDLogDebug("nb: \(navigationBarPercentHidden) ev: \(extendedViewPercentHidden)")
|
|
let applyChanges = {
|
|
let changes = {
|
|
if shadowAlpha >= 0 {
|
|
self.shadowAlpha = shadowAlpha
|
|
}
|
|
if animated {
|
|
self.layoutIfNeeded()
|
|
}
|
|
additionalAnimations?()
|
|
}
|
|
if animated {
|
|
UIView.animate(withDuration: 0.2, animations: changes)
|
|
} else {
|
|
changes()
|
|
}
|
|
}
|
|
|
|
if let underBarViewPercentHiddenForShowingTitle = self.underBarViewPercentHiddenForShowingTitle {
|
|
UIView.animate(withDuration: 0.2, animations: {
|
|
self.delegate?.title = underBarViewPercentHidden >= underBarViewPercentHiddenForShowingTitle ? self.title : nil
|
|
}, completion: { (_) in
|
|
applyChanges()
|
|
})
|
|
} else {
|
|
applyChanges()
|
|
}
|
|
}
|
|
|
|
private func updateTitleBarConstraints() {
|
|
let isUsingTitleBarInsteadOfNavigationBar = (displayType == .largeTitle || displayType == .centeredLargeTitle)
|
|
underBarViewTopTitleBarBottomConstraint.isActive = isUsingTitleBarInsteadOfNavigationBar
|
|
underBarViewTopBarBottomConstraint.isActive = !isUsingTitleBarInsteadOfNavigationBar && displayType != .hidden
|
|
underBarViewTopBottomConstraint.isActive = displayType == .hidden
|
|
bar.isHidden = isUsingTitleBarInsteadOfNavigationBar || displayType == .hidden
|
|
titleBar.isHidden = !isUsingTitleBarInsteadOfNavigationBar || displayType == .hidden
|
|
updateBarTopSpacing()
|
|
setNeedsUpdateConstraints()
|
|
}
|
|
|
|
public override func safeAreaInsetsDidChange() {
|
|
super.safeAreaInsetsDidChange()
|
|
updateBarTopSpacing()
|
|
}
|
|
|
|
// collapse bar top spacing if there's no status bar
|
|
private func updateBarTopSpacing() {
|
|
guard displayType == .largeTitle || displayType == .centeredLargeTitle else {
|
|
barTopSpacing = 0
|
|
return
|
|
}
|
|
let isSafeAreaInsetsTopGreaterThanZero = safeAreaInsets.top > 0
|
|
barTopSpacing = isSafeAreaInsetsTopGreaterThanZero ? 30 : 0
|
|
titleBarHeightConstraint.constant = isSafeAreaInsetsTopGreaterThanZero ? 44 : 32 // it doesn't seem like there's a way to force update of bar metrics - as it stands the bar height gets stuck in whatever mode the app was launched in
|
|
}
|
|
|
|
private func updateShadowConstraints() {
|
|
shadowTopUnderBarViewBottomConstraint.isActive = isShadowBelowUnderBarView
|
|
shadowTopExtendedViewBottomConstraint.isActive = !isShadowBelowUnderBarView
|
|
setNeedsUpdateConstraints()
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var barHeight: CGFloat {
|
|
return (displayType == .largeTitle || displayType == .centeredLargeTitle ? titleBar.frame.height : bar.frame.height)
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var underBarViewHeight: CGFloat {
|
|
return underBarView.frame.size.height
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var extendedViewHeight: CGFloat {
|
|
return extendedView.frame.size.height
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var topSpacingHideableHeight: CGFloat {
|
|
return isTopSpacingHidingEnabled ? barTopSpacing : 0
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var barHideableHeight: CGFloat {
|
|
return isBarHidingEnabled ? barHeight : 0
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var underBarViewHideableHeight: CGFloat {
|
|
return isUnderBarViewHidingEnabled ? underBarViewHeight : 0
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var extendedViewHideableHeight: CGFloat {
|
|
return isExtendedViewHidingEnabled ? extendedViewHeight : 0
|
|
}
|
|
|
|
// Only used by this class and NavigationBarHider
|
|
var hideableHeight: CGFloat {
|
|
return topSpacingHideableHeight + barHideableHeight + underBarViewHideableHeight + extendedViewHideableHeight
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
let navigationBarPercentHidden = _navigationBarPercentHidden
|
|
let extendedViewPercentHidden = _extendedViewPercentHidden
|
|
let underBarViewPercentHidden = _underBarViewPercentHidden
|
|
let topSpacingPercentHidden = safeAreaInsets.top > 0 ? _topSpacingPercentHidden : 1 // treat top spacing as hidden if there's no status bar so that the title is smaller
|
|
|
|
let underBarViewHeight = underBarView.frame.height
|
|
let barHeight = self.barHeight
|
|
let extendedViewHeight = extendedView.frame.height
|
|
|
|
visibleHeight = statusBarUnderlay.frame.size.height + barHeight * (1.0 - navigationBarPercentHidden) + extendedViewHeight * (1.0 - extendedViewPercentHidden) + underBarViewHeight * (1.0 - underBarViewPercentHidden) + (barTopSpacing * (1.0 - topSpacingPercentHidden))
|
|
|
|
let spacingTransformHeight = barTopSpacing * topSpacingPercentHidden
|
|
let barTransformHeight = barHeight * navigationBarPercentHidden + spacingTransformHeight
|
|
let extendedViewTransformHeight = extendedViewHeight * extendedViewPercentHidden
|
|
let underBarTransformHeight = underBarViewHeight * underBarViewPercentHidden
|
|
|
|
hiddenHeight = barTransformHeight + extendedViewTransformHeight + underBarTransformHeight
|
|
|
|
let barTransform = CGAffineTransform(translationX: 0, y: 0 - barTransformHeight)
|
|
let barScaleTransform = CGAffineTransform(scaleX: 1.0 - navigationBarPercentHidden * navigationBarPercentHidden, y: 1.0 - navigationBarPercentHidden * navigationBarPercentHidden)
|
|
|
|
self.bar.transform = barTransform
|
|
self.titleBar.transform = barTransform
|
|
|
|
if isTitleShrinkingEnabled {
|
|
let titleScale: CGFloat = 1.0 - 0.2 * topSpacingPercentHidden
|
|
self.titleView?.transform = CGAffineTransform(scaleX: titleScale, y: titleScale)
|
|
}
|
|
|
|
for subview in self.bar.subviews {
|
|
for subview in subview.subviews {
|
|
subview.transform = barScaleTransform
|
|
}
|
|
}
|
|
self.bar.alpha = min(backgroundAlpha, (1.0 - 2.0 * navigationBarPercentHidden).wmf_normalizedPercentage)
|
|
self.titleBar.alpha = self.bar.alpha
|
|
|
|
let totalTransform = CGAffineTransform(translationX: 0, y: 0 - hiddenHeight)
|
|
self.backgroundView.transform = totalTransform
|
|
|
|
let underBarTransform = CGAffineTransform(translationX: 0, y: 0 - barTransformHeight - underBarTransformHeight)
|
|
self.underBarView.transform = underBarTransform
|
|
|
|
if isUnderBarFadingEnabled {
|
|
self.underBarView.alpha = 1.0 - underBarViewPercentHidden
|
|
}
|
|
|
|
self.extendedView.transform = totalTransform
|
|
|
|
if isExtendedViewFadingEnabled {
|
|
self.extendedView.alpha = min(backgroundAlpha, 1.0 - extendedViewPercentHidden)
|
|
} else {
|
|
self.extendedView.alpha = CGFloat(1).isLessThanOrEqualTo(extendedViewPercentHidden) ? 0 : backgroundAlpha
|
|
}
|
|
|
|
self.progressView.transform = isShadowBelowUnderBarView ? underBarTransform : totalTransform
|
|
self.shadow.transform = isShadowBelowUnderBarView ? underBarTransform : totalTransform
|
|
}
|
|
|
|
|
|
@objc public func setProgressHidden(_ hidden: Bool, animated: Bool) {
|
|
let changes = {
|
|
self.progressView.alpha = min(hidden ? 0 : 1, self.backgroundAlpha)
|
|
}
|
|
if animated {
|
|
UIView.animate(withDuration: 0.2, animations: changes)
|
|
} else {
|
|
changes()
|
|
}
|
|
}
|
|
|
|
@objc public func setProgress(_ progress: Float, animated: Bool) {
|
|
progressView.setProgress(progress, animated: animated)
|
|
}
|
|
|
|
@objc public var progress: Float {
|
|
get {
|
|
return progressView.progress
|
|
}
|
|
set {
|
|
progressView.progress = newValue
|
|
}
|
|
}
|
|
|
|
@objc public func addExtendedNavigationBarView(_ view: UIView) {
|
|
guard extendedView.subviews.first == nil else {
|
|
return
|
|
}
|
|
extendedViewHeightConstraint.isActive = false
|
|
extendedView.wmf_addSubviewWithConstraintsToEdges(view)
|
|
}
|
|
|
|
@objc public func removeExtendedNavigationBarView() {
|
|
guard let subview = extendedView.subviews.first else {
|
|
return
|
|
}
|
|
subview.removeFromSuperview()
|
|
extendedViewHeightConstraint.isActive = true
|
|
}
|
|
|
|
@objc public func addUnderNavigationBarView(_ view: UIView, shouldIgnoreSafeArea: Bool = false) {
|
|
guard underBarView.subviews.first == nil else {
|
|
return
|
|
}
|
|
underBarViewHeightConstraint.isActive = false
|
|
shouldUnderBarIgnoreSafeArea = shouldIgnoreSafeArea
|
|
underBarView.wmf_addSubviewWithConstraintsToEdges(view)
|
|
}
|
|
|
|
@objc public func removeUnderNavigationBarView() {
|
|
guard let subview = underBarView.subviews.first else {
|
|
return
|
|
}
|
|
subview.removeFromSuperview()
|
|
underBarViewHeightConstraint.isActive = true
|
|
}
|
|
|
|
@objc public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
|
|
if allowsUnderbarHitsFallThrough
|
|
&& underBarView.frame.contains(point)
|
|
&& !bar.frame.contains(point) {
|
|
|
|
for subview in underBarView.subviews {
|
|
let convertedPoint = self.convert(point, to: subview)
|
|
if subview.point(inside: convertedPoint, with: event) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
if allowsExtendedHitsFallThrough
|
|
&& extendedView.frame.contains(point)
|
|
&& !bar.frame.contains(point) {
|
|
|
|
for subview in extendedView.subviews {
|
|
let convertedPoint = self.convert(point, to: subview)
|
|
if subview.point(inside: convertedPoint, with: event) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return point.y <= visibleHeight
|
|
}
|
|
|
|
public var backgroundAlpha: CGFloat = 1 {
|
|
didSet {
|
|
statusBarUnderlay.alpha = backgroundAlpha
|
|
backgroundView.alpha = backgroundAlpha
|
|
bar.alpha = backgroundAlpha
|
|
titleBar.alpha = backgroundAlpha
|
|
extendedView.alpha = backgroundAlpha
|
|
if backgroundAlpha < progressView.alpha {
|
|
progressView.alpha = backgroundAlpha
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NavigationBar: Themeable {
|
|
public func apply(theme: Theme) {
|
|
self.theme = theme
|
|
|
|
backgroundColor = .clear
|
|
|
|
statusBarUnderlay.backgroundColor = theme.colors.paperBackground
|
|
backgroundView.backgroundColor = theme.colors.paperBackground
|
|
|
|
titleBar.setBackgroundImage(theme.navigationBarBackgroundImage, forToolbarPosition: .any, barMetrics: .default)
|
|
titleBar.isTranslucent = false
|
|
titleBar.tintColor = theme.colors.chromeText
|
|
titleBar.setShadowImage(theme.navigationBarShadowImage, forToolbarPosition: .any)
|
|
titleBar.barTintColor = theme.colors.chromeBackground
|
|
if let items = titleBar.items {
|
|
for item in items {
|
|
if let label = item.customView as? UILabel {
|
|
label.textColor = theme.colors.chromeText
|
|
} else if item.image == nil {
|
|
item.tintColor = theme.colors.link
|
|
}
|
|
}
|
|
}
|
|
|
|
bar.setBackgroundImage(theme.navigationBarBackgroundImage, for: .default)
|
|
bar.titleTextAttributes = theme.navigationBarTitleTextAttributes
|
|
bar.isTranslucent = false
|
|
bar.barTintColor = theme.colors.chromeBackground
|
|
bar.shadowImage = theme.navigationBarShadowImage
|
|
bar.tintColor = theme.colors.chromeText
|
|
|
|
extendedView.backgroundColor = .clear
|
|
underBarView.backgroundColor = .clear
|
|
|
|
shadow.backgroundColor = theme[keyPath: shadowColorKeyPath]
|
|
|
|
progressView.progressViewStyle = .bar
|
|
progressView.trackTintColor = .clear
|
|
progressView.progressTintColor = theme.colors.link
|
|
}
|
|
}
|
|
|
|
extension NavigationBar: UINavigationBarDelegate {
|
|
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
|
|
delegate?.navigationController?.popViewController(animated: true)
|
|
return false
|
|
}
|
|
|
|
public func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
|
|
/// During iOS 14's long press to access back history, this function is called *after* the unneeded navigationItems have been popped off.
|
|
/// However, with our custom navBar the actual articleVC isn't changed. So we need to find the articleVC for the top navItem, and pop to it.
|
|
/// This should be in `shouldPop`, but as of iOS 14.0, `shouldPop` isn't called when long pressing a back button. Once this is fixed by Apple,
|
|
/// we should move this to `shouldPop` to improve animations. (Update: A bug tracker was filed w/ Apple, and this won't be fixed anytime soon.
|
|
/// Apple: "This is expected behavior. Due to side effects that many clients have in the shouldPop handler, we do not consult it when using the back
|
|
/// button menu. We instead recommend that you hide the back button when you wish to disallow popping past a particular point in the navigation stack.")
|
|
if let topNavigationItem = navigationBar.items?.last,
|
|
let navController = delegate?.navigationController,
|
|
let tappedViewController = navController.viewControllers.first(where: {$0.navigationItem == topNavigationItem}) {
|
|
delegate?.navigationController?.popToViewController(tappedViewController, animated: true)
|
|
}
|
|
}
|
|
}
|