Cari95 Cari95 - 20 days ago 8
Swift Question

Cube transition between UIImages

I would like to create a view that when scrolled horizontally transitions between an array of

UIImage
objects with a cube animation effect. For example:

enter image description here

Can someone please point me in the right direction on how I can scroll horizontally through an array of
UIImage
objects with a cubical transition animation in Swift?

Answer

It is far too broad to explain but you can use this UIViewController:

class CubeScrollViewController: UIViewController,UIScrollViewDelegate {
    var scrollView:UIScrollView?
    var images:[UIImage] = [UIImage]()
    var imageViews:[IntegerLiteralType:UIImageView] = [IntegerLiteralType:UIImageView]()
    var currentIndex = 0
    var scrollOffset:CGFloat = 0.0
    var previousOffset:CGFloat = 0.0
    var suppressScrollEvent:Bool = false
    var add = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        self.images = [UIImage(named: "image1")!,UIImage(named: "image2")!,UIImage(named:"image3")!,UIImage(named: "image4")!]

    }

    override func viewDidLayoutSubviews()
    {
        super.viewDidLayoutSubviews()
        scrollView?.removeFromSuperview()
        scrollView = UIScrollView(frame: self.view.frame)
        scrollView?.autoresizingMask = [.FlexibleWidth,.FlexibleHeight]
        scrollView?.showsHorizontalScrollIndicator = true
        scrollView?.pagingEnabled = true
        scrollView?.directionalLockEnabled = true;
        scrollView?.autoresizesSubviews = false;
        scrollView?.delegate = self
        self.view.addSubview(scrollView!)
        var index = 0
        for image in self.images
        {
            let imageView = UIImageView(frame: self.view.frame)
            imageView.contentMode = .ScaleAspectFill
            imageView.clipsToBounds = true
            imageView.image = image
            imageView.backgroundColor = UIColor.whiteColor()
            self.imageViews[index] = imageView
            index += 1
        }
        var pages = self.images.count
        if self.images.count > 1
        {
            pages += 2
        }
        self.suppressScrollEvent = true
        self.scrollView?.contentSize = CGSize(width: self.view.bounds.size.width * CGFloat(pages), height: self.view.bounds.size.height)
        self.suppressScrollEvent = false
        self.updateContentOffset()
        self.loadUnloadImageViews()
        self.updateLayout()
    }

    func setCurrentImageIndex(currentImageIndex:IntegerLiteralType)
    {
        self.scrollToImageAtIndex(currentImageIndex,animated:true)
    }

    func scrollToImageAtIndex(index:IntegerLiteralType,animated:Bool)
    {
        var offset = index
        if offset > self.images.count
        {
            offset = offset % self.images.count
        }
        offset = max(-1, offset)+1
        scrollView?.setContentOffset(CGPoint(x: self.view.bounds.size.width * CGFloat(offset),y: 0),animated: animated)
    }

    func scrollForward(animated:Bool)
    {
        self.scrollToImageAtIndex(self.currentIndex+1, animated: animated)
    }

    func scrollBack(animated:Bool)
    {
        self.scrollToImageAtIndex(self.currentIndex-1, animated: animated)
    }

    func reloadData()
    {
        for view:UIImageView in self.imageViews.values
        {
            view.removeFromSuperview()
        }
    }

    func reloadImageAtIndex(index:IntegerLiteralType,animated:Bool)
    {
        let image = self.images[index]
        let oldImageView = self.imageViews[index]
        let imageView = UIImageView(frame: self.view.frame)
        imageView.contentMode = .ScaleAspectFill
        imageView.clipsToBounds = true
        imageView.image = image
        imageView.backgroundColor = UIColor.whiteColor()
        let transform = imageView.layer.transform
        let center = imageView.center

        if animated
        {
            let animation = CATransition()
            animation.type = kCATransitionFade
            self.scrollView?.layer.addAnimation(animation, forKey: nil)
        }
        oldImageView!.removeFromSuperview()
        self.scrollView?.addSubview(imageView)
        imageView.layer.transform = transform
        imageView.center = center
    }

    func updateContentOffset()
    {
        var offset = self.scrollOffset
        if self.images.count>1
        {
            offset+=1.0
            while offset<1.0
            {
                offset+=1.0
            }
            while offset>=CGFloat(self.images.count+1)
            {
                offset-=CGFloat(self.images.count)
            }
        }
        self.previousOffset = offset

        self.suppressScrollEvent = true
        self.scrollView?.contentOffset = CGPointMake(self.view.bounds.size.width*offset, 0.0)
        self.suppressScrollEvent = false
    }

    func updateLayout()
    {
        for index in self.imageViews.keys
        {
            let imageView = self.imageViews[index]
            if imageView != nil && imageView!.superview == nil
            {
                imageView?.layer.doubleSided = false
                self.scrollView?.addSubview(imageView!)
                self.add++
            }
            var angle = (self.scrollOffset - CGFloat(index)) * CGFloat(M_PI_2)
            while angle < 0
            {
                angle = angle + CGFloat(M_PI * 2.0)
            }
            while angle > CGFloat(M_PI*2.0)
            {
                angle = angle - CGFloat(M_PI * 2.0)
            }
            var transform = CATransform3DIdentity
            if angle != 0.0
            {
                transform.m34 = -1.0/500;
                transform = CATransform3DTranslate(transform, 0.0, 0.0, -self.view.bounds.size.width / 2.0)
                transform = CATransform3DRotate(transform, -angle, 0, 1, 0)
                transform = CATransform3DTranslate(transform, 0, 0, self.view.bounds.size.width / 2.0)
            }

            imageView?.bounds = self.view.bounds
            imageView?.center = CGPoint(x: self.view.bounds.size.width * 0.5 + self.scrollView!.contentOffset.x, y: self.view.bounds.size.height * 0.5);
            imageView?.layer.transform = transform
        }
    }

    func loadUnloadImageViews()
    {
        var visibleIndices = [IntegerLiteralType]()
        visibleIndices.append(self.currentIndex)
        visibleIndices.append(self.currentIndex + 1)

        if self.currentIndex > 0
        {
            visibleIndices.append(self.currentIndex - 1)
        }
        else
        {
            visibleIndices.append(-1)
        }

        for index in 0...self.images.count
        {
            if !visibleIndices.contains(index)
            {
                let imageView = self.imageViews[index]
                imageView?.removeFromSuperview()
                self.imageViews.removeValueForKey(index)
            }
        }
        for index in visibleIndices
        {
            var imageView:UIImageView? = nil
            if self.imageViews[index] != nil
            {
                imageView = self.imageViews[index]!
            }
            if imageView == nil && self.images.count > 0
            {
                let newIndex = (index + self.images.count) % self.images.count
                let imageView = UIImageView(frame: self.view.frame)
                imageView.contentMode = .ScaleAspectFill
                imageView.clipsToBounds = true
                imageView.backgroundColor = UIColor.whiteColor()
                imageView.image = self.images[newIndex]
                self.imageViews[index] = imageView
            }
        }

    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if !self.suppressScrollEvent
        {
            let offset:CGFloat = scrollView.contentOffset.x / self.view.bounds.size.width
            self.scrollOffset += (offset - self.previousOffset)
            while self.scrollOffset < 0.0
            {
                self.scrollOffset += CGFloat(self.images.count)
            }
            while self.scrollOffset >= CGFloat(self.images.count)
            {
                self.scrollOffset -= CGFloat(self.images.count)
            }
            self.previousOffset = offset

            if offset - floor(offset) == 0.0
            {
                self.scrollOffset = round(self.scrollOffset)
            }

            self.currentIndex = max(0, min(self.images.count - 1, IntegerLiteralType(round(self.scrollOffset))))
            self.updateContentOffset()
            self.loadUnloadImageViews()
            self.updateLayout()

        }
    }

    func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
        let nearestIntegralOffset = round(self.scrollOffset)
        if abs(self.scrollOffset - nearestIntegralOffset) > 0.0
        {
            self.scrollToImageAtIndex(self.currentIndex, animated: true)
        }
    }

}

