
Tinder - kita semua tahu bahwa ini adalah aplikasi kencan di mana Anda bisa menolak atau menerima seseorang dengan menggesekkan ke kiri atau ke kanan. Ide pembaca kartu ini sekarang digunakan dalam banyak aplikasi. Cara ini menampilkan data adalah untuk Anda jika Anda lelah menggunakan tampilan tabel dan koleksi. Ada banyak buku pelajaran tentang hal ini, tetapi proyek ini menghabiskan banyak waktu.
Anda dapat melihat proyek lengkapnya di
github saya.
Pertama-tama, saya ingin membayar upeti kepada
posting Phill Farrugia tentang masalah ini, dan kemudian ke seri
YouTube di studio Big Mountain pada topik yang sama. Jadi bagaimana kita membuat antarmuka ini? Saya mendapat bantuan dalam menerbitkan Phil tentang topik ini. Intinya, idenya adalah membuat UIViews dan menyisipkannya sebagai subview dalam tampilan wadah. Kemudian, menggunakan indeks, kami akan memberikan setiap UIView beberapa penyisipan horisontal dan vertikal dan sedikit mengubah lebarnya. Selanjutnya, ketika kita menyeret satu jari di satu peta, semua bingkai tampilan akan disusun ulang sesuai dengan nilai indeks baru.
Kami akan mulai dengan membuat tampilan kontainer dalam ViewController sederhana.
class ViewController: UIViewController {
Seperti yang Anda lihat, saya membuat kelas saya sendiri bernama SwipeContainerView dan baru saja mengkonfigurasi stackViewContainer menggunakan batasan otomatis. Tidak ada yang perlu dikhawatirkan. Ukuran SwipeContainerView akan menjadi 300x400, dan akan dipusatkan pada sumbu X dan hanya 60 piksel di atas tengah sumbu Y.
Sekarang kita telah mengkonfigurasi stackContainer, kita akan menuju ke subclass dari StackContainerView dan memuat semua jenis peta ke dalamnya. Sebelum itu, kami akan membuat protokol yang akan memiliki tiga metode:
protocol SwipeCardsDataSource { func numberOfCardsToShow() -> Int func card(at index: Int) -> SwipeCardView func emptyView() -> UIView? }
Pikirkan protokol ini sebagai TableViewDataSource. Kepatuhan kelas ViewController kami dengan protokol ini akan memungkinkan mentransfer informasi tentang data kami ke kelas SwipeCardContainer. Ini memiliki tiga metode:
numberOfCardsToShow () -> Int
: Mengembalikan jumlah kartu yang perlu kami tampilkan. Ini hanya penghitung array data.card(at index: Int) -> SwipeCardView
: mengembalikan SwipeCardView (kami akan membuat kelas ini dalam satu saat)EmptyView
-> Kami tidak akan melakukan apa-apa dengannya, tetapi segera setelah semua kartu dihapus, memanggil metode delegasi ini akan mengembalikan tampilan kosong dengan beberapa pesan (saya tidak akan mengimplementasikan ini dalam pelajaran khusus ini, coba sendiri)
Luruskan pengontrol tampilan dengan protokol ini:
extension ViewController : SwipeCardsDataSource { func numberOfCardsToShow() -> Int { return viewModelData.count } func card(at index: Int) -> SwipeCardView { let card = SwipeCardView() card.dataSource = viewModelData[index] return card } func emptyView() -> UIView? { return nil } }
Metode pertama akan mengembalikan jumlah elemen dalam array data. Pada metode kedua, buat instance SwipeCardView () dan kirim data array untuk indeks ini, dan kemudian kembalikan instance SwipeCardView.
SwipeCardView adalah subkelas dari UIView yang memiliki UIImage, UILabel, dan pengenal isyarat. Lebih lanjut tentang ini nanti. Kami akan menggunakan protokol ini untuk berkomunikasi dengan presentasi wadah.
stackContainer.dataSource = self
Ketika kode di atas menyala, fungsi reloadData dipanggil, yang kemudian memanggil fungsi-fungsi sumber data ini.
Class StackViewContainer: UIView { . . var dataSource: SwipeCardsDataSource? { didSet { reloadData() } } ....
Fungsi ReloadData:
func reloadData() { guard let datasource = dataSource else { return } setNeedsLayout() layoutIfNeeded() numberOfCardsToShow = datasource.numberOfCardsToShow() remainingcards = numberOfCardsToShow for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) } }
Dalam fungsi reloadData, pertama-tama kita mendapatkan jumlah kartu dan menyimpannya dalam variabel numberOfCardsToShow. Kemudian kami menetapkan ini ke variabel lain bernama tersisa Kartu. Dalam for loop, kami membuat peta yang merupakan turunan dari SwipeCardView menggunakan nilai indeks.
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) }
Faktanya, kami ingin kurang dari 3 kartu muncul sekaligus. Oleh karena itu, kami menggunakan fungsi min. CardsToBeVisible adalah konstanta sama dengan 3. Jika numberOfToShow lebih besar dari 3, maka hanya tiga kartu yang akan ditampilkan. Kami membuat kartu-kartu ini dari protokol:
func card(at index: Int) -> SwipeCardView
Fungsi addCardView () hanya digunakan untuk menyisipkan peta sebagai subview.
private func addCardView(cardView: SwipeCardView, atIndex index: Int) { cardView.delegate = self addCardFrame(index: index, cardView: cardView) cardViews.append(cardView) insertSubview(cardView, at: 0) remainingcards -= 1 }
Dalam fungsi ini, kami menambahkan cardView ke hierarki tampilan, dan menambahkan kartu sebagai subview, kami mengurangi kartu yang tersisa dengan 1. Setelah kami menambahkan cardView sebagai subview, kami mengatur bingkai kartu-kartu ini. Untuk melakukan ini, kami menggunakan fungsi addCardFrame () lain:
func addCardFrame(index: Int, cardView: SwipeCardView) { var cardViewFrame = bounds let horizontalInset = (CGFloat(index) * self.horizontalInset) let verticalInset = CGFloat(index) * self.verticalInset cardViewFrame.size.width -= 2 * horizontalInset cardViewFrame.origin.x += horizontalInset cardViewFrame.origin.y += verticalInset cardView.frame = cardViewFrame }
Logika addCardFrame () ini diambil langsung dari pos Phil. Di sini kita mengatur bingkai peta sesuai dengan indeksnya. Kartu pertama dengan indeks 0 akan memiliki bingkai, seperti wadah. Kemudian kita mengubah asal bingkai dan lebar peta sesuai dengan sisipan. Jadi, kami menambahkan kartu sedikit ke kanan kartu di atas, mengurangi lebarnya, dan juga perlu menarik kartu ke bawah untuk menciptakan perasaan bahwa kartu ditumpuk satu sama lain.
Setelah ini selesai, Anda akan melihat bahwa kartu ditumpuk satu sama lain. Cukup bagus!

