I need to apply animation like the bottom sheet where Collection View cells overlap each other when closed, and wide opened when bottom sheet is opened. I have achieved the bottom sheet with Collection View with pagination, but need to apply animation that shows only current cell and other cells behind it when bottom sheet is closed.
Here is my Bottom Sheet Class, and some images as an example what I need to achieve:
class ScrollableBottomSheetViewController: UIViewController {
#IBOutlet var pageControl: UIPageControl!
#IBOutlet var collection: UICollectionView!
#IBOutlet var headerView: UIView!
let fullView: CGFloat = 0//50
var partialView: CGFloat {
return UIScreen.main.bounds.height - 50
}
let collectionMargin = CGFloat(32)
let itemSpacing = CGFloat(10)
let itemHeight = CGFloat(333)
var itemWidth = CGFloat(0)
var currentItem = 5
override func viewDidLoad() {
super.viewDidLoad()
self.definesPresentationContext = true
self.collection.delegate = self
self.collection.dataSource = self
self.collection.register(UINib(nibName:"AdsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "default")
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(ScrollableBottomSheetViewController.panGesture))
gesture.delegate = self
view.addGestureRecognizer(gesture)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
//CGFloat(219) //
itemWidth = UIScreen.main.bounds.width - collectionMargin * 2.0
print(itemWidth)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: itemWidth, height: itemHeight)
layout.headerReferenceSize = CGSize(width: collectionMargin, height: 0)
layout.footerReferenceSize = CGSize(width: collectionMargin, height: 0)
layout.minimumLineSpacing = itemSpacing
layout.scrollDirection = .horizontal
collection!.collectionViewLayout = layout
collection?.decelerationRate = UIScrollViewDecelerationRateFast
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = Float(itemWidth + itemSpacing)
let targetXContentOffset = Float(targetContentOffset.pointee.x)
let contentWidth = Float(collection!.contentSize.width )
var newPage = Float(self.pageControl.currentPage)
if velocity.x == 0 {
newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
} else {
newPage = Float(velocity.x > 0 ? self.pageControl.currentPage + 1 : self.pageControl.currentPage - 1)
if newPage < 0 {
newPage = 0
}
if (newPage > contentWidth / pageWidth) {
newPage = ceil(contentWidth / pageWidth) - 1.0
}
}
self.pageControl.currentPage = Int(newPage)
let point = CGPoint (x: CGFloat(newPage * pageWidth) + itemSpacing, y: targetContentOffset.pointee.y)
targetContentOffset.pointee = point
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.6, animations: { [weak self] in
let frame = self?.view.frame
let yComponent = self?.partialView
self?.view.frame = CGRect(x: 0, y: yComponent!, width: frame!.width, height: frame!.height)// - 100)
})
}
#objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
let y = self.view.frame.minY
if (y + translation.y >= fullView) && (y + translation.y <= partialView) {
self.view.frame = CGRect(x: 0, y: y + translation.y, width: view.frame.width, height: view.frame.height)
recognizer.setTranslation(CGPoint.zero, in: self.view)
}
if recognizer.state == .ended {
var duration = velocity.y < 0 ? Double((y - fullView) / -velocity.y) : Double((partialView - y) / velocity.y)
duration = duration > 1.3 ? 1 : duration
UIView.animate(withDuration: duration, delay: 0.0, options: [.allowUserInteraction], animations: {
if velocity.y >= 0 {
self.view.frame = CGRect(x: 0, y: self.partialView, width: self.view.frame.width, height: self.view.frame.height)
} else {
self.view.frame = CGRect(x: 0, y: self.fullView, width: self.view.frame.width, height: self.view.frame.height)
}
}, completion: { [weak self] _ in
if velocity.y < 0 {
self?.collection.isScrollEnabled = true
}
})
}
}
func prepareBackgroundView() {
let blurEffect = UIBlurEffect.init(style: .dark)
let visualEffect = UIVisualEffectView.init(effect: blurEffect)
let bluredView = UIVisualEffectView.init(effect: blurEffect)
bluredView.contentView.addSubview(visualEffect)
visualEffect.frame = UIScreen.main.bounds
bluredView.frame = UIScreen.main.bounds
view.insertSubview(bluredView, at: 0)
}
}
extension ScrollableBottomSheetViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "default", for: indexPath)
return cell
}
}
extension ScrollableBottomSheetViewController: UIGestureRecognizerDelegate {
// Solution
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
let direction = gesture.velocity(in: view).y
let y = view.frame.minY
if (y == fullView && collection.contentOffset.x == 0 && direction > 0) || (y == partialView) {
collection.isScrollEnabled = false
} else {
collection.isScrollEnabled = true
}
return false
}
}
Related
I hope you all working fine
Here I have created a collection view flow layouts some scenarios not working, please share your references.
here is the collection view
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var collection: UICollectionView!
private let kCellHeight: CGFloat = UIScreen.main.bounds.width/2+38
private let kItemSpace: CGFloat = UIScreen.main.bounds.width/2.08
lazy var expand_click = false
lazy var index_value = [String]()
override func viewDidLoad() {
super.viewDidLoad()
collection.delegate = self
collection.dataSource = self
let layout = StickyCollectionViewFlowLayout2()
layout.minimumLineSpacing = -kItemSpace
collection.collectionViewLayout = layout
collection.reloadData()
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
print("Index",indexPath.row)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width-40, height: kCellHeight)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if (15)-1 == indexPath.row {
self.showAlert(alertText: "", alertMessage: "last Cell")
}else{
if index_value.contains("\(indexPath.row)") {
self.showAlert(alertText: "", alertMessage: "Redirect")
expand_click = true
}else{
index_value.removeAll()
index_value.append("\(indexPath.row)")
expand_click = true
cellData = indexPath.row
collectionView.performBatchUpdates({
}, completion: nil)
}
}
}
}
collection view cells
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var container: UIView!
override func layoutSubviews() {
container.backgroundColor = .white
container.layer.cornerRadius = 13
container.layer.shadowRadius = 2
container.layer.shadowOpacity = 0.7
container.layer.shadowOffset = CGSize(width: 0, height: 1)
container.layer.shadowColor = UIColor.black.withAlphaComponent(0.45).cgColor
}
}
When clicked cell it will be expanding the cell and moved to be under the other the cells
> * Expanded cell again click it will be redirected to another page forEx (I showed alert)
> * the last cell click it's should also redirect These are all I have done my self if we have 15 cards it working fine more than 50 cards
> not smooth scrolling and we have a problem for contending size maybe I
> could not solve the issue
**Collection view FlowLayout**
class StickyCollectionViewFlowLayout2: UICollectionViewFlowLayout {
var firstItemTransform: CGFloat?
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
print("------------------------------------------------------------------------------------------------------------------------")
print("Count-------->",items.count,"<--------Count")
print("------------------------------------------------------------------------------------------------------------------------")
var headerAttributes: UICollectionViewLayoutAttributes?
self.firstItemTransform = nil
if cellData != nil {
let min = cellData!
print(min)
var b = [UICollectionViewLayoutAttributes?]()
var a = false
items.forEach({ (object) in
let object = object as! UICollectionViewLayoutAttributes
if min == object.indexPath.row {
a = true
}else{
if a == true {
b.append(object)
}
}
})
for attributes in b {
if attributes?.representedElementKind == UICollectionView.elementKindSectionHeader {
headerAttributes = attributes
}
else {
self.atributeLayout(attributes!, headerAttributes: headerAttributes)
}
}
cellData = nil
}else{
items.enumerateObjects(using: { (object, idex, stop) -> Void in
let attributes = object as! UICollectionViewLayoutAttributes
if attributes.representedElementKind == UICollectionView.elementKindSectionHeader {
headerAttributes = attributes
}
else {
self.atributeLayoutReset(attributes, headerAttributes: headerAttributes)
}
})
}
return items as? [UICollectionViewLayoutAttributes]
}
func atributeLayout(_ attributes: UICollectionViewLayoutAttributes, headerAttributes: UICollectionViewLayoutAttributes?){
let minY = self.collectionView!.bounds.minY + self.collectionView!.contentInset.top
var maxY = attributes.frame.origin.y+attributes.frame.height/2+80
if let headerAttributes = headerAttributes {
maxY -= headerAttributes.bounds.height
}
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height + 100
if let itemTransform = self.firstItemTransform {
let scale = 1 - deltaY * itemTransform
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
func atributeLayoutReset(_ attributes: UICollectionViewLayoutAttributes, headerAttributes: UICollectionViewLayoutAttributes?){
cellIndex = nil
if isExpandCell != true {
let minY = self.collectionView!.bounds.minY + self.collectionView!.contentInset.top
var maxY = attributes.frame.origin.y
if let headerAttributes = headerAttributes {
maxY -= headerAttributes.bounds.height
}
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height + 100
if let itemTransform = self.firstItemTransform {
let scale = 1 - deltaY * itemTransform
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
// })
}else{
print(collectionView!.contentInset.top,collectionView!.bounds.minY)
let minY = 0.0 + collectionView!.contentInset.top
var maxY = attributes.frame.origin.y
if let headerAttributes = headerAttributes {
maxY -= headerAttributes.bounds.height
}
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height + 100
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
}
if anyone experienced collection view please share me your references
Thank you
Hello please try my code. I have tested on an iPhone simulator and it works great.
import SwiftUI
struct CardView: View {
static let CARD_HEIGHT: CGFloat = 200
static let CARD_WIDTH: CGFloat = 300
static let TAB_HEIGHT: CGFloat = 30
#State var selected: Int? = nil
let cards: Int
var body: some View {
ScrollView {
ZStack(alignment: .top) {
ForEach(0 ..< cards) { card_i in
Button {
withAnimation {
self.selected = self.selected == card_i ? nil : card_i
}
print("A card was tapped. Do something with card_i here")
} label: { self.card }
.buttonStyle(PlainButtonStyle())
.frame(width: Self.CARD_WIDTH, height: Self.CARD_HEIGHT, alignment: .top)
.offset(x: .zero, y: CGFloat(card_i) * Self.TAB_HEIGHT + ((selected ?? cards + 1) < card_i ? Self.CARD_HEIGHT - Self.TAB_HEIGHT / 2 : 0))
}
}
.frame(height: Self.CARD_HEIGHT + Self.TAB_HEIGHT * CGFloat(cards), alignment: .top)
.padding()
}
}
var card: some View {
RoundedRectangle(cornerRadius: 17, style: .continuous)
.fill(Color.gray)
.overlay(RoundedRectangle(cornerRadius: 17, style: .continuous).stroke(Color.black, lineWidth: 5))
}
}
Edit
Use it like this
let swiftUIview = CardView(cards: 10, onCardTapped: { card in
print("card number \(card) tapped")
})
let uiViewController = UIHostingController(rootView: swiftUIview)
Gif removed because outdated.
I want customize page control like a image.
I've already search that, but there are only deal scale.
I want change width, height, spacing.
How can I do that?
I tried this
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
subviews.forEach {
$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
But how to change distance??
#oddK Can you try with this below answer. It's my assumption.
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
let spacing = 5.0
subviews.forEach {
$0.frame = ($0 == currentDot) ? CGRect(x: 0, y: 0, width: 16, height: 4) : CGRect(x: spacing, y: 0, width: 8, height: 4)
//$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
The default UIPageControll is not flexible.
class ExtendedpageControll: UIView{
var numberOfPage: Int
var currentpage : Int = 0{didSet{reloadView()}}
var currentIndicatorColor: UIColor = .black
var indicatorColor: UIColor = UIColor(white: 0.9, alpha: 1)
var circleIndicator: Bool = false
private var dotView = [UIView]()
private let spacing: CGFloat = 6
private lazy var extraWidth: CGFloat = circleIndicator ? 6 : 4
init(numberOfPages: Int,currentPage: Int,isCircular: Bool){
self.numberOfPage = numberOfPages
self.currentpage = currentPage
self.circleIndicator = isCircular
super.init(frame: .zero)
configView()
}
required init?(coder: NSCoder) {fatalError("not implemented")}
private func configView(){
backgroundColor = .clear
(0..<numberOfPage).forEach { _ in
let view = UIView()
addSubview(view)
dotView.append(view)
}
}
private func reloadView(){
dotView.forEach{$0.backgroundColor = indicatorColor}
dotView[currentpage].backgroundColor = currentIndicatorColor
UIView.animate(withDuration: 0.2) {
self.dotView[self.currentpage].frame.origin.x = self.dotView[self.currentpage].frame.origin.x - self.extraWidth
self.dotView[self.currentpage].frame.size.width = self.dotView[self.currentpage].frame.size.width + (self.extraWidth * 2)
}
}
override func layoutSubviews() {
super.layoutSubviews()
for (i,view) in dotView.enumerated(){
view.clipsToBounds = true
view.layer.cornerRadius = bounds.height / 2
let width: CGFloat = circleIndicator ? self.bounds.height : CGFloat(self.bounds.width / CGFloat(self.numberOfPage) - self.spacing) - self.extraWidth
UIView.animate(withDuration: 0.2) {
view.frame = CGRect(x: ((self.bounds.width / CGFloat(self.numberOfPage)) * CGFloat(i)) + self.spacing, y: 0, width: width , height: self.bounds.height)
}
}
reloadView()
}
}
Usage: If you want to link ExtendedpageControll to a View Such as CollectionView Just Do like this: (item is your Datamodel)
class SampleViewController: UIViewController{
let colectionView = UICollectionView()
lazy var pageControll: ExtendedpageControll = {
let pc = ExtendedpageControll(numberOfPages: items.count, currentPage: 0,isCircular: true)
pc.currentIndicatorColor = .black
return pc
}()
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControll.currentpage == indexPath.row {
guard let visible = self.collectionView.visibleCells.first else { return }
guard let index = self.collectionView.indexPath(for: visible)?.row else { return }
pageControll.currentpage = index
}
}
}
inside init, you can set the shape of the indicator to be circular or extended via isCircular.
What is a full example of how to implement the rubber banding effect? How can I implement this?
I have tried the following: However, I have been unsuccessful in finding how to implement this.
Currently I have created a card view which can be pulled up to a certain point, however currently when you reach the max there is a sudden halt, which I would like to change to a rubber band effect.
Here is the code I have been using to try and add this:
enum SheetLevel{
case top, bottom, middle
}
protocol BottomSheetDelegate {
func updateBottomSheet(frame: CGRect)
}
class BottomSheetViewController: UIViewController{
#IBOutlet var panView: UIView!
#IBOutlet weak var tableView: UICollectionView!
// #IBOutlet weak var collectionView: UICollectionView! //header view
var lastY: CGFloat = 0
var pan: UIPanGestureRecognizer!
var bottomSheetDelegate: BottomSheetDelegate?
var parentView: UIView!
var initalFrame: CGRect!
var topY: CGFloat = 80 //change this in viewWillAppear for top position
var middleY: CGFloat = 400 //change this in viewWillAppear to decide if animate to top or bottom
var bottomY: CGFloat = 600 //no need to change this
let bottomOffset: CGFloat = 64 //sheet height on bottom position
var lastLevel: SheetLevel = .middle //choose inital position of the sheet
var disableTableScroll = false
//hack panOffset To prevent jump when goes from top to down
var panOffset: CGFloat = 0
var applyPanOffset = false
//tableview variables
var listItems: [Any] = []
var headerItems: [Any] = []
override func viewDidLoad() {
super.viewDidLoad()
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
pan.delegate = self
self.panView.addGestureRecognizer(pan)
self.tableView.panGestureRecognizer.addTarget(self, action: #selector(handlePan(_:)))
//Bug fix #5. see https://github.com/OfTheWolf/UBottomSheet/issues/5
//Tableview didselect works on second try sometimes so i use here a tap gesture recognizer instead of didselect method and find the table row tapped in the handleTap(_:) method
let tap = UITapGestureRecognizer.init(target: self, action: #selector(handleTap(_:)))
tap.delegate = self
tableView.addGestureRecognizer(tap)
//Bug fix #5 end
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.initalFrame = UIScreen.main.bounds
self.topY = round(initalFrame.height * 0.5)
self.middleY = initalFrame.height * 0.6
self.bottomY = initalFrame.height - bottomOffset
self.lastY = self.middleY
bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == tableView else {return}
if (self.parentView.frame.minY > topY){
self.tableView.contentOffset.y = 0
}
}
//this stops unintended tableview scrolling while animating to top
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView == tableView else {return}
if disableTableScroll{
targetContentOffset.pointee = scrollView.contentOffset
disableTableScroll = false
}
}
//Bug fix #5. see https://github.com/OfTheWolf/UBottomSheet/issues/5
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
let p = recognizer.location(in: self.tableView)
//Commented below to prevenet error.. *** apr 2 guillermo
// let index = tableView.indexPathForRow(at: p)
// //WARNING: calling selectRow doesn't trigger tableView didselect delegate. So handle selected row here.
// //You can remove this line if you dont want to force select the cell
// tableView.selectRow(at: index, animated: false, scrollPosition: .none)
}//Bug fix #5 end
#objc func handlePan(_ recognizer: UIPanGestureRecognizer) {
// var x = topY
// var c = 0.55
// var d = view.frame.height
// var formula = (1.0 - (1.0 / ((x * c / d) + 1.0))) * d
let dy = recognizer.translation(in: self.parentView).y
print(recognizer.translation(in: self.parentView).y, " This si dy")
switch recognizer.state {
case .began:
applyPanOffset = (self.tableView.contentOffset.y > 0)
case .changed:
print(".changed here")
if self.tableView.contentOffset.y > 0{
panOffset = dy
return
}
if self.tableView.contentOffset.y <= 0 {
if !applyPanOffset{panOffset = 0}
let maxY = max(topY, lastY + dy - panOffset)
let y = min(bottomY, maxY)
print(y, ".inside if let thindfahfvdsgjafjsda8", maxY)
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: y)
bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: y))
}
if self.parentView.frame.minY > topY{
print(self.tableView.contentOffset.y, " Thsi is taht y vakue thing")
self.tableView.contentOffset.y = 0
}
case .failed, .ended, .cancelled:
panOffset = 0
print(".failed and enededh rhere")
//bug fix #6. see https://github.com/OfTheWolf/UBottomSheet/issues/6
if (self.tableView.contentOffset.y > 0){
return
}//bug fix #6 end
self.panView.isUserInteractionEnabled = false
self.disableTableScroll = self.lastLevel != .top
self.lastY = self.parentView.frame.minY
self.lastLevel = self.nextLevel(recognizer: recognizer)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.9, options: .curveEaseOut, animations: {
print("Animation!!!!!")
switch self.lastLevel {
case .top:
print("in this thanaagaggagagagagagagalanfg")
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.topY)
// self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.topY))
// self.tableView.contentInset.bottom = 50
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
case .middle:
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.middleY)
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
case .bottom:
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.bottomY)
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.bottomY))
}
}) { (_) in
print("Someghtifgnshdfgbk")
self.panView.isUserInteractionEnabled = true
self.lastY = self.parentView.frame.minY
}
default:
break
}
}
func nextLevel(recognizer: UIPanGestureRecognizer) -> SheetLevel{
let y = self.lastY
let velY = recognizer.velocity(in: self.view).y
if velY < -200{
return y > middleY ? .middle : .top
}else if velY > 200{
return y < (middleY + 1) ? .middle : .bottom
}else{
if y > middleY {
return (y - middleY) < (bottomY - y) ? .middle : .bottom
}else{
return (y - topY) < (middleY - y) ? .top : .middle
}
}
}
}
extension BottomSheetViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SimpleTableCell", for: indexPath) as! SimpleTableCell
let model = SimpleTableCellViewModel(image: nil, title: "Title \(indexPath.row)", subtitle: "Subtitle \(indexPath.row)")
cell.configure(model: model)
return cell
}
}
extension BottomSheetViewController: UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
What you need to do is exponentially decrease the length that user is adding by dragging. There are different formula for this such as using sqrt, log10, and etc.
Here's what I've done:
And here's the code:
import UIKit
class ViewController: UIViewController {
lazy private var box : UIView = {
let view = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height-300, width: UIScreen.main.bounds.width, height: 300))
view.backgroundColor = .red
return view
}()
private var panGesture : UIPanGestureRecognizer!
private var boxOriginY : CGFloat = 300.0
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(box)
panGesture = UIPanGestureRecognizer(target: self, action: #selector(pullUp(with:)))
box.addGestureRecognizer(panGesture)
}
#objc private func pullUp(with pan: UIPanGestureRecognizer) {
let yTranslation = pan.translation(in: self.view).y
if pan.state == .changed {
let distance = CGFloat(sqrt( Double(-yTranslation) ) * 10) // times 10 to make it smoother
let newHeight : CGFloat = boxOriginY + distance
box.frame = CGRect(x: 0,
y: UIScreen.main.bounds.height-(newHeight),
width: UIScreen.main.bounds.width,
height: newHeight)
}
if pan.state == .ended {
if pan.state == UIPanGestureRecognizer.State.ended {
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.box.frame = CGRect(x: 0, y: UIScreen.main.bounds.height-300, width: UIScreen.main.bounds.width, height: 300)
})
}
}
}
}
I build a circular UICollectionview by following This guide, Everything is working as expected but I don't want the items to rotate around their own angle/anchor
The top row is how my circular collectionview is working at the moment, the bottom drawing is how I would like my collectionview :
I am using following layout attribute code:
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var anchorPoint = CGPoint(x: 0.3, y: 0.5)
var angle: CGFloat = 0 {
didSet {
zIndex = Int(angle*1000000)
transform = CGAffineTransformMakeRotation(angle)
}
}
override func copyWithZone(zone: NSZone) -> AnyObject {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
with the following layout class:
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 60, height: 110)
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItemsInSection(0) > 0 ? -CGFloat(collectionView!.numberOfItemsInSection(0)-1)*anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme*collectionView!.contentOffset.x/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
}
var radius: CGFloat = 400 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return 0.18
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func collectionViewContentSize() -> CGSize {
return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0))*itemSize.width,
height: CGRectGetHeight(collectionView!.bounds))
}
override class func layoutAttributesClass() -> AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
override func prepareLayout() {
super.prepareLayout()
let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds)/2.0)
let anchorPointY = ((itemSize.height/2.0) + radius)/itemSize.height
let theta = atan2(CGRectGetWidth(collectionView!.bounds)/2.0, radius + (itemSize.height/2.0) - (CGRectGetHeight(collectionView!.bounds)/2.0)) //1
//let theta:CGFloat = 1.0
var startIndex = 0
var endIndex = collectionView!.numberOfItemsInSection(0) - 1
if (angle < -theta) {
startIndex = Int(floor((-theta - angle)/anglePerItem))
}
endIndex = min(endIndex, Int(ceil((theta - angle)/anglePerItem)))
if (endIndex < startIndex) {
endIndex = 0
startIndex = 0
}
attributesList = (startIndex...endIndex).map { (i) -> CircularCollectionViewLayoutAttributes in
let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: i, inSection: 0))
attributes.size = self.itemSize
attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
attributes.angle = self.angle + (self.anglePerItem*CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)
-> (UICollectionViewLayoutAttributes!) {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var finalContentOffset = proposedContentOffset
let factor = -angleAtExtreme/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
let proposedAngle = proposedContentOffset.x*factor
let ratio = proposedAngle/anglePerItem
var multiplier: CGFloat
if (velocity.x > 0) {
multiplier = ceil(ratio)
} else if (velocity.x < 0) {
multiplier = floor(ratio)
} else {
multiplier = round(ratio)
}
finalContentOffset.x = multiplier*anglePerItem/factor
return finalContentOffset
}
}
I tried many things but I was not able to change the cell rotation
I've solved this problem by rotateing the view of cell by negative angle:
Code below:
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
super.applyLayoutAttributes(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
viewRoot.transform = CGAffineTransformMakeRotation(-circularlayoutAttributes.angle)
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
}
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:
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?
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)
}
}
Swift 4.2
import UIKit
class ViewController: 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
let width = UIScreen.main.bounds.size.width - 80
let yPos = (UIScreen.main.bounds.size.height / 2) - (width / 2)
let iFrame = CGRect(x: 40, y: yPos, width: width, height: width)
self.imageViews.values.forEach { $0.frame = iFrame }
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)
{
let width = UIScreen.main.bounds.size.width - 80
let yPos = (UIScreen.main.bounds.size.height / 2) - (width / 2)
let iFrame = CGRect(x: 40, y: yPos, width: width, height: width)
guard 0 ..< self.images.count ~= index else { return }
let image = self.images[index]
let oldImageView = self.imageViews[index]
let imageView = UIImageView(frame: iFrame)
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 = CATransitionType.fade
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()
{
let width = UIScreen.main.bounds.size.width - 80
let yPos = (UIScreen.main.bounds.size.height / 2) - (width / 2)
let iFrame = CGRect(x: 40, y: yPos, width: width, height: width)
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, -iFrame.width * 0.5)
transform = CATransform3DRotate(transform, -angle, 0, 1, 0)
transform = CATransform3DTranslate(transform, 0, 0, iFrame.width * 0.5)
}
imageView.bounds = iFrame
imageView.center = CGPoint(x: iFrame.midX + self.scrollView.contentOffset.x, y: iFrame.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 width = UIScreen.main.bounds.size.width - 80
let yPos = (UIScreen.main.bounds.size.height / 2) - (width / 2)
let iFrame = CGRect(x: 40, y: yPos, width: width, height: width)
let newIndex = (index + self.images.count) % self.images.count
let imageView = UIImageView(frame: iFrame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = .white
imageView.image = self.images[newIndex]
self.imageViews[index] = imageView
}
}
}
}
// MARK: - UIScrollViewDelegate
extension ViewController: 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)
}
}
use Spring animation to create various animation including cubic. and with very less code.