Ich habe eine Bibliothek basierend auf meiner Antwort unten veröffentlicht.
Es ahmt das Overlay der Shortcuts-Anwendung nach. Weitere Informationen finden Sie in diesem Artikel .
Die Hauptkomponente der Bibliothek ist die OverlayContainerViewController
. Es definiert einen Bereich, in dem ein Ansichts-Controller nach oben und unten gezogen werden kann, um den darunter liegenden Inhalt auszublenden oder anzuzeigen.
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
Implementieren Sie OverlayContainerViewControllerDelegate
, um die Anzahl der gewünschten Kerben anzugeben:
enum OverlayNotch: Int, CaseIterable {
case minimum, medium, maximum
}
func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
return OverlayNotch.allCases.count
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
heightForNotchAt index: Int,
availableSpace: CGFloat) -> CGFloat {
switch OverlayNotch.allCases[index] {
case .maximum:
return availableSpace * 3 / 4
case .medium:
return availableSpace / 2
case .minimum:
return availableSpace * 1 / 4
}
}
Vorherige Antwort
Ich denke, es gibt einen wichtigen Punkt, der in den vorgeschlagenen Lösungen nicht behandelt wird: den Übergang zwischen der Schriftrolle und der Übersetzung.
Wie Sie vielleicht bemerkt haben, gleitet contentOffset.y == 0
das untere Blatt in Karten beim Erreichen der Tabellenansicht entweder nach oben oder nach unten.
Der Punkt ist schwierig, da wir den Bildlauf nicht einfach aktivieren / deaktivieren können, wenn unsere Schwenkgeste mit der Übersetzung beginnt. Es würde die Schriftrolle stoppen, bis eine neue Berührung beginnt. Dies ist bei den meisten hier vorgeschlagenen Lösungen der Fall.
Hier ist mein Versuch, diesen Antrag umzusetzen.
Ausgangspunkt: Karten App
Um unsere Untersuchung zu beginnen, lassen Sie uns die Ansicht Hierarchie der Karten zu visualisieren (Start Maps auf einem Simulator und wählen Sie Debug
> Attach to process by PID or Name
> Maps
in Xcode 9).
Es sagt nicht, wie die Bewegung funktioniert, aber es hat mir geholfen, die Logik zu verstehen. Sie können mit dem lldb- und dem Ansichtshierarchie-Debugger spielen.
Unser View Controller stapelt sich
Erstellen wir eine Basisversion der Maps ViewController-Architektur.
Wir beginnen mit einem BackgroundViewController
(unserer Kartenansicht):
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
Wir haben die tableView in eine dedizierte UIViewController
:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
Jetzt benötigen wir einen VC, um das Overlay einzubetten und seine Übersetzung zu verwalten. Um das Problem zu vereinfachen, können wir die Überlagerung von einem statischen Punkt OverlayPosition.maximum
auf einen anderen verschieben OverlayPosition.minimum
.
Derzeit gibt es nur eine öffentliche Methode zum Animieren der Positionsänderung und eine transparente Ansicht:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
Schließlich benötigen wir einen ViewController, um alles einzubetten:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
In unserem AppDelegate sieht unsere Startsequenz folgendermaßen aus:
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
Die Schwierigkeit hinter der Overlay-Übersetzung
Wie übersetzen wir nun unser Overlay?
Die meisten der vorgeschlagenen Lösungen verwenden einen dedizierten Pan-Gesten-Erkenner, aber wir haben bereits einen: die Pan-Geste der Tabellenansicht. Außerdem müssen wir die Schriftrolle und die Übersetzung synchron halten und UIScrollViewDelegate
alle Ereignisse haben, die wir brauchen!
Eine naive Implementierung würde eine zweite Schwenkgeste verwenden und versuchen, contentOffset
die Tabellenansicht zurückzusetzen, wenn die Übersetzung erfolgt:
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
Aber es funktioniert nicht. Die tableView wird aktualisiert, contentOffset
wenn eine eigene Pan-Gestenerkennungsaktion ausgelöst wird oder wenn der displayLink-Rückruf aufgerufen wird. Es besteht keine Chance, dass unser Erkenner direkt nach diesen ausgelöst wird, um das erfolgreich zu überschreiben contentOffset
. Unsere einzige Möglichkeit besteht darin, entweder an der Layoutphase teilzunehmen (indem layoutSubviews
die Aufrufe der Bildlaufansicht in jedem Frame der Bildlaufansicht überschrieben werden ) oder auf die didScroll
Methode des Delegaten zu reagieren, die bei jeder contentOffset
Änderung aufgerufen wird. Lass es uns versuchen.
Die Übersetzung Implementierung
Wir fügen OverlayVC
unserem einen Übersetzer hinzu, um die Ereignisse der Bildlaufansicht an unseren Übersetzer zu senden OverlayContainerViewController
:
protocol OverlayViewControllerDelegate: class {
func scrollViewDidScroll(_ scrollView: UIScrollView)
func scrollViewDidStopScrolling(_ scrollView: UIScrollView)
}
class OverlayViewController: UIViewController {
[...]
func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.scrollViewDidScroll(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
delegate?.scrollViewDidStopScrolling(scrollView)
}
}
In unserem Container verfolgen wir die Übersetzung anhand einer Aufzählung:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
Die aktuelle Positionsberechnung sieht folgendermaßen aus:
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
Wir benötigen 3 Methoden, um die Übersetzung zu handhaben:
Der erste sagt uns, ob wir mit der Übersetzung beginnen müssen.
private func shouldTranslateView(following scrollView: UIScrollView) -> Bool {
guard scrollView.isTracking else { return false }
let offset = scrollView.contentOffset.y
switch overlayInFlightPosition {
case .maximum:
return offset < 0
case .minimum:
return offset > 0
case .progressing:
return true
}
}
Der zweite führt die Übersetzung durch. Es verwendet die translation(in:)
Methode der Schwenkgeste von scrollView.
private func translateView(following scrollView: UIScrollView) {
scrollView.contentOffset = .zero
let translation = translatedViewTargetHeight - scrollView.panGestureRecognizer.translation(in: view).y
translatedViewHeightContraint.constant = max(
Constant.minimumHeight,
min(translation, Constant.maximumHeight)
)
}
Der dritte animiert das Ende der Übersetzung, wenn der Benutzer seinen Finger loslässt. Wir berechnen die Position anhand der Geschwindigkeit und der aktuellen Position der Ansicht.
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
Die Delegiertenimplementierung unseres Overlays sieht einfach so aus:
class OverlayContainerViewController: UIViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard shouldTranslateView(following: scrollView) else { return }
translateView(following: scrollView)
}
func scrollViewDidStopScrolling(_ scrollView: UIScrollView) {
// prevent scroll animation when the translation animation ends
scrollView.isEnabled = false
scrollView.isEnabled = true
animateTranslationEnd()
}
}
Letztes Problem: Versenden der Berührungen des Overlay-Containers
Die Übersetzung ist jetzt ziemlich effizient. Es gibt jedoch noch ein letztes Problem: Die Berührungen werden nicht in unsere Hintergrundansicht übertragen. Sie werden alle von der Ansicht des Overlay-Containers abgefangen. Wir können nicht festlegen isUserInteractionEnabled
, false
da dies auch die Interaktion in unserer Tabellenansicht deaktivieren würde. Die Lösung wird massiv in der Maps-App verwendet PassThroughView
:
class PassThroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self {
return nil
}
return view
}
}
Es entfernt sich von der Responderkette.
In OverlayContainerViewController
:
override func loadView() {
view = PassThroughView()
}
Ergebnis
Hier ist das Ergebnis:
Den Code finden Sie hier .
Bitte, wenn Sie irgendwelche Fehler sehen, lassen Sie es mich wissen! Beachten Sie, dass Ihre Implementierung natürlich eine zweite Schwenkgeste verwenden kann, insbesondere wenn Sie Ihrem Overlay einen Header hinzufügen.
Update 23/08/18
Wir können ersetzen scrollViewDidEndDragging
mit
willEndScrollingWithVelocity
anstatt enabling
/ disabling
Scroll , wenn der Benutzer Enden ziehen:
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
Wir können eine Federanimation verwenden und Benutzerinteraktion während der Animation ermöglichen, um den Bewegungsfluss zu verbessern:
func moveOverlay(to position: OverlayPosition,
duration: TimeInterval,
velocity: CGPoint) {
overlayPosition = position
translatedViewHeightContraint.constant = translatedViewTargetHeight
UIView.animate(
withDuration: duration,
delay: 0,
usingSpringWithDamping: velocity.y == 0 ? 1 : 0.6,
initialSpringVelocity: abs(velocity.y),
options: [.allowUserInteraction],
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}