Cada desenvolvedor de plataformas móveis é constantemente confrontado com uma tarefa que não pode ser resolvida de uma única maneira. Sempre existem várias maneiras, algumas rápidas, outras complicadas, e cada uma tem suas próprias vantagens e desvantagens.A rolagem interminável / cíclica no iOS não pode ser implementada usando ferramentas padrão; você precisa fazer truques diferentes. Neste artigo, mostrarei quais opções para resolver o problema estão na superfície e quais opções foram implementadas.Tarefa
Era necessário fazer uma rolagem cíclica sem fim com elementos na forma de uma imagem, título e legenda preparados. Entrada: a imagem central é recuada da tela em 16,0 pontos. Nas laterais da imagem central, as “orelhas” se destacam. E a distância entre os banners é de 8,0 pontos.Estudamos o que foi feito por colegas
Dodo - banners não são cíclicos, o banner central é sempre adjacente à borda esquerda a uma certa distância.Auto.ru - os banners não são cíclicos; se você deslizar, os banners são exibidos por um longo período de tempo.Os banners de ozônio são cíclicos, mas não podem ser controlados pelo toque: depois que a direção do furto é determinada, a imagem não pode mais ser interrompida.Wildberries - banners não são cíclicos, ocorre centralização, conclusão longa da animação de rolagem.Desejos finais:- As faixas devem estar centralizadas.
- A rolagem deve terminar sem uma longa espera pela animação.
- Gerenciamento de banners: deve haver a capacidade de controlar a animação de rolagem com o dedo.
Opções de implementação
Quando surge uma nova tarefa que ainda não foi resolvida, vale a pena considerar as soluções e abordagens existentes.UICollectionView
Atuando na testa, você pode optar por criar UICollectionView
. Fazemos o número de elementos Int.max
e, durante a inicialização, mostramos o meio e, ao chamar o método em dataSource
- func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell
. Retornaremos o elemento correspondente, assumindo que o elemento zero seja esse Int.max / 2
. Um monstro com muitos recursos, como UICollectionView
, é inapropriado para nossa simples tarefa.UIScrollView e (n + 2) UIView
Ainda existe uma opção na qual o UIScrollView é criado, absolutamente todos os banners são colocados nele e são adicionados ao início e ao fim pelo banner. Quando o terminamos, alteramos o deslocamento imperceptivelmente para o usuário e retornamos ao primeiro elemento. E ao rolar para trás, fazemos o oposto. Como resultado, com um grande número de elementos, um monte de visualizações será criado sem reutilizá-las.FontePróprio caminho
Decidimos fazer o UIScrollView + três UIView. Estes UIView serão reutilizados. No momento da rolagem, retornaremos contentOffset
ao banner central e substituiremos o conteúdo dos três UIView. E então você deve obter um componente leve que feche esta tarefa.No entanto, existe a preocupação de que a falsificação de conteúdo durante a rolagem seja perceptível para o usuário. Aprendemos sobre isso durante a implementação.Implementação
Preparando o UIScrollView e três UIImageView
Crie um herdeiro UIView
, coloque UIScrollView
três nele 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)
}
}
Adicione a implementação do método com a configuração scrollView
:Após a configuração básica, podemos fazer o layout e o posicionamento 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)
}
Adicione às UIImageView
imagens que acessaremos do site gerador de imagens https://placeholder.com : let imageURLs = ImageURLFactory.makeImageURLS()
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
O resultado das primeiras etapas preparatórias:Centralizar imagens ao rolar
Para controlar o pergaminho, usaremos UIScrollViewDelegate
. Definimos setup
o delegado para o método UIScrollView
e também definimos contentInset
a primeira e a última imagens com recuos nas laterais.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0)
self.scrollView.delegate = self
Criamos extension
para o nosso BannersView
um dos métodos. O método delegado func scrollViewWillEndDragging
é chamado quando o usuário para de rolar. Neste método, estamos interessados targetContentOffset
- essa é a variável responsável pelo deslocamento final da rolagem (o ponto em que a rolagem para).
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
- esta é a distância em que assumiremos que a visão é central. Se um terço da largura da imagem laranja for exibido na tela, definiremos o deslocamento final para que a imagem laranja fique no centro.targetRightOffsetX
- Este ponto ajudará a determinar se a visão correta é central.O resultado da implementação deste método:Gerenciar deslocamento ao rolar
Agora, logo durante a rolagem, mudaremos contentOffset
, retornando ao centro da tela a vista central. Isso permitirá que o usuário crie uma ilusão de rolagem infinita despercebida.Adicione um método delegado func scrollViewDidScroll(_ scrollView: UIScrollView)
, chamado quando contentOffset
y for alterado 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
- esta é a distância a partir da qual determinaremos a necessidade de deslocamento contentOffset
. Calculamos o ponto para rightItemView
:, self.rightItemView.frame.maxX — gap
após a interseção da qual mudaremos contentOffset
. Por exemplo, se rightItemView
100.0 pontos ainda tiverem que ser rolados para exibição total , então contentOffset
retrocederemos, pela largura de uma faixa, levando em consideração a distância entre as faixas (espaçamento), para que ela fique centerItemView
no lugar rightItemView
. Da mesma forma, fazemos o seguinte leftItemView
: calculamos o ponto, após a interseção da qual mudaremos contentOffset
.Adicione um método func set(imageURLs: [URL])
para expor os dados para exibição externa. Aí transferimos parte do código de setup
.Além disso, adicionaremos uma linha para que, quando você definir o conteúdo, ele seja centerItemView
imediatamente centralizado. horizontalItemOffsetFromSuperView
como já usamos layoutSubviews
, colocamos em constantes e usamos novamente.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
}
Vamos chamar esse método de fora para dentro UIViewController.viewDidAppear
. Ou você pode mover o primeiro alinhamento para layoutSubviews
, mas verifique se isso será feito apenas ao alterar o quadro de toda a exibição. Para demonstrar o trabalho, usaremos o primeiro método: Então ... Com um rolo afiado, a centralização quebrou.O fato é que, com rolagem forte, é ignorado targetContentOffset
. Vamos aumentar contentInset
, depois disso tudo funciona corretamente. A vista central será sempre centralizada.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 300.0, bottom: 0.0, right: 300.0)
Substituímos o conteúdo
A tarefa é contentOffset
substituir o conteúdo da visualização em deslocamento ao mesmo tempo. Ao rolar para a direita, a imagem direita se tornará central, o centro ficará à esquerda e a esquerda ficará à direita. 1 - 2 - 3 2 - 3 - 1.Por conveniência, crie um ViewModel:struct BannersViewModel {
let items: [URL] = ImageURLFactory.makeImageURLS()
}
Para verificar qual elemento está agora no centro, adicione uma variável BannersView
e variáveis com conteúdo para cada uma das visualizações: 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
- com base em currentCenterItemIndex
conteúdo relevante de retorno para cada vista. force unwrap
e fatal
aqui o usamos porque o número de elementos é ≥ 3 (se desejar, você pode adicionar uma verificação ao método set
).Adicione métodos que serão chamados, se necessário, para alterar o conteúdo das visualizações: 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)
}
Altere o método usado externamente para expor o conteúdo: func set(viewModel: BannersViewModel) {
self.viewModel = viewModel
self.updateViews()
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
E chamaremos nextItem
, e prevItem
no método, o delegado ao alterar 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()
}
}
Vamos aumentar o número de links de entrada para imagens para 5 (por conveniência, foram três):Etapas finais
Resta customizar em UIView
vez de uma imagem simples. Este será o título, legenda e imagem.Estender ViewModel
:struct BannersViewModel {
let items: [Item]
struct Item {
let title: String
let subtitle: String
let imageUrl: URL
}
}
E escreva uma implementação de banner: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
}
}
}
Substitua UIImageView
e ViewModel
entre 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)
}
.......
Resultado:achados
Fazer um rolo interminável com faixas acabou sendo uma tarefa interessante. Estou certo de que todos serão capazes de tirar suas próprias conclusões ou tirar idéias de nossa decisão de administrar com apenas três reutilizáveis UIView
.Mais uma vez, estávamos convencidos de que as soluções que vêm à mente primeiro e as soluções que você pode encontrar na Internet nem sempre são ótimas. No início, tínhamos medo de que a substituição do conteúdo durante a rolagem levasse a um problema, mas tudo funcionava sem problemas. Não tenha medo de tentar suas abordagens, se você acha que isso é certo. Pense com sua própria cabeça :).