Cada desarrollador para plataformas móviles se enfrenta constantemente a una tarea que no se puede resolver de una sola manera. Siempre hay varias formas, algunas rápidas, otras complicadas, y cada una tiene sus propias ventajas y desventajas.El desplazamiento sin fin / cíclico en iOS no se puede implementar usando herramientas estándar, debe ir a diferentes trucos. En este artículo, le diré qué opciones para resolver el problema se encuentran en la superficie y qué opción implementamos eventualmente.Tarea
Era necesario hacer un desplazamiento cíclico sin fin con elementos en forma de una imagen preparada, título y subtítulo. Entrada: la imagen central se sangra de la pantalla en 16.0 puntos. A los lados de la imagen central, sobresalen las "orejas". Y la distancia entre las pancartas es de 8.0 puntos.Estudiamos lo que han hecho los colegas.
Dodo: las pancartas no son cíclicas, la pancarta central siempre está adyacente al borde izquierdo a cierta distancia.Auto.ru: las pancartas no son cíclicas; si desliza el dedo, las pancartas se vuelven durante mucho tiempo.Ozon: las pancartas son cíclicas, pero no se pueden controlar con el tacto: una vez que se determina la dirección del deslizamiento, la imagen ya no se puede detener.Arándanos silvestres: los banners no son cíclicos, se produce el centrado y se completa la animación de desplazamiento.Deseos finales:- Las pancartas deben estar centradas.
- El desplazamiento debería finalizar sin una larga espera para la animación.
- Capacidad de administración de pancartas: debe haber la capacidad de controlar la animación de desplazamiento con el dedo.
Opciones de implementación
Cuando surge una nueva tarea que aún no se ha resuelto, vale la pena considerar las soluciones y enfoques existentes.UICollectionView
Actuando en la frente, puede llegar a la opción de crear UICollectionView
. Hacemos el número de elementos Int.max
y durante la inicialización mostramos el medio, y cuando llamamos al método en dataSource
- func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell
. Devolveremos el elemento correspondiente, suponiendo que el elemento cero es este Int.max / 2
. Tal monstruo con un montón de características, como UICollectionView
, es inapropiado de usar para nuestra tarea simple.UIScrollView y (n + 2) UIView
Todavía hay una opción en la que se crea UIScrollView, absolutamente todos los banners se colocan en él, y se agrega al principio y al final mediante el banner. Cuando lo terminamos hasta el final, cambiamos el desplazamiento de manera imperceptible para el usuario y volvemos al primer elemento. Y al desplazarse hacia atrás, hacemos lo contrario. Como resultado, con una gran cantidad de elementos, se crearán un montón de vistas sin reutilizarlas.FuentePropia manera
Decidimos hacer UIScrollView + tres UIView. Estas UIView serán reutilizadas. En el momento del desplazamiento, volveremos contentOffset
al banner central y reemplazaremos el contenido de los tres UIView. Y luego debe obtener un componente ligero que cierre esta tarea.Sin embargo, existe la preocupación de que la suplantación de contenido durante el desplazamiento sea notoria para el usuario. Aprendemos sobre esto durante la implementación.Implementación
Preparando UIScrollView y Three UIImageView
Crea un heredero UIView
, coloca UIScrollView
tres en él 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)
}
}
Agregue la implementación del método con la configuración scrollView
:Después de la configuración básica, podemos hacer el diseño y la ubicación 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)
}
Agregue a las UIImageView
imágenes que extraeremos del sitio del generador de imágenes https://placeholder.com : let imageURLs = ImageURLFactory.makeImageURLS()
imageViews.enumerated().forEach { key, view in
view.setImage(with: imageURLs[key])
}
El resultado de los primeros pasos preparatorios:Centrar imágenes al desplazarse
Para controlar el desplazamiento usaremos UIScrollViewDelegate
. setup
Configuramos el delegado para el método UIScrollView
, y también configuramos contentInset
la primera y la última imagen para que tengan sangrías en los lados.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0)
self.scrollView.delegate = self
Creamos extension
para nuestro BannersView
uno de los métodos. Se func scrollViewWillEndDragging
llama al método delegado cuando el usuario deja de desplazarse. En este método, nos interesa targetContentOffset
: esta es la variable responsable del desplazamiento final del desplazamiento (el punto en el que se detiene el desplazamiento).
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 es la distancia a la que asumiremos que la vista es central. Si se muestra un tercio del ancho de la imagen naranja en la pantalla, estableceremos el desplazamiento final para que la imagen naranja esté en el centro.targetRightOffsetX
- Este punto ayudará a determinar si la vista correcta es central.El resultado de la implementación de este método:Administrar el desplazamiento mientras se desplaza
Ahora, justo durante el desplazamiento, cambiaremos contentOffset
, volviendo al centro de la pantalla la vista central. Esto permitirá al usuario crear una ilusión de desplazamiento infinito inadvertido.Agregue un método delegado func scrollViewDidScroll(_ scrollView: UIScrollView)
, se llama cuando contentOffset
y cambia 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 es la distancia desde la cual determinaremos la necesidad de desplazamiento contentOffset
. Calculamos el punto para rightItemView
:, self.rightItemView.frame.maxX — gap
después de la intersección de la cual cambiaremos contentOffset
. Por ejemplo, si rightItemView
quedan 100.0 puntos para desplazarse a pantalla completa , entonces contentOffset
retrocedemos, por el ancho de una pancarta, teniendo en cuenta la distancia entre las pancartas (espaciado), para que esté centerItemView
en su lugar rightItemView
. Del mismo modo, lo hacemos para leftItemView
: calculamos el punto, después de la intersección de la cual cambiaremos contentOffset
.Agregue un método func set(imageURLs: [URL])
para exponer datos para su visualización en exteriores. Allí transferimos parte del código de setup
.Y también agregaremos una línea para que cuando establezca el contenido se centerItemView
centre inmediatamente. horizontalItemOffsetFromSuperView
ya lo usamos layoutSubviews
, así que lo ponemos en constantes y lo usamos nuevamente.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
}
Llamaremos a este método de afuera hacia adentro UIViewController.viewDidAppear
. O puede mover la primera alineación a layoutSubviews
, pero verifique que esto se haga solo cuando cambie el marco de toda la vista. Para demostrar el trabajo, usaremos el primer método: Entonces ... Con un pergamino afilado, el centrado se rompió.El hecho es que con un desplazamiento fuerte se ignora targetContentOffset
. Vamos a aumentar contentInset
, después de eso todo funciona correctamente. La vista central siempre estará centrada.self.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 300.0, bottom: 0.0, right: 300.0)
Reemplazamos contenido
La tarea es contentOffset
reemplazar el contenido de la vista en el desplazamiento al mismo tiempo. Al desplazarse hacia la derecha, la imagen derecha se convertirá en central, el centro se convertirá en izquierda y la izquierda será derecha. 1 - 2 - 3 | 2 - 3 - 1.Para mayor comodidad, cree un ViewModel:struct BannersViewModel {
let items: [URL] = ImageURLFactory.makeImageURLS()
}
Para verificar qué elemento está ahora en el centro, agregue una variable BannersView
y variables con contenido para cada una de las vistas: 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
- sobre la base de currentCenterItemIndex
un contenido relevante de retorno para cada vista. force unwrap
y fatal
aquí lo usamos porque el número de elementos es ≥ 3 (si lo desea, puede agregar un cheque al método set
).Agregue métodos que se llamarán si es necesario para cambiar el contenido de las vistas: 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)
}
Cambie el método utilizado externamente para exponer contenido: func set(viewModel: BannersViewModel) {
self.viewModel = viewModel
self.updateViews()
self.scrollView.contentOffset.x = self.centerItemView.frame.minX - Constants.horizontalItemOffsetFromSuperView
}
Y llamaremos nextItem
, y prevItem
en el método al delegado al cambiar 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()
}
}
Aumentemos el número de enlaces de entrada a las imágenes a 5 (por conveniencia, hubo tres):Pasos finales
Queda por hacer UIView
una imagen personalizada en lugar de una simple. Este será el título, subtítulo e imagen.Ampliar ViewModel
:struct BannersViewModel {
let items: [Item]
struct Item {
let title: String
let subtitle: String
let imageUrl: URL
}
}
Y escriba una implementación 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
}
}
}
Reemplazar UIImageView
y ViewModel
en 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:recomendaciones
Hacer un desplazamiento en bucle sin fin con pancartas resultó ser una tarea interesante. Estoy seguro de que todos podrán sacar sus propias conclusiones o sacar cualquier idea de nuestra decisión de administrar con solo tres reutilizables UIView
.Una vez más, estábamos convencidos de que las soluciones que vienen a la mente primero y las soluciones que puede encontrar en Internet no siempre son óptimas. Al principio, temíamos que reemplazar el contenido durante el desplazamiento condujera a un problema, pero todo funcionó sin problemas. No tenga miedo de probar sus enfoques si cree que esto es correcto. Piensa con tu propia cabeza :).