I have a custom UIImageView that acts like a 'carousel' in that users can swipe it to see an image (which, by the way, I adapted from this excellent post on Medium.
I want the corners to be rounded to 20, but I can't find the correct value for the imageView's content mode.
What I want to happen is for the image to scale to fill the view and retain its aspect, which I would normally use .scaleAspectFill for. But due to the way this custom view is set up, it turns into this bizarre mess, as you can see.
I've pasted the custom class below - does anyone have any ideas?
class ImageCarouselView: UIView {
private var 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)
var topView = UIView()
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)
}()
convenience init(_ images: [UIImage?]) {
self.init()
self.images = images
self.setUpActions()
}
init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
self.layer.cornerRadius = 20
self.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func load(images: [UIImage?]) {
print("ImageCarouselView - Laod Images")
self.images = images
self.setUpActions()
}
private func setUpActions() {
setupLayout()
setupSwipeRecognizer()
setupImages()
}
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() {
print(images.count)
guard images.count > 0 else { return }
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 = .scaleAspectFit
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.init(white: 0.3, alpha: 1)
return view
}
}



There are many ways to create a "Carousel" view ... this is an interesting approach. It doesn't allow "dragging left-right" - only swiping - but if that's the desired goal then fine.
Couple things it's doing wrong though...
First:
is a very bad idea. The class will not work unless the view is, actually, the full width of the screen. It also won't adapt to frame changes (such as on device rotation). And, it will fail miserably if the app is running in Multitasking Mode on an iPad, for example.
So, let's use the view width instead, and update it in
layoutSubviews():Next, the code creates a new image view and completely rebuilds the view hierarchy on every swipe... which is a lot more processing than needed.
Instead, we can re-position the existing image views and update their
.imageproperties on swipe-animation completion.So, if we assume we start with this (the red-dashed line is the frame of the "slide show view"):
the question is - what should have rounded corners?
Consider these images during the animation:
How you want the corners to look during the animation will determine whether we round the corners of the image views, the view itself, both, or neither.
To simplify the code a little bit more, we can constrain
so they "stick together" ... now we only need to manage One "dynamic constraint."
Here is a modified version of your
ImageCarouselViewclass:and an example view controller: