Specific Image Spawn - ios

How can I edit this code so the spawnRandomPosition function also doesn't allow the image to spawn 45 pixels down from the bottom of the screen? So the top 45 pixels of the screen cannot be spawned in.
class SecondViewController: UIViewController {
private var addOne = 0
func spawnRandomPosition() -> CGPoint
{
let width = UIScreen.main.fixedCoordinateSpace.bounds.width
let height = UIScreen.main.fixedCoordinateSpace.bounds.height
let centerArea = CGRect(x: UIScreen.main.fixedCoordinateSpace.bounds.midX - 75.0 / 2,
y: UIScreen.main.fixedCoordinateSpace.bounds.midY - 75.0 / 2,
width: 75.0,
height: 75.0)
while true
{
let randomPosition = CGPoint(x: Int(arc4random_uniform(UInt32(width))), y: Int(arc4random_uniform(UInt32(height))))
if !centerArea.contains(randomPosition)
{
return randomPosition
}
}
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPoint.zero, in: self.view)
if (WhiteDot.frame.contains(smallDot.frame) && smallDot.image != nil) {
addOne += 1
score.text = "\(addOne)"
smallDot?.center = spawnRandomPosition()
}
}
}

Just modify function spawnRandomPosition a bit:
func spawnRandomPosition() -> CGPoint
{
let minY: CGFloat = 45.0
let width = UIScreen.main.fixedCoordinateSpace.bounds.width
let height = UIScreen.main.fixedCoordinateSpace.bounds.height - minY
let centerArea = CGRect(x: UIScreen.main.fixedCoordinateSpace.bounds.midX - 75.0 / 2,
y: UIScreen.main.fixedCoordinateSpace.bounds.midY - 75.0 / 2,
width: 75.0,
height: 75.0)
while true
{
let randomPosition = CGPoint(x:CGFloat(arc4random()).truncatingRemainder(dividingBy: height),
y:minY + CGFloat(arc4random()).truncatingRemainder(dividingBy: width))
// Check for 'forbidden' area
if !centerArea.contains(randomPosition)
{
return randomPosition
}
}
}

