Prolog
Di salah satu proyek saya, saya perlu membuat antarmuka seperti itu di Snepchat. Ketika sebuah kartu dengan informasi keluar dari gambar dari kamera, gantikan dengan warna solid, dan juga arah yang berlawanan. Saya pribadi sangat terpesona oleh transisi dari jendela kamera ke kartu samping, dan dengan senang hati saya pergi untuk menceritakan cara-cara untuk menyelesaikan masalah ini.
Di sebelah kiri adalah contoh dari Snepchat, di sebelah kanan adalah contoh dari aplikasi yang akan kita buat.


Mungkin solusi pertama yang muncul dalam pikiran adalah untuk mengadaptasi UIScrollView
, entah bagaimana mengatur pandangan di atasnya, menggunakan pagination, tetapi, terus terang, gulungan itu dianggap untuk menyelesaikan tugas yang sama sekali berbeda, mengambil animasi tambahan yang melelahkan, dan tidak memiliki fleksibilitas yang diperlukan pengaturan. Oleh karena itu, menggunakannya untuk menyelesaikan masalah ini sama sekali tidak dapat dibenarkan.
Gulir antara jendela kamera dan tab samping menipu - ini bukan gulungan sama sekali, ini merupakan transisi interaktif antara tampilan yang dimiliki oleh pengontrol yang berbeda. Tombol-tombol di bagian bawahnya adalah tab biasa, mengklik tombol yang melemparkan kita di antara pengontrol.

Dengan cara ini, Snatch menggunakan pengontrol navigasi versi sendiri seperti UITabBarController
dengan transisi interaktif khusus.
UIKit
menyertakan dua opsi untuk pengontrol navigasi yang memungkinkan Anda untuk menyesuaikan transisi - ini adalah UINavigationController
dan UITabBarController
. Keduanya memiliki metode navigationController(_:interactionControllerFor:)
dan tabBarController(_:interactionControllerFor:)
di masing-masing delegasi, yang memungkinkan kami untuk menggunakan animasi interaktif sendiri untuk transisi.
tabBarController (_: interaksiControllerFor :)
navigationController (_: interaksiControllerFor :)
Tetapi saya tidak ingin dibatasi oleh implementasi UITabBarController
atau UINavigationController
, terutama karena kita tidak dapat mengendalikan logika internal mereka. Oleh karena itu, saya memutuskan untuk menulis controller serupa saya, dan sekarang saya ingin memberi tahu dan menunjukkan apa yang terjadi.
Pernyataan masalah
Buat pengontrol wadah Anda sendiri, di mana Anda dapat beralih di antara pengontrol anak menggunakan animasi interaktif untuk transisi, menggunakan mekanisme standar di UITabBarController
dan UINavigationController
. Kita memerlukan mekanisme standar ini untuk menggunakan animasi transisi yang sudah jadi dari tipe UIViewControllerAnimatedTransitioning
sudah ditulis.
Persiapan proyek
Biasanya saya mencoba untuk memindahkan modul ke dalam kerangka kerja yang terpisah, untuk ini saya membuat proyek aplikasi baru, dan menambahkan target Cocoa Touch Framework
sana, dan kemudian menyebarkan sumber dalam proyek untuk target yang sesuai. Dengan cara ini saya mendapatkan kerangka kerja terpisah dengan aplikasi uji untuk debugging.
Buat Single View App
.

Product Name
akan menjadi target kami.

Klik pada +
untuk menambahkan target.

Pilih Cocoa Touch Framework
.

Kami menyebut kerangka kerja kami nama yang tepat, Xcode secara otomatis memilih proyek untuk target kami dan menawarkan untuk mengikat biner langsung ke dalam aplikasi. Kami setuju.

Kami tidak akan memerlukan Main.storyboard
dan ViewController.swift
default, kami menghapusnya.

Juga jangan lupa untuk menghapus nilai dari Main Interface
di target aplikasi pada tab General
.

