How to create horizontal carousel in iOS 10 Swift 3
Short answer
You should use a UICollectionView.
More detailed answer
1) Setup the horizontal layout with UICollectionViewFlowLayout
let layout = UICollectionViewFlowLayout()
// The size of each item. Pick a suitable height so that the items do not get stacked:
layout.itemSize = CGSize(width: view.frame.width / 2, height: view.frame.height)
// The most important part:
layout.scrollDirection = .horizontal
// Then initialize collectionView
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
2) Setup scaling (pseudocode)
i. Have self
implement UIScrollViewDelegate
method scrollViewDidScroll(UIScrollView)
ii. Set collectionView.delgate = self
iii. In scrollViewDidScroll(UIScrollView)
, get all visible cells by casting scrollView as! UICollectionView
and getting visibleCells
property.
iv. For cell in visible cells, set appropriate scale (of the cell contents, not of the cell itself) with CGAffineTransform
given cell's distance from the horizontal center of the screen. This may take some arithmetic; this should work just fine:
s = −0.00005 * pow(cell.center.x, 2) + 1.5
Hope this helps! Feel free to ask for clarification if you need it.
I have tried CollectionView for the carousel slideshow, but it didn't work out for me. I didn't like the hackish ways I had to do to make it show images in one row and I also didn't like the fact that it cannot return the active image (there is some workaround here as well, but they don't seem reliable). So, naturally, I ended up building a custom slideshow carousel for my purpose. I will share the code here, so hopefully, it can help(or at least guide someone) with a similar problem.
NOTE: My carousel is full width, singleImagePerScreen carousel, with a swipe recognizer to swipe through images and delegate function that is triggered when an image is active(I use it to display active image - "1 of 5").
TESTED ON: SWIFT 5, XCode 12.2, iOS 14.2
// ImageCarouselView class
import UIKit
class ImageCarouselView: UIView {
private let images: [UIImage?]
private var index = 0
private let screenWidth = UIScreen.main.bounds.width
var delegate: ImageCarouselViewDelegate?
lazy var previousImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var currentImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var nextImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var previousImageLeadingConstraint: NSLayoutConstraint = {
return previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
}()
lazy var currentImageLeadingConstraint: NSLayoutConstraint = {
return currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
}()
lazy var nextImageLeadingConstraint: NSLayoutConstraint = {
return nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
}()
init(_ images: [UIImage?]) {
self.images = images
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
setupLayout()
setupImages()
setupSwipeRecognizer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupLayout() {
self.subviews.forEach({ $0.removeFromSuperview() })
addSubview(previousImageView)
addSubview(currentImageView)
addSubview(nextImageView)
previousImageLeadingConstraint = previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
currentImageLeadingConstraint = currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
nextImageLeadingConstraint = nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
NSLayoutConstraint.activate([
previousImageLeadingConstraint,
previousImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
previousImageView.widthAnchor.constraint(equalToConstant: screenWidth),
currentImageLeadingConstraint,
currentImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
currentImageView.widthAnchor.constraint(equalToConstant: screenWidth),
nextImageLeadingConstraint,
nextImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
nextImageView.widthAnchor.constraint(equalToConstant: screenWidth),
])
}
private func setupImages() {
currentImageView.image = images[self.index]
guard images.count > 1 else { return }
if (index == 0) {
previousImageView.image = images[images.count - 1]
nextImageView.image = images[index + 1]
}
if (index == (images.count - 1)) {
previousImageView.image = images[index - 1]
nextImageView.image = images[0]
}
}
private func setupSwipeRecognizer() {
guard images.count > 1 else { return }
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
leftSwipe.direction = .left
rightSwipe.direction = .right
self.addGestureRecognizer(leftSwipe)
self.addGestureRecognizer(rightSwipe)
}
@objc private func handleSwipes(_ sender: UISwipeGestureRecognizer) {
if (sender.direction == .left) {
showNextImage()
}
if (sender.direction == .right) {
showPreviousImage()
}
}
private func showPreviousImage() {
previousImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.nextImageView = self.currentImageView
self.currentImageView = self.previousImageView
self.previousImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == 0 ? self.images.count - 1 : self.index - 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.previousImageView.image = self.index == 0 ? self.images[self.images.count - 1] : self.images[self.index - 1]
self.setupLayout()
})
}
private func showNextImage() {
nextImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = -screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.previousImageView = self.currentImageView
self.currentImageView = self.nextImageView
self.nextImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == (self.images.count - 1) ? 0 : self.index + 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.nextImageView.image = self.index == (self.images.count - 1) ? self.images[0] : self.images[self.index + 1]
self.setupLayout()
})
}
func imageView(image: UIImage? = nil, contentMode: UIImageView.ContentMode) -> UIImageView {
let view = UIImageView()
view.image = image
view.contentMode = contentMode
view.translatesAutoresizingMaskIntoConstraints = false
return view
}
}
// ImageCarouselViewDelegate
import UIKit
protocol ImageCarouselViewDelegate: NSObjectProtocol {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int)
}
// Usage
let slideshowView = ImageCarouselView(images) // initialize
self.slideshowView.delegate = self // set delegate in viewDidLoad()
extension YourViewController: ImageCarouselViewDelegate {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int) {
// do something with index
}
}