Bir kütüphane yayınladımAşağıdaki cevabım temelinde .
Kısayollar uygulama katmanını taklit eder. Bkz bu yazıyı bakın.
Kütüphanenin ana bileşeni OverlayContainerViewController
. Bir görünüm denetleyicisinin altındaki içeriği gizleyerek veya göstererek aşağı ve yukarı sürüklenebileceği bir alan tanımlar.
let contentController = MapsViewController()
let overlayController = SearchViewController()
let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
contentController,
overlayController
]
window?.rootViewController = containerController
OverlayContainerViewControllerDelegate
İstenen çentik sayısını belirtmek için uygulayın :
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
}
}
Önceki cevap
Önerilen çözümlerde ele alınmayan önemli bir nokta olduğunu düşünüyorum: kaydırma ve çeviri arasındaki geçiş.
Haritalar'da, fark etmiş olabileceğiniz gibi, tableView ulaştığında contentOffset.y == 0
, alt sayfa yukarı kayar veya aşağı iner.
Nokta zordur, çünkü kaydırma hareketimiz çeviriyi başlattığında kaydırma işlemini etkinleştiremez / devre dışı bırakamazız. Yeni bir dokunuş başlayana kadar kaydırma durur. Burada önerilen çözümlerin çoğunda durum böyledir.
İşte bu hareketi uygulamaya çalışıyorum.
Başlangıç noktası: Haritalar Uygulaması
Soruşturmayı başlatmak için, hadi Maps görünüm hiyerarşisi görselleştirmek (bir simülatörde Maps başlatmak ve seçmek Debug
> Attach to process by PID or Name
> Maps
Xcode 9'da).
Hareketin nasıl işlediğini söylemiyor, ama onun mantığını anlamama yardımcı oldu. Lldb ve görünüm hiyerarşisi hata ayıklayıcısı ile oynayabilirsiniz.
Görünüm denetleyici yığınlarımız
Maps ViewController mimarisinin temel bir sürümünü oluşturalım.
Bir BackgroundViewController
(harita görünümümüz) ile başlıyoruz :
class BackgroundViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
}
TableView öğesini özel bir alana koyduk UIViewController
:
class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView = UITableView()
override func loadView() {
view = tableView
tableView.dataSource = self
tableView.delegate = self
}
[...]
}
Şimdi kaplamayı yerleştirmek ve çevirisini yönetmek için bir VC'ye ihtiyacımız var. Sorunu basitleştirmek için, bindirmeyi bir statik noktadan OverlayPosition.maximum
diğerine çevirebileceğini düşünüyoruzOverlayPosition.minimum
.
Şimdilik, pozisyon değişikliğini canlandırmak için sadece bir genel yöntemi var ve şeffaf bir görünüme sahip:
enum OverlayPosition {
case maximum, minimum
}
class OverlayContainerViewController: UIViewController {
let overlayViewController: OverlayViewController
var translatedViewHeightContraint = ...
override func loadView() {
view = UIView()
}
func moveOverlay(to position: OverlayPosition) {
[...]
}
}
Son olarak, hepsini gömmek için bir ViewController'a ihtiyacımız var:
class StackViewController: UIViewController {
private var viewControllers: [UIViewController]
override func viewDidLoad() {
super.viewDidLoad()
viewControllers.forEach { gz_addChild($0, in: view) }
}
}
AppDelegate'imizde başlangıç dizimiz şöyle:
let overlay = OverlayViewController()
let containerViewController = OverlayContainerViewController(overlayViewController: overlay)
let backgroundViewController = BackgroundViewController()
window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController])
Yer paylaşımlı çevirinin arkasındaki zorluk
Şimdi, bindirmemizi nasıl çevirebiliriz?
Önerilen çözümlerin çoğunda özel bir pan jest tanıyıcı kullanılır, ancak aslında zaten bir tane var: tablo görünümünün pan hareketi. Dahası, kaydırma ve çeviriyi senkronize tutmamız gerekiyor ve ihtiyacımız olan UIScrollViewDelegate
tüm olaylar var!
Saf bir uygulama ikinci bir kaydırma hareketi contentOffset
kullanır ve çeviri gerçekleştiğinde tablo görünümünü sıfırlamaya çalışır :
func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
if isTranslating {
tableView.contentOffset = .zero
}
}
Ama bu çalışmıyor. TableView onun günceller contentOffset
zaman kendi tava jest tanıyıcı eylem tetikler veya DisplayLink geri arama çağrıldığında. Tanıyıcımızın başarılı bir şekilde geçersiz kılma işleminden hemen sonra tetikleme şansı yoktur contentOffset
. Tek şansımız ya yerleşim aşamasının bir parçası olmak ( layoutSubviews
kaydırma görünümünün her karesinde kaydırma görünümü çağrılarını geçersiz kılmak suretiyle) ya da didScroll
her contentOffset
değiştirildiğinde çağrılan temsilci yöntemine yanıt vermektir. Bunu deneyelim.
Çeviri Uygulaması
OverlayVC
Scrollview'ın olaylarını çeviri işleyicimize göndermek için bir temsilci ekliyoruz 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)
}
}
Konteynırımızda çeviriyi bir numaralandırma kullanarak takip ediyoruz:
enum OverlayInFlightPosition {
case minimum
case maximum
case progressing
}
Mevcut konum hesaplaması aşağıdaki gibidir:
private var overlayInFlightPosition: OverlayInFlightPosition {
let height = translatedViewHeightContraint.constant
if height == maximumHeight {
return .maximum
} else if height == minimumHeight {
return .minimum
} else {
return .progressing
}
}
Çeviriyi ele almak için 3 yönteme ihtiyacımız var:
İlki bize çeviriye başlamamız gerekip gerekmediğini söyler.
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
}
}
İkincisi çeviriyi yapar. translation(in:)
ScrollView'in pan hareketinin yöntemini kullanır .
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)
)
}
Üçüncüsü, kullanıcı parmağını serbest bıraktığında çevirinin sonunu canlandırır. Pozisyonu, görünümün hızını ve mevcut konumunu kullanarak hesaplıyoruz.
private func animateTranslationEnd() {
let position: OverlayPosition = // ... calculation based on the current overlay position & velocity
moveOverlay(to: position)
}
Yer paylaşımımızın delege uygulaması şöyle görünür:
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()
}
}
Son sorun: yer paylaşımı kabının dokunuşlarını gönderme
Çeviri artık oldukça etkili. Ancak yine de son bir sorun var: dokunuşlar arka plan görünümümüze sunulmuyor. Bunların hepsi, yer paylaşımı kabının görünümü tarafından engellenir. Biz ayarlayamıyor isUserInteractionEnabled
için false
de bizim tablo görünümünde etkileşimi devre dışı çünkü. Çözüm, Haritalar uygulamasında büyük ölçüde kullanılan çözümdür 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
}
}
Kendini cevaplayıcı zincirinden çıkarır.
İçinde OverlayContainerViewController
:
override func loadView() {
view = PassThroughView()
}
Sonuç
İşte sonuç:
Kodu burada bulabilirsiniz .
Herhangi bir hata görürseniz lütfen bana bildirin! Uygulamanızın, özellikle yer paylaşımınıza bir başlık eklerseniz, ikinci bir kaydırma hareketi kullanabileceğini unutmayın.
Güncelleme 23/08/18
Kullanıcı sürüklemeyi bitirdiğinde / kaydırma yerine / scrollViewDidEndDragging
ile
değiştirebiliriz :willEndScrollingWithVelocity
enabling
disabling
func scrollView(_ scrollView: UIScrollView,
willEndScrollingWithVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
switch overlayInFlightPosition {
case .maximum:
break
case .minimum, .progressing:
targetContentOffset.pointee = .zero
}
animateTranslationEnd(following: scrollView)
}
Hareket akışını daha iyi hale getirmek için bir yay animasyonu kullanabilir ve animasyon yaparken kullanıcı etkileşimi sağlayabiliriz:
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)
}