Sekarang kita pergi ke AppDelegate.swift
dan hanya menyisakan metode application
dari konten berikut:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Di sini kita mengatur pengontrol kita di tempat utama sehingga muncul setelah peluncur.
Sekarang buat MasterViewController
. Ini akan terkait dengan aplikasi, jadi penting untuk memilih target yang tepat saat membuat file.

Kami akan mewarisi MasterViewController
dari SnapchatNavigationController
, yang akan kami implementasikan nanti dalam framework. Jangan lupa untuk menentukan import
kerangka kerja kami. Saya tidak memberikan kode pengontrol lengkap di sini, kelalaian diperlihatkan oleh elips ...
, saya menempatkan aplikasi pada GitHub , di sana Anda dapat melihat semua detail. Dalam pengontrol ini, kami hanya tertarik pada metode viewDidLoad()
, yang menginisialisasi pengontrol latar belakang dengan kamera + satu pengontrol transparan (jendela utama) + pengontrol yang berisi kartu yang berangkat.
import MakingSnapchatNavigation class MasterViewController: SnapchatNavigationController { override func viewDidLoad() { super.viewDidLoad()
Apa yang sedang terjadi di sini? Kami membuat pengontrol dengan kamera dan mengaturnya ke latar belakang menggunakan metode setBackground
dari SnapchatNavigationController
. Pengontrol ini berisi gambar yang diregangkan untuk seluruh tampilan dari kamera. Kemudian kita membuat pengontrol transparan kosong dan menambahkannya ke array, itu hanya melewati gambar dari kamera melaluinya, kita dapat menempatkan kontrol di atasnya, membuat pengontrol transparan lain, menambahkan gulir ke sana, menambahkan tampilan dengan konten di dalam gulir, menambahkan pengontrol kedua ke array dan set array ini menggunakan metode setViewControllers
khusus dari induk SnapchatNavigationController
.
Jangan lupa untuk menambahkan permintaan untuk menggunakan kamera di Info.plist
<key>NSCameraUsageDescription</key> <string>Need camera for background</string>
Pada ini, kami menganggap aplikasi uji siap, dan beralih ke bagian paling menarik - implementasi kerangka kerja.
Struktur Pengontrol Induk
Pertama, buat SnapchatNavigationController
kosong, penting untuk memilih target yang tepat untuk itu. Jika semuanya dilakukan dengan benar, maka aplikasi harus dibangun. Status proyek ini dapat diturunkan dengan referensi .
open class SnapchatNavigationController: UIViewController { override open func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } // MARK: - Public interface /// Sets view controllers. public func setViewControllers(vcs: [UIViewController]) { } /// Sets background view. public func setBackground(vc: UIViewController) { } }
Sekarang tambahkan komponen internal yang akan terdiri dari pengontrol. Saya tidak membawa semua kode di sini, saya hanya fokus pada poin-poin penting.
Kami mengatur variabel untuk menyimpan array pengendali anak. Sekarang kita secara kaku mengatur jumlah yang dibutuhkan - 2 buah. Di masa mendatang, dimungkinkan untuk memperluas logika pengontrol untuk digunakan dengan sejumlah pengontrol. Kami juga mengatur variabel untuk menyimpan pengontrol saat ini ditampilkan.
private let requiredChildrenAmount = 2 // MARK: - View controllers /// top child view controller private var topViewController: UIViewController? /// all children view controllers private var children: [UIViewController] = []
Buat tampilan. Kita perlu satu tampilan untuk latar belakang, satu tampilan dengan efek yang ingin kita terapkan ke latar belakang saat mengganti controller. Kami juga memiliki wadah tampilan untuk pengontrol anak saat ini dan indikator tampilan yang akan memberi tahu pengguna cara bekerja dengan navigasi.
// MARK: - Views private let backgroundViewContainer = UIView() private let backgroundBlurEffectView: UIVisualEffectView = { let backgroundBlurEffect = UIBlurEffect(style: UIBlurEffectStyle.light) let backgroundBlurEffectView = UIVisualEffectView(effect: backgroundBlurEffect) backgroundBlurEffectView.alpha = 0 return backgroundBlurEffectView }() /// content view for children private let contentViewContainer = UIView() private let swipeIndicatorView = UIView()
Di blok berikutnya, kita menetapkan dua variabel, swipeAnimator
bertanggung jawab untuk animasi, swipeInteractor
bertanggung jawab atas interaksi (kemampuan untuk mengontrol kemajuan animasi), kita harus menginisialisasi selama boot controller, jadi kami memaksa membuka.
// MARK: - Animation and transition private let swipeAnimator = AnimatedTransitioning() private var swipeInteractor: CustomSwipeInteractor!
Kami juga mengatur transformasi untuk indikator. Kami menggeser indikator dengan lebar wadah + pergeseran ganda dari tepi + lebar indikator itu sendiri sehingga indikator berada di ujung wadah. Lebar wadah akan diketahui selama aplikasi, sehingga variabel dihitung saat bepergian.
// MARK: - Animation transforms private var swipeIndicatorViewTransform: CGAffineTransform { get { return CGAffineTransform(translationX: -contentViewContainer.bounds.size.width + (swipeIndicatorViewXShift * 2) + swipeIndicatorViewWidth, y: 0) } }
Saat memuat pengontrol, kami menetapkan self
animasi (kami akan mengimplementasikan protokol yang sesuai di bawah ini), menginisialisasi berinteraksi berdasarkan animasi kami, kemajuan yang akan dikontrolnya. Kami juga menunjuknya sebagai delegasi. Delegasi akan menanggapi permulaan gerakan pengguna dan memulai animasi atau membatalkan tergantung pada status controller. Kemudian kami menambahkan semua tampilan ke tampilan utama dan memanggil setupViews()
, yang mengatur kendala.
override open func viewDidLoad() { super.viewDidLoad() swipeAnimator.animation = self swipeInteractor = CustomSwipeInteractor(with: swipeAnimator) swipeInteractor.delegate = self view.addSubview(backgroundViewContainer) view.addSubview(backgroundBlurEffectView) view.addSubview(contentViewContainer) view.addSubview(swipeIndicatorView) setupViews() }
Selanjutnya, kita beralih ke logika menginstal dan menghapus pengontrol anak dalam sebuah wadah. Semuanya di sini sederhana seperti dalam dokumentasi Apple. Kami menggunakan metode yang ditentukan untuk jenis operasi ini.
addChildViewController(vc)
- tambahkan pengendali anak ke yang sekarang.
contentViewContainer.addSubview(vc.view)
- tambahkan tampilan controller ke hierarki tampilan.
vc.view.frame = contentViewContainer.bounds
- rentangkan tampilan ke seluruh wadah. Karena kita menggunakan bingkai di sini alih-alih tata letak otomatis, kita perlu mengubah ukurannya setiap kali ukuran pengontrol berubah, kita akan menghilangkan logika ini dan menganggap bahwa wadah tidak akan mengubah ukuran aplikasi saat aplikasi sedang berjalan.
vc.didMove(toParentViewController: self)
- mengakhiri operasi penambahan pengendali anak.
swipeInteractor.wireTo
- kita ikat pengontrol saat ini dengan gerakan pengguna. Nanti kita akan menganalisis metode ini.
// MARK: - Private methods private func addChild(vc: UIViewController) { addChildViewController(vc) contentViewContainer.addSubview(vc.view) vc.view.frame = contentViewContainer.bounds vc.didMove(toParentViewController: self) topViewController = vc let goingRight = children.index(of: topViewController!) == 0 swipeInteractor.wireTo(viewController: topViewController!, edge: goingRight ? .right : .left) } private func removeChild(vc: UIViewController) { vc.willMove(toParentViewController: nil) vc.view.removeFromSuperview() vc.removeFromParentViewController() topViewController = nil }
Ada dua metode lagi yang kodenya tidak akan saya berikan di sini: setViewControllers
dan setBackground
. Dalam metode setViewControllers
kita cukup mengatur array pengontrol anak dalam variabel yang sesuai dari controller kita dan memanggil addChild
untuk menampilkan salah satu dari mereka pada tampilan. Dalam metode setBackground
kami melakukan hal yang sama seperti di addChild
, hanya untuk pengontrol latar belakang.
Logika Pengontrol Kontainer Animasi
Total, dasar dari kontroler induk kami adalah:
- UIView dibagi menjadi dua jenis
- Daftar anak UIViewController
- Objek kontrol animasi dari
swipeAnimator
jenis AnimatedTransitioning
- Objek yang mengontrol program interaktif dari animasi
CustomSwipeInteractor
tipe CustomSwipeInteractor
- Delegasikan Animasi Interaktif
- Implementasi Protokol Animasi
Sekarang kita akan menganalisis dua poin terakhir, kemudian beralih ke implementasi AnimatedTransitioning
dan CustomSwipeInteractor
.
Delegasikan Animasi Interaktif
Delegasi hanya terdiri dari satu panGestureDidStart(rightToLeftSwipe: Bool) -> Bool
metode panGestureDidStart(rightToLeftSwipe: Bool) -> Bool
, yang menginformasikan pengontrol tentang awal gerakan dan arahnya. Sebagai tanggapan, dia menunggu informasi tentang apakah animasi dapat dianggap dimulai.
Sebagai delegasi, kami memeriksa urutan pengontrol saat ini untuk memahami apakah kami dapat memulai animasi dalam arah yang diberikan, dan jika semuanya baik-baik saja, kami memulai metode transition
, dengan parameter: pengontrol tempat kami bergerak, pengontrol tempat kami bergerak, pengontrol tempat kami bergerak, arah gerakan, bendera interaktivitas (Jika false
, animasi transisi yang diperbaiki waktu dipicu).
func panGestureDidStart(rightToLeftSwipe: Bool) -> Bool { guard let topViewController = topViewController, let fromIndex = children.index(of: topViewController) else { return false } let newIndex = rightToLeftSwipe ? 1 : 0 // - if newIndex > -1 && newIndex < children.count && newIndex != fromIndex { transition(from: children[fromIndex], to: children[newIndex], goingRight: rightToLeftSwipe, interactive: true) return true } return false }
Mari kita segera memeriksa isi dari metode transition
. Pertama-tama, kami membuat konteks animasi untuk animasi CustomControllerContext
. Kami juga akan menganalisis kelas ini sedikit kemudian, ini mengimplementasikan protokol UIViewControllerContextTransitioning
. Dalam kasus UINavigationController
dan UITabBarController
instance dari implementasi protokol ini secara otomatis dibuat oleh sistem dan logikanya tersembunyi dari kita, kita perlu membuat kita sendiri.
let ctx = CustomControllerContext(fromViewController: from, toViewController: to, containerView: contentViewContainer, goingRight: goingRight) ctx.isAnimated = true ctx.isInteractive = interactive ctx.completionBlock = { (didComplete: Bool) in if didComplete { self.removeChild(vc: from) self.addChild(vc: to) } };
Kemudian kita cukup memanggil animasi tetap atau interaktif. Di masa mendatang, akan dimungkinkan untuk menggantung yang tetap pada tab tombol navigasi antara pengontrol, dalam contoh ini kita tidak akan melakukan ini.
if interactive { // Animate with interaction swipeInteractor.startInteractiveTransition(ctx) } else { // Animate without interaction swipeAnimator.animateTransition(using: ctx) }
Protokol Animasi
Protokol animasi TransitionAnimation
terdiri dari 4 metode:
addTo
adalah metode yang dirancang untuk membuat struktur pandangan anak yang benar dalam wadah, sehingga tampilan sebelumnya tumpang tindih dengan yang baru sesuai dengan ide animasi.
prepare
adalah metode yang disebut sebelum animasi untuk mempersiapkan tampilan.
/// Setup the views position prior to the animation start. func prepare(fromView from: UIView?, toView to: UIView?, fromLeft: Bool)
animation
- animasi itu sendiri.
finalize
- tindakan yang diperlukan setelah selesainya animasi.
Kami tidak akan mempertimbangkan implementasi yang digunakan, semuanya cukup transparan di sana, kami akan langsung ke tiga kelas utama, berkat animasi yang berlangsung.
class CustomControllerContext: NSObject, UIViewControllerContextTransitioning
Konteks animasi. Untuk menjelaskan fungsinya, kami merujuk pada bantuan protokol UIViewControllerContextTransitioning
:
Objek konteks merangkum informasi tentang pandangan dan pengendali tampilan yang terlibat dalam transisi. Ini juga berisi perincian tentang bagaimana menjalankan transisi.
Yang paling menarik adalah larangan adaptasi protokol ini:
Jangan mengadopsi protokol ini di kelas Anda sendiri, Anda juga tidak harus secara langsung membuat objek yang mengadopsi protokol ini.
Tapi kami benar-benar membutuhkannya untuk menjalankan mesin animasi standar, jadi kami tetap mengadaptasinya. Ini hampir tidak memiliki logika, hanya menyimpan keadaan. Karena itu, saya bahkan tidak akan membawanya ke sini. Anda dapat menontonnya di GitHub .
Ini bekerja sangat baik pada animasi yang ditetapkan waktu. Tetapi ketika menggunakannya untuk animasi interaktif, satu masalah muncul - UIPercentDrivenInteractiveTransition
memanggil metode tidak berdokumen pada konteksnya. Satu-satunya solusi yang tepat dalam situasi ini adalah mengadaptasi protokol lain - UIViewControllerInteractiveTransitioning
untuk menggunakan konteks Anda sendiri.
class PercentDrivenInteractiveTransition: NSObject, UIViewControllerInteractiveTransitioning
Ini dia - jantung dari proyek, memungkinkan animasi interaktif ada di pengontrol wadah kustom. Mari kita bereskan.
Kelas diinisialisasi dengan satu parameter dari tipe UIViewControllerAnimatedTransitioning
, ini adalah protokol standar untuk menjiwai transisi antar pengontrol. Dengan cara ini kita dapat menggunakan salah satu animasi yang sudah ditulis bersama dengan kelas kita.
init(with animator: UIViewControllerAnimatedTransitioning) { self.animator = animator }
Antarmuka publik cukup sederhana, empat metode, yang fungsinya harus jelas.
Kita hanya perlu mencatat saat animasi dimulai, kita mengambil tampilan induk wadah dan mengatur kecepatan layer ke 0, jadi kita mendapatkan kemampuan untuk mengontrol kemajuan animasi secara manual.
// MARK: - Public func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { self.transitionContext = transitionContext transitionContext.containerView.superview?.layer.speed = 0 animator.animateTransition(using: transitionContext) } func updateInteractiveTransition(percentComplete: CGFloat) { setPercentComplete(percentComplete: (CGFloat(fmaxf(fminf(Float(percentComplete), 1), 0)))) } func cancelInteractiveTransition() { transitionContext?.cancelInteractiveTransition() completeTransition() } func finishInteractiveTransition() { transitionContext?.finishInteractiveTransition() completeTransition() }
Kita sekarang beralih ke blok logika pribadi kelas kita.
setPercentComplete
menetapkan waktu offset perkembangan animasi untuk lapisan superview, menghitung nilai dari persentase penyelesaian dan durasi animasi.
private func setPercentComplete(percentComplete: CGFloat) { setTimeOffset(timeOffset: TimeInterval(percentComplete) * duration) transitionContext?.updateInteractiveTransition(percentComplete) } private func setTimeOffset(timeOffset: TimeInterval) { transitionContext?.containerView.superview?.layer.timeOffset = timeOffset }
completeTransition
dipanggil ketika pengguna telah menghentikan gerakan mereka. Di sini kita membuat instance dari kelas CADisplayLink
, yang akan memungkinkan kita untuk menyelesaikan animasi secara indah dari titik ketika pengguna tidak lagi mengontrol progresnya. Kami menambahkan displayLink
kami ke run loop
sehingga sistem memanggil pemilih kami setiap kali perlu menampilkan bingkai baru di layar perangkat.
private func completeTransition() { displayLink = CADisplayLink(target: self, selector: #selector(tickAnimation)) displayLink!.add(to: .main, forMode: .commonModes) }
Di pemilih kami, kami menghitung dan mengatur perpindahan sementara dari perkembangan animasi, seperti yang kami lakukan sebelumnya selama gerakan pengguna, atau kami menyelesaikan animasi ketika mencapai titik awal atau akhir.
@objc private func tickAnimation() { var timeOffset = self.timeOffset() let tick = (displayLink?.duration ?? 0) * TimeInterval(completionSpeed) timeOffset += (transitionContext?.transitionWasCancelled ?? false) ? -tick : tick; if (timeOffset < 0 || timeOffset > duration) { transitionFinished() } else { setTimeOffset(timeOffset: timeOffset) } } private func timeOffset() -> TimeInterval { return transitionContext?.containerView.superview?.layer.timeOffset ?? 0 }
Menyelesaikan animasi, kita displayLink
kita, kembalikan kecepatan lapisan, dan jika animasi belum dibatalkan, yaitu, itu telah mencapai bingkai akhir, kita menghitung waktu dari mana animasi lapisan harus dimulai. Anda dapat mempelajari lebih lanjut tentang ini di Panduan Pemrograman Core Animation, atau dalam jawaban ini untuk stackoverflow.
private func transitionFinished() { displayLink?.invalidate() guard let layer = transitionContext?.containerView.superview?.layer else { return } layer.speed = 1; let wasNotCanceled = !(transitionContext?.transitionWasCancelled ?? false) if (wasNotCanceled) { let pausedTime = layer.timeOffset layer.timeOffset = 0.0; let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime layer.beginTime = timeSincePause } animator.animationEnded?(wasNotCanceled) }
class AnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning
Kelas terakhir yang belum kita periksa adalah implementasi protokol UIViewControllerAnimatedTransitioning
, di mana kita mengontrol urutan eksekusi metode protokol dari animasi kita, addTo
, prepare
, animation
, finalize
. Semuanya di sini cukup sederhana, perlu dicatat hanya penggunaan UIViewPropertyAnimator
untuk melakukan animasi, bukan UIView.animate(withDuration:animations:)
lebih khas UIView.animate(withDuration:animations:)
. Hal ini dilakukan agar memungkinkan untuk lebih mengontrol kemajuan animasi, dan jika dibatalkan, kembalikan ke posisi finishAnimation(at: .start)
dengan memanggil finishAnimation(at: .start)
, yang menghindari kedip bingkai animasi terakhir yang tidak perlu pada layar.
Epilog
Kami telah membuat demo kerja antarmuka yang mirip dengan Snapchat. Dalam versi saya, saya mengkonfigurasi konstanta sehingga ada bidang di kanan dan kiri kartu, selain itu, saya meninggalkan kamera pada tampilan latar belakang untuk membuat efek di belakang kartu. Ini dilakukan semata-mata untuk menunjukkan kemampuan pendekatan ini, bagaimana itu akan mempengaruhi kinerja perangkat dan saya tidak memeriksa daya baterainya.
β , - , . , - .
GitHub .
, , , !

:
Custom Container View Controller Transitions, Joachim Bondo.
Objective C. Swift.
Interactive Custom Container View Controller Transitions, Alek Γ
strΓΆm
, Objective C, Swift.
SwipeableTabBarController
, UITabBarController
. .