Setiap pengembang untuk platform seluler selalu dihadapkan dengan tugas yang tidak dapat diselesaikan dengan satu cara. Selalu ada beberapa cara, beberapa cepat, beberapa rumit, dan masing-masing memiliki kelebihan dan kekurangan.Scroll tak berujung / siklik di iOS tidak dapat diimplementasikan menggunakan alat standar, Anda harus pergi ke trik yang berbeda. Dalam artikel ini saya akan memberi tahu Anda opsi mana untuk menyelesaikan masalah yang ada di permukaan dan opsi mana yang akhirnya kami terapkan.Tugas
Itu perlu untuk membuat bergulir siklik tak berujung dengan unsur-unsur dalam bentuk gambar, judul dan subtitle yang disiapkan. Input: gambar pusat diindentasi dari layar sebesar 16.0 poin. Di sisi gambar tengah, "telinga" menonjol. Dan jarak antara spanduk adalah 8,0 poin.Kami mempelajari apa yang telah dilakukan oleh rekan kerja
Spanduk Dodo tidak siklis, spanduk pusat selalu bersebelahan dengan tepi kiri pada jarak tertentu.Auto.ru - spanduk tidak siklis, jika Anda menggesek, maka spanduk dibalik untuk waktu yang sangat lama.Ozon - spanduk bersifat siklis, tetapi tidak dapat dikontrol dengan sentuhan: begitu arah gesek ditentukan, gambar tidak lagi dapat dihentikan.Wildberries - spanduk tidak siklis, pemusatan terjadi, penyelesaian panjang dari animasi gulir.Keinginan terakhir:- Spanduk harus dipusatkan.
- Menggulir harus diakhiri tanpa menunggu lama untuk animasi.
- Pengaturan spanduk: harus ada kemampuan untuk mengontrol animasi gulir dengan jari Anda.
Opsi implementasi
Ketika tugas baru muncul yang belum diselesaikan, ada baiknya mempertimbangkan solusi dan pendekatan yang ada.UICollectionView
Bertindak di dahi, Anda bisa sampai pada opsi untuk membuat UICollectionView
. Kami membuat jumlah elemen Int.max
dan selama inisialisasi kami menunjukkan tengah, dan saat memanggil metode di dataSource
- func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell
. Kami akan mengembalikan elemen yang sesuai, dengan asumsi bahwa elemen nol adalah ini Int.max / 2
. Monster seperti itu dengan banyak fitur, seperti UICollectionView
, tidak pantas digunakan untuk tugas sederhana kita.UIScrollView dan (n + 2) UIView
Masih ada opsi di mana UIScrollView dibuat, benar-benar semua spanduk ditempatkan di atasnya, dan ditambahkan ke awal dan akhir oleh spanduk. Ketika kami menyelesaikannya sampai akhir, kami mengubah offset secara tidak terlihat untuk pengguna dan kembali ke elemen pertama. Dan ketika bergulir kembali, kami melakukan yang sebaliknya. Akibatnya, dengan sejumlah besar elemen, sekelompok tampilan akan dibuat tanpa menggunakannya kembali.SumberJalannya sendiri
Kami memutuskan untuk membuat UIScrollView + tiga UIView. UIView ini akan digunakan kembali. Pada saat menggulir, kami akan kembali contentOffset
ke spanduk pusat dan mengganti konten ketiga UIView. Dan kemudian Anda harus mendapatkan komponen ringan yang menutup tugas ini.Namun, ada kekhawatiran bahwa spoofing konten selama pengguliran akan terlihat oleh pengguna. Kami belajar tentang ini selama implementasi.Penerapan
Mempersiapkan UIScrollView dan Tiga UIImageView
Buat pewaris UIView
, tempatkan UIScrollView
tiga di atasnya UIImageView
:final class BannersView: UIView {
private let scrollView = UIScrollView()
private let leftItemView = UIImageView()
private let centerItemView = UIImageView()
private let rightItemView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.scrollView)
self.setupScrollView()
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach(self.scrollView.addSubview)
}
}
Tambahkan implementasi metode dengan pengaturan scrollView
:Setelah pengaturan dasar, kita bisa melakukan tata letak dan penempatan ImageView
:override func layoutSubviews() {
super.layoutSubviews()
self.scrollView.frame = self.bounds
let horizontalItemOffsetFromSuperView: CGFloat = 16.0
let spaceBetweenItems: CGFloat = 8.0
let itemWidth = self.frame.width - horizontalItemOffsetFromSuperView * 2
let itemHeight: CGFloat = self.scrollView.frame.height
var startX: CGFloat = 0.0
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.forEach { view in
view.frame.origin = CGPoint(x: startX, y: 0.0)
view.frame.size = CGSize(width: itemWidth, height: itemHeight)
startX += itemWidth + spaceBetweenItems
}
let viewsCount: CGFloat = 3.0
let contentWidth: CGFloat = itemWidth * viewsCount + spaceBetweenItems * (viewsCount - 1.0)
self.scrollView.contentSize = CGSize(width: contentWidth, height: self.frame.height)
}
Tambahkan ke UIImageView
gambar yang akan kami tarik dari situs penghasil gambar https://placeholder.com : let imageURLs = ImageURLFactory.makeImageURLS()
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
Hasil dari langkah persiapan pertama:Tengahkan gambar saat menggulir
Untuk mengontrol gulir yang akan kita gunakan UIScrollViewDelegate
. Kami setup
menetapkan delegasi untuk ke metode UIScrollView
, dan juga mengatur contentInset
gambar pertama dan terakhir untuk memiliki lekukan di samping.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0)
self.scrollView.delegate = self
Kami membuat extension
untuk kami BannersView
salah satu metode. Metode delegasi func scrollViewWillEndDragging
dipanggil ketika pengguna berhenti menggulir. Dalam metode ini, kami tertarik targetContentOffset
- ini adalah variabel yang bertanggung jawab untuk offset gulir akhir (titik di mana gulir berhenti).
extension BannersView: UIScrollViewDelegate {
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let gap: CGFloat = self.centerItemView.frame.width / 3
let targetRightOffsetX = targetContentOffset.pointee.x + self.frame.width
if (self.rightItemView.frame.minX + gap) < targetRightOffsetX {
targetContentOffset.pointee.x = self.rightItemView.frame.midX - self.frame.midX
}
else if (self.leftItemView.frame.maxX - gap) > targetContentOffset.pointee.x {
targetContentOffset.pointee.x = self.leftItemView.frame.midX - self.frame.midX
}
else {
targetContentOffset.pointee.x = self.centerItemView.frame.midX - self.frame.midX
}
}
}
gap
- ini adalah jarak di mana kita akan menganggap bahwa pandangan adalah pusat. Jika sepertiga dari lebar gambar oranye ditampilkan di layar, maka kita akan mengatur offset akhir sehingga gambar oranye berada di tengah.targetRightOffsetX
- Poin ini akan membantu menentukan apakah pandangan yang benar adalah pusat.Hasil implementasi dari metode ini:Kelola offset saat menggulir
Sekarang, tepat saat menggulir, kita akan berubah contentOffset
, kembali ke tengah layar tampilan tengah. Ini akan memungkinkan pengguna untuk membuat ilusi bergulir tanpa batas tanpa disadari.Tambahkan metode delegasi func scrollViewDidScroll(_ scrollView: UIScrollView)
, disebut ketika contentOffset
y berubah UIScrollView
.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard
self.leftItemView.frame.width > 0,
self.centerItemView.frame.width > 0,
self.rightItemView.frame.width > 0
else {
return
}
let gap: CGFloat = self.centerItemView.frame.width / 3
let spacing: CGFloat = 8.0
let currentRightOffset: CGFloat = scrollView.contentOffset.x + self.frame.width + scrollView.contentInset.left
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
}
}
gap
- ini adalah jarak dari mana kita akan menentukan kebutuhan perpindahan contentOffset
. Kami menghitung titik untuk rightItemView
:, self.rightItemView.frame.maxX — gap
setelah persimpangan yang kita akan bergeser contentOffset
. Misalnya, jika rightItemView
100.0 poin tetap digulir ke tampilan penuh , maka kami menggeser contentOffset
mundur, dengan lebar satu spanduk, dengan mempertimbangkan jarak antara spanduk (spasi), sehingga ada centerItemView
di tempat rightItemView
. Demikian pula, kita lakukan untuk leftItemView
: kita menghitung titik, setelah persimpangan yang akan kita ubah contentOffset
.Tambahkan metode func set(imageURLs: [URL])
untuk mengekspos data untuk ditampilkan di luar. Di sana kami mentransfer sebagian kode dari setup
.Dan kami juga akan menambahkan baris sehingga ketika Anda mengatur konten itu centerItemView
langsung terpusat. horizontalItemOffsetFromSuperView
kami sudah menggunakannya layoutSubviews
, jadi kami memasukkannya ke dalam konstanta dan menggunakannya lagi.func set(imageURLs: [URL]) {
let imageViews = [self.leftItemView, self.centerItemView, self.rightItemView]
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
Kami akan memanggil metode ini di luar di UIViewController.viewDidAppear
. Atau Anda dapat memindahkan penyelarasan pertama layoutSubviews
, tetapi verifikasi bahwa ini akan dilakukan hanya ketika mengubah bingkai seluruh tampilan. Untuk menunjukkan pekerjaan, kami akan menggunakan metode pertama: Jadi ... Dengan scroll yang tajam, centering pecah.Faktanya adalah bahwa dengan scrolling yang kuat itu diabaikan targetContentOffset
. Mari kita tingkatkan contentInset
, setelah itu semuanya bekerja dengan benar. Pandangan sentral akan selalu terpusat.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 300.0, bottom: 0.0, right: 300.0)
Kami mengganti konten
Tugasnya adalah untuk contentOffset
mengganti konten tampilan di offset pada saat bersamaan. Saat menggulir ke kanan, gambar kanan akan menjadi pusat, pusat akan menjadi kiri, dan kiri akan kanan. 1 - 2 - 3 | 2 - 3 - 1.Untuk kenyamanan, buat ViewModel:struct BannersViewModel {
let items: [URL] = ImageURLFactory.makeImageURLS()
}
Untuk memeriksa elemen mana yang sekarang ada di tengah, tambahkan variabel ke BannersView
dan variabel dengan konten untuk setiap tampilan: private var currentCenterItemIndex: Int = 0
private var viewModel: BannersViewModel?
private var leftItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let leftIndex = items.index(before: self.currentCenterItemIndex)
return leftIndex < 0 ? items.last! : items[leftIndex]
}
private var centerItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
return items[self.currentCenterItemIndex]
}
private var rightItemViewModel: URL {
guard let items = self.viewModel?.items else { fatalError("not ready") }
let rightIndex = items.index(after: self.currentCenterItemIndex)
return rightIndex >= items.count ? items.first! : items[rightIndex]
}
leftItemViewModel
, centerItemViewModel
, rightItemViewModel
- atas dasar currentCenterItemIndex
konten yang relevan kembali untuk setiap tampilan. force unwrap
dan di fatal
sini kita menggunakannya karena jumlah elemen ≥ 3 (jika diinginkan, Anda dapat menambahkan tanda centang pada metode set
).Tambahkan metode yang akan dipanggil jika perlu untuk mengubah konten tampilan: func nextItem() {
self.currentCenterItemIndex += 1
if self.viewModel?.items.count == self.currentCenterItemIndex {
self.currentCenterItemIndex = 0
}
self.updateViews()
}
func prevItem() {
self.currentCenterItemIndex -= 1
if self.currentCenterItemIndex == -1 {
self.currentCenterItemIndex = self.viewModel?.items.indices.last ?? 0
}
self.updateViews()
}
private func updateViews() {
self.leftItemView.setImage(with: self.leftItemViewModel)
self.centerItemView.setImage(with: self.centerItemViewModel)
self.rightItemView.setImage(with: self.rightItemViewModel)
}
Ubah metode yang digunakan secara eksternal untuk mengekspos konten: func set(viewModel: BannersViewModel) {
self.viewModel = viewModel
self.updateViews()
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
Dan kami akan menelepon nextItem
, dan prevItem
dalam metode delegasi ketika mengubah contentOffset
: func scrollViewDidScroll(_ scrollView: UIScrollView) {
.......
if (self.rightItemView.frame.maxX - gap) < currentRightOffset {
scrollView.contentOffset.x -= self.centerItemView.frame.width + spacing
self.nextItem()
} else if (self.leftItemView.frame.minX + gap) > scrollView.contentOffset.x {
scrollView.contentOffset.x += self.centerItemView.frame.width + spacing
self.prevItem()
}
}
Mari kita tingkatkan jumlah tautan input ke gambar 5 (untuk kenyamanan ada tiga):Langkah terakhir
Tetap membuat kustom UIView
alih-alih gambar sederhana. Ini akan menjadi judul, subtitle dan gambar.Perpanjang ViewModel
:struct BannersViewModel {
let items: [Item]
struct Item {
let title: String
let subtitle: String
let imageUrl: URL
}
}
Dan tulis implementasi spanduk:extension BannersView {
final class ItemView: UIView {
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
init() {
super.init(frame: .zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.addSubview(self.imageView)
self.addSubview(self.titleLabel)
self.addSubview(self.subtitleLabel)
self.imageView.contentMode = .scaleAspectFill
self.layer.masksToBounds = true
self.layer.cornerRadius = 8.0
}
func set(viewModel: BannersViewModel.Item) {
self.titleLabel.text = viewModel.title
self.subtitleLabel.text = viewModel.subtitle
self.imageView.setImage(with: viewModel.imageUrl)
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView.frame = self.bounds
self.titleLabel.frame.origin = CGPoint(x: 16.0, y: 16.0)
self.titleLabel.frame.size = CGSize(width: self.bounds.width - 32.0, height: 20.0)
self.subtitleLabel.frame.origin = CGPoint(x: 16.0, y: self.titleLabel.frame.maxY + 4.0)
self.subtitleLabel.frame.size = self.titleLabel.frame.size
}
}
}
Ganti UIImageView
dan ViewModel
di BannersView
::
.......
private let leftItemView = ItemView()
private let centerItemView = ItemView()
private let rightItemView = ItemView()
private var leftItemViewModel: BannersViewModel.Item { ... }
private var centerItemViewModel: BannersViewModel.Item { ... }
private var rightItemViewModel: BannersViewModel.Item { ... }
.......
private func updateViews() {
self.leftItemView.set(viewModel: self.leftItemViewModel)
self.centerItemView.set(viewModel: self.centerItemViewModel)
self.rightItemView.set(viewModel: self.rightItemViewModel)
}
.......
Hasil:temuan
Membuat gulungan perulangan tanpa akhir dengan spanduk ternyata menjadi tugas yang menarik. Saya yakin bahwa setiap orang akan dapat menarik kesimpulan mereka sendiri atau mengambil ide dari keputusan kami untuk mengelola dengan hanya tiga yang dapat digunakan kembali UIView
.Sekali lagi, kami yakin bahwa solusi yang muncul pertama kali dan solusi yang dapat Anda temukan di Internet tidak selalu optimal. Pada awalnya, kami takut bahwa mengganti konten selama pengguliran akan menyebabkan masalah, tetapi semuanya berjalan lancar. Jangan takut untuk mencoba pendekatan Anda jika Anda pikir ini benar. Pikirkan dengan kepalamu sendiri :).