Maybe you need a different approach, is not elegant but you could use if's
func spawnRandomPosition() -> CGPoint {
let screenWidth = UIScreen.main.fixedCoordinateSpace.bounds.width
let screenHeight = UIScreen.main.fixedCoordinateSpace.bounds.height
boolean valid = true;
repeat
{
let randomPosition = CGPoint(x:CGFloat(arc4random()),y:arc4random())
if(randomPosition.x >screenWidth || randomPosition.y < 45 || (randomPosition.x > ((screenWidth/2)- 37.5) && randomPosition.x< ((screenWidth/2)+ 37.5) || (randomPosition.y > ((screenHeight/2)- 37.5) && randomPosition.y< ((screenHeight/2)+ 37.5))
valid = false
}while(valid = false;)
}
But of course you should take in consideration that could arc4random() give back a very high number and end up with an inefficient solution, so you could use rand() instead

Related

Make UIPinchGestureRecognizer work outside imageview like stickers in Instagram story

I am working on an image editing application. When sticker is added to the view, pinchGesture can be used to scale it. Problem is when the imageView(sticker) is very small, the gesture is very difficult to use. I need to be able to pinch outside of the sticker and still scale the imageView.
I then added pinchGesture to self.view and detected it's touches; if the straight line from the finger intersects the imageView, pinchGesture is applied. This works fine until multiple stickers(imageViews) are added and the gesture works simultaneously on multiple stickers.
So, I calculated the distance of intersection from the fingers to the rect of each view and the view with the longest distance would get the pinchGesture.
This doesn't work all the time.
Here is the code:
#objc func pinched(sender: UIPinchGestureRecognizer){
slider.isUserInteractionEnabled = false
if sender.numberOfTouches == 2 && sender.state != .ended{
selectedView = []
let finger1 = sender.location(ofTouch: 0, in: self.view)
let finger2 = sender.location(ofTouch: 1, in: self.view)
for subview in self.view.subviews{
if subview is UIImageView{
let rect = subview.frame
let width = rect.size.width
let height = rect.size.height
let Ax = rect.origin.x
let Ay = rect.origin.y
let Bx = rect.origin.x + width
let By = rect.origin.y
let Dx = rect.origin.x
let Dy = rect.origin.y + height
let Cx = rect.origin.x + height
let Cy = rect.origin.y + width
let A = CGPoint(x: Ax, y: Ay)
let B = CGPoint(x: Bx, y: By)
let C = CGPoint(x: Cx, y: Cy)
let D = CGPoint(x: Dx, y: Dy)
let lineToAB = getIntersectionOfLines(line1: (finger1, finger2), line2: (A, B))
let lineToBC = getIntersectionOfLines(line1: (finger1, finger2), line2: (B, C))
let lineToCD = getIntersectionOfLines(line1: (finger1, finger2), line2: (C, D))
let lineToAD = getIntersectionOfLines(line1: (finger1, finger2), line2: (A, D))
var dx: CGFloat!
if lineToAB != CGPoint.zero || lineToBC != CGPoint.zero || lineToCD != CGPoint.zero || lineToAD != CGPoint.zero{
print("touched")
touched = true
if lineToAB != CGPoint.zero{
dx = distance(lineToAB, lineToCD)
}else if lineToBC != CGPoint.zero{
dx = distance(lineToBC, lineToAD)
}else if lineToCD != CGPoint.zero{
dx = distance(lineToAB, lineToCD)
}else if lineToAD != CGPoint.zero{
dx = distance(lineToAD, lineToBC)
}else{
dx = 0.0
}
}else{
dx = 0.0
touched = false
}
selectedView.append(selectedViews(distance: dx, view: subview))
}
}
var max: CGFloat = 0.0
var viewtoscale: UIView!
for item in selectedView{
if item.distance > max{
max = item.distance
viewtoscale = item.view
}
}
if viewtoscale != nil{
if viewtoscale != self.slider || viewtoscale != tempImageView || viewtoscale != drawView{
if viewtoscale is UIImageView{
viewtoscale.transform = viewtoscale.transform.scaledBy(x: sender.scale, y: sender.scale)
}
}
}
sender.scale = 1
}
}
Also the functions for getting intersection and distance:
func getIntersectionOfLines(line1: (a: CGPoint, b: CGPoint), line2: (a: CGPoint, b: CGPoint)) -> CGPoint {
let distance = (line1.b.x - line1.a.x) * (line2.b.y - line2.a.y) - (line1.b.y - line1.a.y) * (line2.b.x - line2.a.x)
if distance == 0 {
print("error, parallel lines")
return CGPoint.zero
}
let u = ((line2.a.x - line1.a.x) * (line2.b.y - line2.a.y) - (line2.a.y - line1.a.y) * (line2.b.x - line2.a.x)) / distance
let v = ((line2.a.x - line1.a.x) * (line1.b.y - line1.a.y) - (line2.a.y - line1.a.y) * (line1.b.x - line1.a.x)) / distance
if (u < 0.0 || u > 1.0) {
print("error, intersection not inside line1")
return CGPoint.zero
}
if (v < 0.0 || v > 1.0) {
print("error, intersection not inside line2")
return CGPoint.zero
}
return CGPoint(x: line1.a.x + u * (line1.b.x - line1.a.x), y: line1.a.y + u * (line1.b.y - line1.a.y))
}
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(sqrt((xDist * xDist) + (yDist * yDist)))
}
You can set tag for each imageView and declare selectedSticker as Int, when user tapped in sticker set selectedSticker to its tag, then just scale the selected sticker.

How to make Circular audio visualizer in swift?

