Jangan muncul! Transisi Interruptable di iOS

Apakah Anda juga kesal oleh pop-up dalam aplikasi? Pada artikel ini saya akan menunjukkan cara menyembunyikan dan menampilkan pop-up secara interaktif, membuat animasi dapat terputus dan tidak membuat marah klien saya.



Dalam artikel sebelumnya, saya melihat bagaimana Anda dapat menghidupkan tampilan controller baru.


Kami sepakat pada kenyataan bahwa viewController dapat menampilkan dan menyembunyikan animasi:



Sekarang kita akan mengajar dia untuk menanggapi gerakan penyembunyian.


Transisi Interaktif


Tambahkan gerakan dekat


Untuk mengajarkan pengontrol untuk menutup secara interaktif, Anda perlu menambahkan gerakan dan memprosesnya. Semua pekerjaan akan berada di kelas TransitionDriver :


 class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? } 

Anda dapat melampirkan handler di lokasi DimmPresentationController, di dalam PanelTransition:


 private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController } 

Pada saat yang sama, Anda perlu menunjukkan bahwa menyembunyikan telah dapat dikelola (kami sudah melakukan ini di artikel terakhir):


 // PanelTransition.swift func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

Tangani gerakan itu


Mari kita mulai dengan gerakan penutup: jika Anda menyeret panel ke bawah, animasi penutup akan dimulai, dan gerakan jari akan memengaruhi tingkat penutupan.
UIPercentDrivenInteractiveTransition memungkinkan UIPercentDrivenInteractiveTransition untuk menangkap animasi transisi dan mengendalikannya secara manual. Ini telah update , finish , cancel metode. Lebih mudah untuk melakukan pemrosesan gerakan di subkelasnya.


Pemrosesan gerakan


 private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() //   percentComplete   0 let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } case .changed: update(percentComplete + r.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } 

.begin
Mulai pemisahan dengan cara yang paling umum. Kami menyimpan tautan ke pengontrol di metode link(to:)


.changed
Hitung kenaikannya dan berikan ke metode update . Nilai yang diterima dapat bervariasi dari 0 hingga 1, jadi kami akan mengontrol tingkat penyelesaian animasi dari interactionControllerForDismissal(using:) metode interactionControllerForDismissal(using:) . Perhitungan dilakukan dalam ekstensi gerakan, sehingga kode menjadi lebih bersih.


Perhitungan gerakan
 private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } } 

Perhitungan didasarkan pada maxTranslation , kami menghitungnya sebagai ketinggian pengontrol yang ditampilkan:


 var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 } 

.end
Kami melihat kelengkapan isyarat itu. Aturan penyelesaian: jika lebih dari setengahnya telah bergeser, maka tutuplah. Dalam hal ini, offset harus dipertimbangkan tidak hanya oleh koordinat saat ini, tetapi juga oleh velocity . Jadi, kami memahami maksud pengguna: ia mungkin tidak menyelesaikan hingga bagian tengah, tetapi menggesek ke bawah sangat banyak. Atau sebaliknya: turun, tetapi geser ke atas untuk kembali.


Perhitungan Proyeksi Lokasi
 private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat { // Velocity value func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat { // Magic formula from WWDC let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000 return self * multiplier } } extension CGPoint { static func +(left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y) } } 

.cancelled - akan terjadi jika Anda mengunci layar ponsel atau jika mereka memanggil. Anda bisa menanganinya sebagai blok .ended atau membatalkan tindakan.
.failed - akan terjadi jika gerakan dibatalkan oleh gerakan lain. Jadi, misalnya, gerakan seret dapat membatalkan gerakan ketukan.
.possible - keadaan awal gerakan, biasanya tidak memerlukan banyak pekerjaan.


Sekarang panel juga dapat ditutup dengan gesek, tetapi tombol dismiss telah dismiss . Ini terjadi karena ada properti wantsInteractiveStart di TransitionDriver , secara default memang true . Ini normal untuk sapuan, tapi itu memblokir dismiss biasa.


Pertimbangkan perilaku berdasarkan keadaan gerakan. Jika isyarat dimulai, maka ini adalah penutupan interaktif, dan jika itu tidak dimulai, maka yang biasa:


 override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } } 

Sekarang pengguna dapat mengontrol menyembunyikan:



Transisi Interupsi


Misalkan kita mulai menutup kartu kita, tetapi berubah pikiran dan ingin kembali. Sederhana: dalam keadaan .began , .began memanggil pause() untuk berhenti.


Tetapi Anda perlu memisahkan dua skenario:


  • ketika kita mulai bersembunyi dari gerakan;
  • ketika kita menyela yang saat ini.