Set the images you want in the cube to self.images. The current implementation wraps the images, meaning when you swipe left on the first image the last image appears, and swipe right on last image the first one appears.

Swift 3.0

import UIKit

public class CubeScrollViewController: UIViewController
{
    //MARK: - Properties
    private lazy var scrollView: UIScrollView =
    {
        let scrollView = UIScrollView()
        scrollView.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        scrollView.showsHorizontalScrollIndicator = true
        scrollView.isPagingEnabled = true
        scrollView.isDirectionalLockEnabled = true;
        scrollView.autoresizesSubviews = false;
        scrollView.delegate = self
        return scrollView
    }()
    var images = [UIImage]()
    fileprivate var imageViews = [Int: UIImageView]()
    fileprivate var currentIndex = 0
    fileprivate var scrollOffset: CGFloat = 0.0
    fileprivate var previousOffset: CGFloat = 0.0
    fileprivate var suppressScrollEvent = false

    //MARK: - Lifecycle
    override func viewDidLoad()
    {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        for (index, image) in self.images.enumerated()
        {
            let imageView = UIImageView(image: image)
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            imageView.backgroundColor = UIColor.white
            self.imageViews[index] = imageView
        }
    }

    override func viewDidLayoutSubviews()
    {
        super.viewDidLayoutSubviews()
        self.scrollView.frame = self.view.bounds
        self.imageViews.values.forEach {  $0.frame  = self.view.bounds }
        var pages = CGFloat(self.images.count)
        pages = self.images.count > 1 ? pages + 2 : pages
        self.suppressScrollEvent = true
        self.scrollView.contentSize = CGSize(width: self.view.bounds.width * pages, height: self.view.bounds.height)
        self.suppressScrollEvent = false
        self.updateContentOffset()
        self.loadUnloadViews()
        self.updateLayout()
    }