I want to make a visualizer like this Circular visualizer, click the green flag to see the animation.
In my project first I draw a circle, I calculate the points on the circle to draw the visualizer bars, I rotate the view to make the bars feels like circle. I use StreamingKit to stream live radio. StreamingKit provides the live audio power in decibels. Then I animate the visualizer bars. But when I rotate the view the height and width changes according to the angle I rotate. But the bounds value not change (I know the frame depends on superViews).
audioSpectrom Class
class audioSpectrom: UIView {
let animateDuration = 0.15
let visualizerColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
var barsNumber = 0
let barWidth = 4 // width of bar
let radius: CGFloat = 40
var radians = [CGFloat]()
var barPoints = [CGPoint]()
private var rectArray = [CustomView]()
private var waveFormArray = [Int]()
private var initialBarHeight: CGFloat = 0.0
private let mainLayer: CALayer = CALayer()
// draw circle
var midViewX: CGFloat!
var midViewY: CGFloat!
var circlePath = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
self.layer.addSublayer(mainLayer)
barsNumber = 10
}
override func layoutSubviews() {
mainLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
drawVisualizer()
}
//-----------------------------------------------------------------
// MARK: - Drawing Section
//-----------------------------------------------------------------
func drawVisualizer() {
midViewX = self.mainLayer.frame.midX
midViewY = self.mainLayer.frame.midY
// Draw Circle
let arcCenter = CGPoint(x: midViewX, y: midViewY)
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
let circleShapeLayer = CAShapeLayer()
circleShapeLayer.path = circlePath.cgPath
circleShapeLayer.fillColor = UIColor.blue.cgColor
circleShapeLayer.strokeColor = UIColor.clear.cgColor
circleShapeLayer.lineWidth = 1.0
mainLayer.addSublayer(circleShapeLayer)
// Draw Bars
rectArray = [CustomView]()
for i in 0..<barsNumber {
let angle = ((360 / barsNumber) * i) - 90
let point = calculatePoints(angle: angle, radius: radius)
let radian = angle.degreesToRadians
radians.append(radian)
barPoints.append(point)
let rectangle = CustomView(frame: CGRect(x: barPoints[i].x, y: barPoints[i].y, width: CGFloat(barWidth), height: CGFloat(barWidth)))
initialBarHeight = CGFloat(self.barWidth)
rectangle.setAnchorPoint(anchorPoint: CGPoint.zero)
let rotationAngle = (CGFloat(( 360/barsNumber) * i)).degreesToRadians + 180.degreesToRadians
rectangle.transform = CGAffineTransform(rotationAngle: rotationAngle)
rectangle.backgroundColor = visualizerColor
rectangle.layer.cornerRadius = CGFloat(rectangle.bounds.width / 2)
rectangle.tag = i
self.addSubview(rectangle)
rectArray.append(rectangle)
var values = [5, 10, 15, 10, 5, 1]
waveFormArray = [Int]()
var j: Int = 0
for _ in 0..<barsNumber {
waveFormArray.append(values[j])
j += 1
if j == values.count {
j = 0
}
}
}
}
//-----------------------------------------------------------------
// MARK: - Animation Section
//-----------------------------------------------------------------
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
DispatchQueue.main.async {
UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: {
for i in 0..<self.barsNumber {
let channelValue: Int = Int(arc4random_uniform(2))
let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i])))
let barView = self.rectArray[i] as? CustomView
guard var barFrame = barView?.frame else { return }
// calculate the bar height
let barH = (self.frame.height / 2 ) - self.radius
// scale the value to 40, input value of this func range from 0-60, 60 is low and 0 is high. Then calculate the height by minimise the scaled height from bar height.
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
if channelValue == 0 {
barFrame.size.height = calc0
} else {
barFrame.size.height = calc1
}
if barFrame.size.height < 4 || barFrame.size.height > ((self.frame.size.height / 2) - self.radius) {
barFrame.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barView?.frame = barFrame
}
}, completion: nil)
}
}
func calculatePoints(angle: Int, radius: CGFloat) -> CGPoint {
let barX = midViewX + cos((angle).degreesToRadians) * radius
let barY = midViewY + sin((angle).degreesToRadians) * radius
return CGPoint(x: barX, y: barY)
}
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
extension UIView{
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position : CGPoint = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
self.layer.position = position;
self.layer.anchorPoint = anchorPoint;
}
}
I drag a empty view to storyBoard and give custom class as audioSpectrom.
ViewController
func startAudioVisualizer() {
visualizerTimer?.invalidate()
visualizerTimer = nil
visualizerTimer = Timer.scheduledTimer(timeInterval: visualizerAnimationDuration, target: self, selector: #selector(self.visualizerTimerFunc), userInfo: nil, repeats: true)
}
#objc func visualizerTimerFunc(_ timer: CADisplayLink) {
let lowResults = self.audioPlayer!.averagePowerInDecibels(forChannel: 0)
let lowResults1 = self.audioPlayer!.averagePowerInDecibels(forChannel: 1)
audioSpectrom.animateAudioVisualizerWithChannel(level0: -lowResults, level1: -lowResults1)
}
OUTPUT
Without animation
With animation
In my observation, the height value and width value of frame changed when rotates. Means when I give CGSize(width: 4, height: 4) to bar, then when I rotate using some angle it changes the size of frame like CGSize(width: 3.563456, height: 5.67849) (not sure for the value, it's an assumption).
How to resolve this problem?
Any suggestions or answers will be appreciated.
Edit
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
DispatchQueue.main.async {
UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: {
for i in 0..<self.barsNumber {
let channelValue: Int = Int(arc4random_uniform(2))
let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i])))
var barView = self.rectArray[i] as? CustomView
guard let barViewUn = barView else { return }
let barH = (self.frame.height / 2 ) - self.radius
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
let kSavedTransform = barViewUn.transform
barViewUn.transform = .identity
if channelValue == 0 {
barViewUn.frame.size.height = calc0
} else {
barViewUn.frame.size.height = calc1
}
if barViewUn.frame.height < CGFloat(4) || barViewUn.frame.height > ((self.frame.size.height / 2) - self.radius) {
barViewUn.frame.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barViewUn.transform = kSavedTransform
barView = barViewUn
}
}, completion: nil)
}
}
Output
Run the below code snippet show the output
<img src="https://i.imgflip.com/227xsa.gif" title="made at imgflip.com"/>
GOT IT!!
circular-visualizer
There are two (maybe three) issues in your code:
1. audioSpectrom.layoutSubviews()
You create new views in layoutSubviews and add them to the view hierarchy. This is not what you are intened to do, because layoutSubviews is called multiple times and you should use it only for layouting purposes.
As a dirty work-around, I modified the code in the func drawVisualizer to only add the bars once:
func drawVisualizer() {
// ... some code here
// ...
mainLayer.addSublayer(circleShapeLayer)
// This will ensure to only add the bars once:
guard rectArray.count == 0 else { return } // If we already have bars, just return
// Draw Bars
rectArray = [CustomView]()
// ... Rest of the func
}
Now, it almost looks good, but there are still some dirt effects with the topmost bar. So you'll have to change
2. audioSectrom.animateAudioVisualizerWithChannel(level0:level1:)
Here, you want to recalculate the frame of the bars. Since they are rotated, the frame also is rotated, and you'd have to apply some mathematical tricks. To avoid this adn make your life more easy, you save the rotated transform, set it to .identity, modify the frame, and then restore the original rotated transform. Unfortunately, this causes some dirt effects with rotations of 0 or 2pi, maybe caused by some rounding issues. Never mind, there is a much more simple solution:
Instead of modifiying the frame, you better modify the bounds.
frame is measured in the outer (in your case: rotated) coordinate system
bounds is measured in the inner (non-transformed) coordinate system
So I simply replaced all the frames with bounds in the function animateAudioVisualizerWithChannel and also removed the saving and restoring of the transformation matrix:
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
// some code before
guard let barViewUn = barView else { return }
let barH = (self.bounds.height / 2 ) - self.radius
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
if channelValue == 0 {
barViewUn.bounds.size.height = calc0
} else {
barViewUn.bounds.size.height = calc1
}
if barViewUn.bounds.height < CGFloat(4) || barViewUn.bounds.height > ((self.bounds.height / 2) - self.radius) {
barViewUn.bounds.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barView = barViewUn
// some code after
}
3. Warnings
By the way, you should get rid of all the warnings in your code. I didn't clean up my answer code to keep it comparable with the orginal code.
For example, in var barView = self.rectArray[i] as? CustomView you don't need the conditional cast, because the array already contains CustomView objects.
So, all the barViewUn stuff is unnecessary.
Much more to find and to clean up.