Namun, sekarang kita perlu menambahkan gerakan menggesek ke tampilan peta. Sekarang mari kita mengalihkan perhatian kita ke kelas SwipeCardView.
SwipeCardView
Kelas swipeCardView adalah subkelas reguler dari UIView. Namun, untuk alasan yang hanya diketahui oleh insinyur Apple, sangat sulit untuk menambahkan bayangan ke UIView dengan sudut bulat. Untuk menambahkan bayangan ke tampilan peta, saya membuat dua UIViews. Salah satunya adalah shadowView, dan kemudian ke swipeView. Pada dasarnya, shadowView memiliki bayangan dan hanya itu. SwipeView memiliki sudut-sudut bulat. Pada swipeView, saya menambahkan UIImageView, label UIL untuk menampilkan data dan gambar.
var swipeView : UIView! var shadowView : UIView!
Pengaturan shadowView dan swipeView:
func configureShadowView() { shadowView = UIView() shadowView.backgroundColor = .clear shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 0, height: 0) shadowView.layer.shadowOpacity = 0.8 shadowView.layer.shadowRadius = 4.0 addSubview(shadowView) shadowView.translatesAutoresizingMaskIntoConstraints = false shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true } func configureSwipeView() { swipeView = UIView() swipeView.layer.cornerRadius = 15 swipeView.clipsToBounds = true shadowView.addSubview(swipeView) swipeView.translatesAutoresizingMaskIntoConstraints = false swipeView.leftAnchor.constraint(equalTo: shadowView.leftAnchor).isActive = true swipeView.rightAnchor.constraint(equalTo: shadowView.rightAnchor).isActive = true swipeView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true swipeView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true }
Kemudian saya menambahkan pengenal isyarat ke kartu jenis ini dan fungsi pemilih dipanggil saat dikenali. Fungsi pemilih ini memiliki banyak logika untuk menggulir, memiringkan, dll. Mari kita lihat:
@objc func handlePanGesture(sender: UIPanGestureRecognizer){ let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y) switch sender.state { case .ended: if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return } UIView.animate(withDuration: 0.2) { card.transform = .identity card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) self.layoutIfNeeded() } case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation) default: break } }
Empat baris pertama dalam kode di atas:
let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
Pertama-tama kita mendapatkan ide dengan mana gerakan itu diadakan. Selanjutnya, kami menggunakan metode transfer untuk mengetahui berapa kali pengguna menekan kartu. Baris ketiga pada dasarnya mendapatkan titik tengah wadah induk. Baris terakhir tempat kami memasang card.center. Ketika pengguna menggesekkan jari di atas kartu, bagian tengah kartu dinaikkan oleh nilai terjemahan x dan nilai terjemahan y. Untuk mendapatkan perilaku jepret ini, kami secara signifikan mengubah titik pusat peta dari koordinat tetap. Ketika terjemahan gerakan berakhir, kami mengembalikannya ke card.center.
Dalam kasus state.ended:
if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }
Kami memeriksa apakah card.center.x lebih besar dari 400 atau jika card.center.x kurang dari -65. Jika demikian, maka kami membuang kartu-kartu ini, mengubah bagian tengah.
Jika geser ke kanan:
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
Jika geser ke kiri:
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
Jika pengguna mengakhiri gerakan di tengah antara 400 dan -65, maka kami akan mengatur ulang pusat peta. Kami juga memanggil metode delegasi ketika gesekan berakhir. Lebih lanjut tentang ini nanti.
Untuk mendapatkan kemiringan ini saat Anda menggesek peta; Saya akan jujur ββsecara brutal. Saya menggunakan sedikit geometri dan menggunakan nilai yang berbeda dari tegak lurus dan alasnya, dan kemudian menggunakan fungsi tan untuk mendapatkan sudut rotasi. Sekali lagi, ini hanya coba-coba. Menggunakan point.x dan lebar wadah sebagai dua perimeter tampaknya bekerja dengan baik. Silakan bereksperimen dengan nilai-nilai ini.
case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation)
Sekarang mari kita bicara tentang fungsi delegasi. Kami akan menggunakan fungsi delegasi untuk berkomunikasi antara SwipeCardView dan ContainerView.
protocol SwipeCardsDelegate { func swipeDidEnd(on view: SwipeCardView) }
Fungsi ini akan mempertimbangkan jenis di mana gesekan terjadi, dan kami akan mengambil beberapa langkah untuk menghapusnya dari subview, dan kemudian mengulang semua frame untuk kartu di bawahnya. Begini caranya:
func swipeDidEnd(on view: SwipeCardView) { guard let datasource = dataSource else { return } view.removeFromSuperview() if remainingcards > 0 { let newIndex = datasource.numberOfCardsToShow() - remainingcards addCardView(cardView: datasource.card(at: newIndex), atIndex: 2) for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } }else { for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } } }
Pertama-tama hapus tampilan ini dari tampilan super. Setelah ini selesai, periksa apakah masih ada kartu yang tersisa. Jika ada, maka kami akan membuat indeks baru untuk kartu yang akan dibuat. Kami akan membuatindex baru dengan mengurangi jumlah kartu untuk ditampilkan dengan sisa kartu. Kemudian kita akan menambahkan peta sebagai subview. Namun, kartu baru ini akan menjadi yang terendah sehingga 2 yang kami kirim pada dasarnya akan menjamin bahwa bingkai yang ditambahkan cocok dengan indeks 2 atau yang terendah.
Untuk menghidupkan bingkai dari kartu yang tersisa, kami akan menggunakan indeks subview. Untuk melakukan ini, kita akan membuat array terlihat Kartu, yang akan berisi semua subview wadah sebagai array.
var visibleCards: [SwipeCardView] { return subviews as? [SwipeCardView] ?? [] }
Masalahnya, bagaimanapun, adalah bahwa array kartu yang terlihat akan memiliki indeks subview terbalik. Dengan demikian, kartu pertama akan menjadi yang ketiga, yang kedua akan tetap di tempat kedua, dan yang ketiga akan berada di posisi pertama. Untuk mencegah hal ini terjadi, kami akan menjalankan array terlihat kartu dalam urutan terbalik untuk mendapatkan indeks subview yang sebenarnya, bukan bagaimana mereka berada di array terlihat kartu.
for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) }
Jadi sekarang kita akan memperbarui bingkai dari sisa cardViews.
Itu saja. Ini adalah cara yang ideal untuk menyajikan sejumlah kecil data.