    //MARK: - Exposed Functions
    func set(_ currentImageIndex: Int)
    {
        self.scrollToImage(at: currentIndex)
    }

    func scrollToImage(at index: Int, animated: Bool = true)
    {
        var offset = index > self.images.count ? index % self.images.count : index
        offset = max(-1, offset) + 1
        self.scrollView.setContentOffset(CGPoint(x: self.view.bounds.width * CGFloat(offset), y: 0.0), animated: animated)
    }

    func scrollForward(animated: Bool = true)
    {
        self.scrollToImage(at: self.currentIndex + 1, animated: animated)
    }

    func scrollBack(animated: Bool = true)
    {
        self.scrollToImage(at: self.currentIndex - 1, animated: animated)
    }

    func reloadData()
    {
        self.imageViews.values.forEach { $0.removeFromSuperview() }
    }

    func reloadImage(at index: Int, animated: Bool = true)
    {
        guard 0 ..< self.images.count ~= index else { return }
        let image = self.images[index]
        let oldImageView = self.imageViews[index]
        let imageView = UIImageView(frame: self.view.bounds)
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.image = image
        imageView.backgroundColor = .white
        let transform = imageView.layer.transform
        let center = imageView.center

        if animated
        {
            let animation = CATransition()
            animation.type = kCATransitionFade
            self.scrollView.layer.add(animation, forKey: nil)
        }
        oldImageView?.removeFromSuperview()
        self.scrollView.addSubview(imageView)
        imageView.layer.transform = transform
        imageView.center = center
    }

