Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
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
2023-04-08 18:37:13 +00:00

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)
}
}
}