Rotating ImageView using UIPanGestureRecognizer- Swift 3

I am trying to rotate an ImageView I have depending on the X coordinate it is on. Basically, I want it to have a rotation of 0º when x = 300 and a rotation of 180º when x = 190.
I had to program the UIPanGestureRecognizer programmatically. Here is the code I currently have right now:
#objc func personDrag(recognizer: UIPanGestureRecognizer) {
let rotationSub: CGFloat = 1
let translation = recognizer.translation(in: rView)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x, y:view.center.y + translation.y)
view.transform = view.transform.rotated(by: CGFloat.pi - rotationSub)
}
recognizer.setTranslation(CGPoint.zero, in: rView)
}
I was going to attempt to change the rotation degree by 1 every time they panned but it doesn't really work/make sense. Any help would be appreciated. Thank you so much!
Cheers, Theo
You can build your implementation on this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageview: UIImageView!
private var currentRotation: Rotation = .none
/* Certain rotation points (rotation of 0º when x = 300 and a rotation of 180º when x = 190) */
enum Rotation {
case none, xPoint190, xPoint300
}
override func viewDidLoad() {
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
imageview.addGestureRecognizer(gestureRecognizer)
imageview.isUserInteractionEnabled = true
}
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard gestureRecognizer.state == .began || gestureRecognizer.state == .changed else {
return
}
guard let imgView = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: self.view)
imgView.center = CGPoint(x: imgView.center.x + translation.x, y: imgView.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
let angle: CGFloat = self.degreesToRadians(180.0)
/* After reaching x point case - rotating and setting rotation occured to prohibit further rotation */
if imgView.layer.frame.origin.x <= 190, currentRotation != .xPoint190 {
imgView.transform = imgView.transform.rotated(by: angle)
currentRotation = .xPoint190
} else if imgView.layer.frame.origin.x >= 300, currentRotation != .xPoint300 {
imgView.transform = imgView.transform.rotated(by: angle)
currentRotation = .xPoint300
}
private func degreesToRadians(_ deg: CGFloat) -> CGFloat {
return deg * CGFloat.pi / 180
}
}
I hope this will help you.
#objc func rotateViewPanGesture(_ recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.location(in: superview)
let center = CGRectGetCenter(frame)
switch recognizer.state {
case .began:
deltaAngle = atan2(touchLocation!.y - center.y, touchLocation!.x - center.x) - CGAffineTrasformGetAngle(transform)
initialBounds = bounds
initialDistance = CGpointGetDistance(center, point2: touchLocation!)
case .changed:
let ang = atan2(touchLocation!.y - center.y, touchLocation!.x - center.x)
let angleDiff = deltaAngle! - ang
let a = transform.a
let b = transform.b
let c = transform.c
let d = transform.d
let sx = sqrt(a * a + b * b)
let sy = sqrt(c * c + d * d)
let currentScale = CGPoint(x: sx, y: sy)
let scale = CGAffineTransform(scaleX: currentScale.x, y: currentScale.y)
self.transform = scale.rotated(by: -angleDiff)
layoutIfNeeded()
case .ended:
print("end gesture status")
default:break
}
}
Using Swift5
Programmatically
Rotate view by single point touch
import UIKit
class ViewController: UIViewController {
//Variable for rotating
private var deltaAngle:CGFloat = 0
let squareView : UIView = {
let anyView = UIView()
anyView.backgroundColor = .red
anyView.isUserInteractionEnabled = true
anyView.isMultipleTouchEnabled = true
return anyView
}()
let rotateButton : UIButton = {
let button = UIButton()
button.backgroundColor = .black
button.setImage(UIImage(systemName: "rotate.right"), for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
squareView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
rotateButton.frame = CGRect(x: 0, y: squareView.frame.height-30, width: 30, height: 30)
squareView.center = view.center
view.addSubview(squareView)
squareView.addSubview(rotateButton)
let PanToRotate = UIPanGestureRecognizer(target: self, action: #selector(handleRotateGesture(_:)))
rotateButton.addGestureRecognizer(PanToRotate)
}
#objc func handleRotateGesture(_ recognizer : UIPanGestureRecognizer){
let touchLocation = recognizer.location(in: squareView.superview)
let center = squareView.center
switch recognizer.state{
case .began :
self.deltaAngle = atan2(touchLocation.y - center.y, touchLocation.x - center.x) - atan2(squareView.transform.b, squareView.transform.a)
case .changed:
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
let angleDiff = self.deltaAngle - angle
squareView.transform = CGAffineTransform(rotationAngle: -angleDiff)
default: break
}
}
}

How to forward the pan gesture from UIScrollView to UIImageView?

I have a UIScrollView and inside a UIImageView so that I can pinche zoom the image view using:
extension CropperViewController : UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageView;
}
}
I now also want to be able to freely move the UIImageView so I tried adding a UIPanGestureRecognizer to myUIImageView`:
self.imageView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))));
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view);
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y);
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view);
}
}
I now had the problem that no pan touch event was fired at all so I thought maybe the UIScrollView is catching all those events. So some research on Stackoverflow told me to add the following to my UIScrollView:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)));
tapGestureRecognizer.numberOfTapsRequired = 1;
tapGestureRecognizer.cancelsTouchesInView = false;
self.scrollView.addGestureRecognizer(tapGestureRecognizer);
but actually that changed nothing. I can zoom and move the image after zooming but I can not move the image using UIPanGestureRecognizer. I want to use the UIScrollView to be able to zoom but I want to use the UIPanGestureRecognizer to move the UIImageView.
How can I do that?
EDIT
Maybe it is possible to disable or change the pan gesture recognizer of the UIScrollView and forward those events to the UIImageView?
You need to give the pan recognizer a delegate, then return true for shouldRecogniseSimultaneouslyWith....
You may also need to do the same with the scroll view's pan recognizer, which is available as a property.
Alternatively, add another target/action to the scroll view's pan recognizer (using addTarget(_, action:) instead of creating your own.
Try this:
scrollView.panGestureRecognizer.require(toFail: imagePanRecognizer)
If you still get problems (like scrolling feeling laggy) set scrollView's delaysContentTouches to false
Since it seems that there isnt a solution for that I came up with not using a UIScrollView and impelementing UIPinchGestureRecognizer and UIPanGestureRecognizer for my UIImageView by myself:
import Foundation
import UIKit
/**
*
*/
protocol CropperCallback {
/**
*
*/
func croppingDone(image: UIImage);
/**
*
*/
func croppingCancelled();
}
/**
*
*/
class CropperViewController : UIViewController {
/**
*
*/
#IBOutlet var imageView: UIImageView!;
/**
*
*/
var imageViewScaleCurrent: CGFloat! = 1.0;
var imageViewScaleMin: CGFloat! = 0.5;
var imageViewScaleMax: CGFloat! = 5.0;
/**
*
*/
#IBOutlet var cropAreaView: CropAreaView!;
/**
*
*/
#IBOutlet weak var cropAreaViewConstraintWidth: NSLayoutConstraint!
#IBOutlet weak var cropAreaViewConstraintHeight: NSLayoutConstraint!
/**
*
*/
#IBOutlet var btnCrop: UIButton!;
/**
*
*/
#IBOutlet var btnCancel: UIButton!;
/**
*
*/
var callback: CropperCallback! = nil;
/**
*
*/
var image: UIImage! = nil;
/**
*
*/
var imageOriginalWidth: CGFloat!;
var imageOriginalHeight: CGFloat!;
/**
*
*/
var cropWidth: CGFloat! = 287;
/**
*
*/
var cropHeight: CGFloat! = 292;
/**
*
*/
var cropHeightFix: CGFloat! = 1.0;
/**
*
*/
var cropArea: CGRect {
/**
*
*/
get {
let factor = self.imageView.image!.size.width / self.view.frame.width;
let scale = 1 / self.imageViewScaleCurrent;
let x = (self.cropAreaView.frame.origin.x - self.imageView.frame.origin.x) * scale * factor;
let y = (self.cropAreaView.frame.origin.y - self.imageView.frame.origin.y) * scale * factor;
let width = self.cropAreaView.frame.size.width * scale * factor;
let height = self.cropAreaView.frame.size.height * scale * factor;
return CGRect(x: x, y: y, width: width, height: height);
}
}
/**
*
*/
static func storyboardInstance() -> CropperViewController? {
let storyboard = UIStoryboard(name: String(describing: NSStringFromClass(CropperViewController.classForCoder()).components(separatedBy: ".").last!), bundle: nil);
return storyboard.instantiateInitialViewController() as? CropperViewController;
}
/**
*
*/
override func viewDidLoad() {
super.viewDidLoad();
self.imageView.image = self.image;
self.imageView.isUserInteractionEnabled = true;
self.imageView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))));
self.imageView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))));
self.cropAreaViewConstraintWidth.constant = self.cropWidth;
self.cropAreaViewConstraintHeight.constant = self.cropHeight;
self.cropAreaView.layer.borderWidth = 1;
self.cropAreaView.layer.borderColor = UIColor(red: 173/255, green: 192/255, blue: 4/255, alpha: 1.0).cgColor;
self.btnCrop.addTarget(self, action: #selector(self.didTapCropButton), for: UIControlEvents.touchUpInside);
self.btnCancel.addTarget(self, action: #selector(self.didTapCancelButton), for: UIControlEvents.touchUpInside);
}
/**
*
*/
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
let imageOriginalRect = self.getRectOfImageInImageView(imageView: self.imageView);
self.imageOriginalWidth = imageOriginalRect.size.width;
self.imageOriginalHeight = imageOriginalRect.size.height;
}
/**
*
*/
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let rect = self.getRectOfImageInImageView(imageView: self.imageView);
let xImage = rect.origin.x;
let yImage = rect.origin.y;
let widthImage = rect.size.width;
let heightImage = rect.size.height;
let xCropView = self.cropAreaView.frame.origin.x;
let yCropView = self.cropAreaView.frame.origin.y;
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
let translation = gestureRecognizer.translation(in: self.view);
var x: CGFloat;
var y: CGFloat;
if (translation.x > 0) {
if (!(xImage >= xCropView)) {
x = gestureRecognizer.view!.center.x + translation.x;
} else {
x = gestureRecognizer.view!.center.x;
}
} else if (translation.x < 0) {
if (!((xImage + widthImage) <= (xCropView + widthCropView))) {
x = gestureRecognizer.view!.center.x + translation.x;
} else {
x = gestureRecognizer.view!.center.x;
}
} else {
x = gestureRecognizer.view!.center.x;
}
if (translation.y > 0) {
if (!(yImage >= (yCropView - self.cropHeightFix))) {
y = gestureRecognizer.view!.center.y + translation.y;
} else {
y = gestureRecognizer.view!.center.y;
}
} else if (translation.y < 0) {
if (!((yImage + heightImage) <= (yCropView + heightCropView + self.cropHeightFix))) {
y = gestureRecognizer.view!.center.y + translation.y;
} else {
y = gestureRecognizer.view!.center.y;
}
} else {
y = gestureRecognizer.view!.center.y;
}
gestureRecognizer.view!.center = CGPoint(x: x, y: y);
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view);
self.fixImageViewPosition();
}
}
/**
*
*/
func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
if let view = gestureRecognizer.view {
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
if (((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalWidth) > widthCropView)
&& ((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalHeight) > (heightCropView + (2 * self.cropHeightFix)))
&& ((self.imageViewScaleCurrent * gestureRecognizer.scale) < self.imageViewScaleMax)) {
self.imageViewScaleCurrent = self.imageViewScaleCurrent * gestureRecognizer.scale;
view.transform = CGAffineTransform(scaleX: self.imageViewScaleCurrent, y: self.imageViewScaleCurrent);
}
gestureRecognizer.scale = 1.0;
self.fixImageViewPosition();
}
}
/**
*
*/
func fixImageViewPosition() {
let rect = self.getRectOfImageInImageView(imageView: self.imageView);
let xImage = rect.origin.x;
let yImage = rect.origin.y;
let widthImage = rect.size.width;
let heightImage = rect.size.height;
let xCropView = self.cropAreaView.frame.origin.x;
let yCropView = self.cropAreaView.frame.origin.y;
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
if (xImage > xCropView) {
self.imageView.frame = CGRect(x: xCropView, y: self.imageView.frame.origin.y, width: widthImage, height: heightImage);
}
if ((xImage + widthImage) < (xCropView + widthCropView)) {
self.imageView.frame = CGRect(x: ((xCropView + widthCropView) - widthImage), y: self.imageView.frame.origin.y, width: widthImage, height: heightImage);
}
if (yImage > yCropView) {
self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: (yCropView - self.cropHeightFix), width: widthImage, height: heightImage);
}
if ((yImage + heightImage) < (yCropView + heightCropView + self.cropHeightFix)) {
self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: ((yCropView + heightCropView + self.cropHeightFix) - heightImage), width: widthImage, height: heightImage);
}
}
/**
*
*/
func getRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size;
let imageSize = imageView.image!.size;
let scaleW = imageViewSize.width / imageSize.width;
let scaleH = imageViewSize.height / imageSize.height;
let aspect = min(scaleW, scaleH);
var imageRect = CGRect(x: 0, y: 0, width: (imageSize.width * aspect), height: (imageSize.height * aspect));
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2;
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2;
imageRect.origin.x += imageView.frame.origin.x;
imageRect.origin.y += imageView.frame.origin.y;
return imageRect;
}
/**
*
*/
func didTapCropButton(sender: AnyObject) {
let croppedCGImage = self.imageView.image?.cgImage?.cropping(to: self.cropArea);
let croppedImage = UIImage(cgImage: croppedCGImage!);
if (self.callback != nil) {
self.callback.croppingDone(image: croppedImage);
}
self.dismiss(animated: true, completion: nil);
}
/**
*
*/
func didTapCancelButton(sender: AnyObject) {
if (self.callback != nil) {
self.callback.croppingCancelled();
}
self.dismiss(animated: true, completion: nil);
}
}
/**
*
*/
extension UIImageView {
/**
*
*/
func imageFrame() -> CGRect {
let imageViewSize = self.frame.size;
guard let imageSize = self.image?.size else {
return CGRect.zero;
}
let imageRatio = imageSize.width / imageSize.height;
let imageViewRatio = imageViewSize.width / imageViewSize.height;
if (imageRatio < imageViewRatio) {
let scaleFactor = imageViewSize.height / imageSize.height;
let width = imageSize.width * scaleFactor;
let topLeftX = (imageViewSize.width - width) * 0.5;
return CGRect(x: topLeftX, y: 0, width: width, height: imageViewSize.height);
} else {
let scaleFactor = imageViewSize.width / imageSize.width;
let height = imageSize.height * scaleFactor;
let topLeftY = (imageViewSize.height - height) * 0.5;
return CGRect(x: 0, y: topLeftY, width: imageViewSize.width, height: height);
}
}
}

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:
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.

Resources