    //MARK: - Layout
    fileprivate func updateContentOffset()
    {
        guard self.images.count > 1 else { return }

        var offset = self.scrollOffset
        offset += 1.0
        while offset < 1.0
        {
            offset += 1.0
        }
        while offset >= CGFloat(self.images.count + 1)
        {
            offset -= CGFloat(self.images.count)
        }
        self.previousOffset = offset

        self.suppressScrollEvent = true
        self.scrollView.contentOffset = CGPoint(x: self.view.bounds.width * offset, y: 0.0)
        self.suppressScrollEvent = false

    }

    fileprivate func updateLayout()
    {
        for index in self.imageViews.keys
        {
            guard let imageView = self.imageViews[index] else { continue }
            if imageView.superview == nil
            {
                imageView.layer.isDoubleSided = false
                self.scrollView.addSubview(imageView)
            }

            var angle = (self.scrollOffset - CGFloat(index)) * CGFloat.pi * 0.5
            while angle < 0
            {
                angle += CGFloat.pi * 2.0
            }
            while angle > CGFloat.pi * 2.0
            {
                angle -= CGFloat.pi * 2.0
            }

            var transform = CATransform3DIdentity
            if angle != 0.0
            {
                transform.m34 = -1.0 / 500.0
                transform = CATransform3DTranslate(transform, 0.0, 0.0, -self.view.bounds.width * 0.5)
                transform = CATransform3DRotate(transform, -angle, 0, 1, 0)
                transform = CATransform3DTranslate(transform, 0, 0, self.view.bounds.width * 0.5)
            }

            imageView.bounds = self.view.bounds
            imageView.center = CGPoint(x: self.view.bounds.midX + self.scrollView.contentOffset.x, y: self.view.bounds.midY)
            imageView.layer.transform = transform
        }
    }

    fileprivate func loadUnloadViews()
    {
        var visibleIndices = [Int]()
        visibleIndices.append(self.currentIndex)
        visibleIndices.append(self.currentIndex + 1)

        if self.currentIndex > 0
        {
            visibleIndices.append(self.currentIndex - 1)
        }
        else
        {
            visibleIndices.append(-1)
        }

        for index in 0 ..< self.images.count
        {
            guard !visibleIndices.contains(index) else { continue }

            let imageView = self.imageViews[index]
            imageView?.removeFromSuperview()
            self.imageViews.removeValue(forKey: index)
        }
        for index in visibleIndices
        {
            if let _ = self.imageViews[index]
            {

            }
            else if self.images.count > 0
            {
                let newIndex = (index + self.images.count) % self.images.count
                let imageView = UIImageView(frame: self.view.bounds)
                imageView.contentMode = .scaleAspectFill
                imageView.clipsToBounds = true
                imageView.backgroundColor = .white
                imageView.image = self.images[newIndex]
                self.imageViews[index] = imageView
            }
        }
    }
}

// MARK: - UIScrollViewDelegate
extension CubeScrollViewController: UIScrollViewDelegate
{
    func scrollViewDidScroll(_ scrollView: UIScrollView)
    {
        guard !self.suppressScrollEvent else { return }

        let offset: CGFloat = scrollView.contentOffset.x / self.view.bounds.width
        self.scrollOffset += (offset - self.previousOffset)
        while self.scrollOffset < 0.0
        {
            self.scrollOffset += CGFloat(self.images.count)

        }
        while self.scrollOffset >= CGFloat(self.images.count)
        {
            self.scrollOffset -= CGFloat(self.images.count)

        }
        self.previousOffset = offset

        if offset - floor(offset) == 0.0
        {
            self.scrollOffset = round(self.scrollOffset)

        }
        self.currentIndex = max(0, min(self.images.count - 1, Int(round(self.scrollOffset))))
        self.updateContentOffset()
        self.loadUnloadViews()
        self.updateLayout()
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
    {
        let nearestIntegralOffset = round(self.scrollOffset)
        guard abs(self.scrollOffset - nearestIntegralOffset) > 0.0 else { return }
        self.scrollToImage(at: self.currentIndex)
    }
}
Comments