Untuk melakukan ini, setelah berhenti, periksa percentComplete: jika 0, maka kita mulai menutup kartu secara manual, ditambah kita perlu memanggil dismiss . Jika bukan 0, maka persembunyiannya sudah dimulai, cukup untuk menghentikan animasinya saja:


 case .began: pause() // Pause allows to detect percentComplete if percentComplete == 0 { presentedController?.dismiss(animated: true) } 

Saya menekan tombol dan segera geser ke atas untuk membatalkan sembunyikan:


Berhenti menampilkan pengontrol


Situasi sebaliknya: kartu mulai muncul, tetapi kita tidak membutuhkannya. Kami menangkapnya dan mengirimkannya dengan menggesek ke bawah. Anda dapat menghentikan animasi tampilan pengontrol dalam langkah yang sama.


Kembalikan driver sebagai pengontrol tampilan interaktif:


 func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

Memproses gerakan, tetapi dengan bias terbalik dan nilai kelengkapan:


 private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } } 

Untuk memisahkan acara dan sembunyikan, saya memasukkan enum dengan arah animasi saat ini:


 enum TransitionDirection { case present, dismiss } 

Properti disimpan di TransitionDriver dan memengaruhi gesture handler mana yang akan digunakan:


 var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } } 

Ini juga memengaruhi wantsInteractiveStart . Kami tidak berencana menampilkan pengontrol dengan gerakan, jadi kami mengembalikan false untuk .present :


 override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } } 

Nah, itu tetap mengubah arah gerakan ketika controller sepenuhnya ditampilkan. Tempat terbaik di PresentationController :


 override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } } 

Apakah mungkin tanpa enum?

Tampaknya kita dapat mengandalkan properti dari controller isBeingPresented dan isBeingDismissed . Tetapi mereka hanya menunjukkan prosesnya, dan kita juga membutuhkan arahan yang mungkin: pada awal penutupan interaktif, kedua nilai akan false , dan kita sudah perlu tahu bahwa ini adalah arah untuk menutup. Ini dapat diselesaikan dengan kondisi tambahan untuk memeriksa hierarki pengendali, tetapi penugasan eksplisit melalui enum tampaknya menjadi solusi yang lebih sederhana.


Sekarang Anda dapat mengganggu animasi pertunjukan. Saya menekan tombol dan segera menggesek ke bawah:



Tampilkan dengan gerakan


Jika Anda membuat menu hamburger untuk suatu aplikasi, kemungkinan besar Anda ingin menampilkannya dengan gerakan. Ini berfungsi seperti persembunyian interaktif, tetapi dalam gerakan, alih-alih dismiss menyebutnya present .
Mari kita mulai dari akhir. Di handlePresentation(recognizer:) tampilkan controller:


 case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } 

Mari kita tunjukkan secara interaktif:


 override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … } 

Agar kode berfungsi, tidak ada cukup tautan ke presentingController dan presentedController . Kami akan meneruskannya saat membuat gerakan, tambahkan UIScreenEdgePanGestureRecognizer :


 func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController //    panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController.view.addGestureRecognizer(panRecognizer!) //    screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePresentation(recognizer:))) screenEdgePanRecognizer!.edges = .bottom presentingController.view.addGestureRecognizer(screenEdgePanRecognizer!) } 

Anda dapat mentransfer pengontrol saat membuat PanelTransition :


 class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() } 

Tetap membuat PanelTransition :


  1. Mari buat pengontrol child di viewDidLoad , karena kita mungkin memerlukan pengontrol kapan saja.
  2. Buat PanelTransition . Dalam konstruktornya, gerakan terikat ke controller.
  3. Letakkan delegasi Transisi untuk pengontrol anak.
  4. Untuk tujuan pelatihan, saya menggesek dari bawah, tetapi ini bertentangan dengan penutupan aplikasi pada iPhone X dan pusat kontrol. Menggunakan preferredScreenEdgesDeferringSystemGestures menonaktifkan gesekan sistem dari bawah.


     class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController() // 1 transition = PanelTransition(presented: child, presenting: self) // 2 // Setup the child child.modalPresentationStyle = .custom child.transitioningDelegate = transition // 3 } override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return .bottom // 4 } } 

    Setelah perubahan, ternyata ada masalah: setelah penutupan pertama panel, itu selamanya tetap dalam status TransitionDirection.dismiss . Atur status yang benar setelah menyembunyikan controller di PresentationController :


     override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } } 

    Kode tampilan interaktif dapat dilihat di utas terpisah . Ini terlihat seperti ini:




Kesimpulan


Sebagai hasilnya, kami dapat menunjukkan pengontrol dengan animasi terputus, dan pengguna memiliki kendali atas apa yang terjadi di layar. Ini jauh lebih bagus, karena animasi tidak lagi memblokir antarmuka, itu dapat dibatalkan atau bahkan dipercepat.


Contohnya bisa dilihat di github.


Berlangganan saluran Dodo Pizza Mobile.

Source: https://habr.com/ru/post/id465073/


All Articles