How to forward the pan gesture from UIScrollView to UIImageView? - ios

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);
}
}
}

Related

How to make waveform for my recorded audio?

I am using AVFoundation to record audio with the setting below.
After recording successfully, I need to show the waveform of the recorded file to the user. Can anyone help me with this task?
Here is my setting for recorder:
let recordSettings =
[AVNumberOfChannelsKey: 1,
AVFormatIDKey : kAudioFormatOpus,
AVSampleRateKey: 24000.0] as [String : Any]
import UIKit
import RYKit
let normalColor = UIColor.white
let normalAlphaColor = UIColor.init(white: 1.0, alpha: 0.5)
let highlightColor = UIColor.init(red: 163.0/255.0, green: 243.0/255.0, blue: 16.0/255.0, alpha: 1.0)
let highlightAlphaColor = UIColor.init(red: 163.0/255.0, green: 243.0/255.0, blue: 16.0/255.0, alpha: 0.24)
let waveWidth = CGFloat(2.5)
let waveSpace = CGFloat(0.5)
let waveRadius = CGFloat(1.25)
let upMaxHeight = CGFloat(60)
let downMaxHeight = CGFloat(30)
let upDownSpace = CGFloat(2)
protocol WaveformScrollDelegate: NSObjectProtocol {
func didScrollToTime(time: NSInteger)
func didScrollByPercentage(percent: Double, animated: Bool)
}
class WaveformComponent: UIView, CAAnimationDelegate, UIGestureRecognizerDelegate {
private var timeLine: UILabel!
private var topView: WaveformView!
private var topViewMask: CALayer!
private var bottomView: WaveformView!
private var isAnimated = false
private let convertTime = {
(seconds: Int) -> String in
let minute = seconds / 60
let minuteStr = minute > 9 ? "\(minute)" : "0\(minute)"
let second = seconds % 60
let secondStr = second > 9 ? "\(second)" : "0\(second)"
return "\(minuteStr):\(secondStr)"
}
var animationTimer: Timer!
weak var delegate: WaveformScrollDelegate?
var isVisible = true
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
required init?(coder: NSCoder) {
super.init(coder: coder)
}
init(frame: CGRect, amplitudes: [Double]) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
self.isOpaque = true
self.clipsToBounds = true
let width = (waveWidth + waveSpace) * CGFloat(amplitudes.count / 2)
let height = upMaxHeight + downMaxHeight + upDownSpace
let waveRect = CGRect.init(x: frame.size.width/2.0, y: (frame.size.height - height)/2.0, width: width, height: height)
bottomView = WaveformView.init(frame: waveRect, amplitudes: amplitudes, isHighlight: true)
self.addSubview(bottomView)
topView = WaveformView.init(frame: waveRect, amplitudes: amplitudes, isHighlight: false)
self.addSubview(topView)
topViewMask = CALayer()
topViewMask.frame = topView.bounds
topViewMask.backgroundColor = UIColor.white.cgColor
topView.layer.mask = topViewMask
timeLine = UILabel.init(frame: CGRect.init(x: (frame.size.width - 61.5)/2.0, y: (frame.size.height - upMaxHeight - upDownSpace - downMaxHeight)/2.0 + upMaxHeight - 19.0, width: 61.5, height: 19.0))
timeLine.backgroundColor = UIColor.init(red: 18/255.0, green: 18/255.0, blue: 18/255.0, alpha: 0.72)
timeLine.layer.cornerRadius = 9.5
timeLine.layer.masksToBounds = true
timeLine.textColor = UIColor.white
timeLine.font = UIFont.init(name: "PingFangSC-Regular", size: 8.0)
timeLine.textAlignment = .center
timeLine.text = "\(convertTime(0))/\(convertTime(amplitudes.count/2))"
self.addSubview(timeLine)
let panGesture = UIPanGestureRecognizer.init(target: self, action: #selector(handleGesture(gesture:)))
panGesture.delegate = self
addGestureRecognizer(panGesture)
isUserInteractionEnabled = true
}
func configureAmplitudes(amplitudes: [Double]) {
let width = (waveWidth + waveSpace) * CGFloat(amplitudes.count / 2)
let height = upMaxHeight + downMaxHeight + upDownSpace
self.topView.amplitudes = amplitudes
self.topView.frame = CGRect(x: screenw/2, y: 0, width: width, height: height)
self.topView.setNeedsDisplay()
topViewMask.frame = topView.bounds
self.bottomView.amplitudes = amplitudes
self.bottomView.frame = CGRect(x: screenw/2, y: 0, width: width, height: height)
self.bottomView.setNeedsDisplay()
}
func play() {
if !isAnimated {
isAnimated = true
topView.layer.add(keyframeAnimationFrom(topView.layer.position.x, to: (self.bounds.size.width - topView.layer.bounds.size.width)/2, isTop: false), forKey: "pan")
topViewMask.add(keyframeAnimationFrom(topViewMask.position.x, to: topViewMask.bounds.size.width*3/2, isTop: false), forKey: "pan")
bottomView.layer.add(keyframeAnimationFrom(bottomView.layer.position.x, to: (self.bounds.size.width - bottomView.layer.bounds.size.width)/2, isTop: false), forKey: "pan")
weak var weakSelf = self
animationTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timer) in
guard let presentation = weakSelf?.topView.layer.presentation() else { return }
let delta = (weakSelf!.bounds.size.width + weakSelf!.topView.bounds.size.width)/2 - presentation.position.x
weakSelf!.timeLine.text = "\(weakSelf!.convertTime(Int(round(delta / 3))))/\(weakSelf!.convertTime(weakSelf!.topView.amplitudes.count/2))"
if weakSelf!.delegate != nil {
let offset = delta / 3
let distance = weakSelf!.topView.amplitudes.count/2
if distance > 0 {
weakSelf!.delegate?.didScrollByPercentage(percent: Double(offset) / Double(distance), animated: true)
}else {
weakSelf!.delegate?.didScrollByPercentage(percent: 0, animated: true)
}
}
})
}
}
func pause() {
if isAnimated {
topView.layer.position = topView.layer.presentation()!.position
topViewMask.position = topViewMask.presentation()!.position
bottomView.layer.position = bottomView.layer.presentation()!.position
removeAnimate()
}
}
func reset() {
timeLine.text = "\(convertTime(0))/\(convertTime(topView.amplitudes.count/2))"
let position = CGPoint(x: (self.size.width + topView.size.width) / 2, y: self.size.height / 2)
topView.layer.position = position
topView.layer.removeAllAnimations()
topViewMask.position = CGPoint(x: topView.size.width / 2, y: topView.size.height / 2)
topViewMask.removeAllAnimations()
bottomView.layer.position = position
bottomView.layer.removeAllAnimations()
isAnimated = false
stopTimer()
}
func initialOffset(offset: Int) {
let position = CGPoint(x: (self.size.width + topView.size.width) / 2 - 3 * CGFloat(offset), y: self.size.height / 2)
topView.layer.position = position
topViewMask.position = CGPoint(x: topView.size.width / 2 + 3 * CGFloat(offset), y: topView.size.height / 2)
bottomView.layer.position = position
timeLine.text = "\(convertTime(offset))/\(convertTime(topView.amplitudes.count/2))"
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.isKind(of: UISwipeGestureRecognizer.self) {
let swipe = otherGestureRecognizer as! UISwipeGestureRecognizer
if (swipe.direction == .up || swipe.direction == .down) && ((swipe.qmui_targetView?.parentViewController?.isKind(of: AudioPlayerViewController.self)) != nil) {
return true
}
}
return false
}
// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// return true
// }
#objc private func handleGesture(gesture: UIPanGestureRecognizer) {
if gesture.state == .changed {
let translation = gesture.translation(in: self)
let absX = abs(translation.x)
let absY = abs(translation.y)
if (absX > absY ) {
if (translation.x < 0) {
//向左滑动
if isAnimated {
topView.layer.position = CGPoint.init(x: max(topView.layer.presentation()!.position.x + translation.x, (self.bounds.size.width - topView.layer.bounds.size.width)/2), y: topView.layer.position.y)
topViewMask.position = CGPoint.init(x: min(topViewMask.presentation()!.position.x - translation.x, topViewMask.bounds.size.width*3/2), y: topViewMask.position.y)
bottomView.layer.position = CGPoint.init(x: max(bottomView.layer.presentation()!.position.x + translation.x, (self.bounds.size.width - bottomView.layer.bounds.size.width)/2), y: bottomView.layer.position.y)
}else {
if topView.layer.frame.origin.x + topView.layer.frame.size.width <= self.bounds.size.width / 2 {
print("左滑,切歌下一曲")
return
}
topView.layer.position = CGPoint.init(x: max(topView.layer.position.x + translation.x, (self.bounds.size.width - topView.layer.bounds.size.width)/2), y: topView.layer.position.y)
topViewMask.position = CGPoint.init(x: min(topViewMask.position.x - translation.x, topViewMask.bounds.size.width*3/2), y: topViewMask.position.y)
bottomView.layer.position = CGPoint.init(x: max(bottomView.layer.position.x + translation.x, (self.bounds.size.width - bottomView.layer.bounds.size.width)/2), y: bottomView.layer.position.y)
}
gesture.setTranslation(CGPoint.zero, in: self)
}else{
//向右滑动
if isAnimated {
topView.layer.position = CGPoint.init(x: min(topView.layer.presentation()!.position.x + translation.x, (self.bounds.size.width + topView.layer.bounds.size.width)/2), y: topView.layer.position.y)
topViewMask.position = CGPoint.init(x: max(topViewMask.presentation()!.position.x - translation.x, topViewMask.bounds.size.width/2), y: topViewMask.position.y)
bottomView.layer.position = CGPoint.init(x: min(bottomView.layer.presentation()!.position.x + translation.x, (self.bounds.size.width + bottomView.layer.bounds.size.width)/2), y: bottomView.layer.position.y)
}else {
if topView.layer.frame.origin.x >= self.bounds.size.width / 2 {
print("右滑,切歌上一曲")
return
}
topView.layer.position = CGPoint.init(x: min(topView.layer.position.x + translation.x, (self.bounds.size.width + topView.layer.bounds.size.width)/2), y: topView.layer.position.y)
topViewMask.position = CGPoint.init(x: max(topViewMask.position.x - translation.x, topViewMask.bounds.size.width/2), y: topViewMask.position.y)
bottomView.layer.position = CGPoint.init(x: min(bottomView.layer.position.x + translation.x, (self.bounds.size.width + bottomView.layer.bounds.size.width)/2), y: bottomView.layer.position.y)
}
gesture.setTranslation(CGPoint.zero, in: self)
}
removeAnimate()
scrollTimeLineWhetherNotice(notice: false)
if delegate != nil {
let offset = (self.size.width + topView.size.width) / 2 - topView.layer.position.x
let distance = topView.size.width
delegate?.didScrollByPercentage(percent: Double(offset) / Double(distance), animated: false)
}
}
}
if gesture.state == .ended {
//考虑到歌曲存在缓冲,请手动调用play方法
// play()
scrollTimeLineWhetherNotice(notice: true)
}
}
private func scrollTimeLineWhetherNotice(notice: Bool) {
let delta = (self.bounds.size.width + self.topView.bounds.size.width)/2 - self.topView.layer.position.x
var time = NSInteger(round(delta / 3))
if time >= topView.amplitudes.count / 2 {
time = topView.amplitudes.count / 2 - 1
}
timeLine.text = "\(convertTime(time))/\(convertTime(topView.amplitudes.count/2))"
if delegate != nil && notice {
delegate?.didScrollToTime(time: time)
}
}
private func removeAnimate() {
if isAnimated {
isAnimated = false
topView.layer.removeAnimation(forKey: "pan")
topViewMask.removeAnimation(forKey: "pan")
bottomView.layer.removeAnimation(forKey: "pan")
}
}
private func keyframeAnimationFrom(_ start: CGFloat, to end: CGFloat, isTop:Bool) -> CAAnimation {
let animation = CAKeyframeAnimation.init(keyPath: "position.x")
let scale = UIScreen.main.scale
let increment = copysign(1, end - start) / scale
let numberOfSteps = Int(abs((end - start) / increment))
let positions = NSMutableArray.init(capacity: numberOfSteps)
for i in 0..<numberOfSteps {
positions.add(start + CGFloat(i) * increment)
}
animation.values = (positions as! [Any])
animation.calculationMode = .discrete
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.duration = Double(Int(abs(end-start) / (AppConstants.waveWidth + AppConstants.waveSpace)))
animation.delegate = self
return animation
}
func animationDidStart(_ anim: CAAnimation) {
if anim == topView.layer.animation(forKey: "pan") {
}
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if UIApplication.shared.applicationState == .active && isVisible {
if isAnimated {
reset()
}
stopTimer()
}
}
private func stopTimer() {
guard let animationTimer = self.animationTimer else {
return
}
if animationTimer.isValid {
self.animationTimer.invalidate()
self.animationTimer = nil
}
}
deinit {
print("release WaveformComponent")
}
}
class WaveformView: UIView {
var isHighlight = false
var amplitudes = [Double]()
required init?(coder: NSCoder) {
super.init(coder: coder)
}
init(frame: CGRect, amplitudes: [Double], isHighlight: Bool) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
self.isOpaque = true
self.amplitudes = amplitudes
self.isHighlight = isHighlight
}
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
guard let context = UIGraphicsGetCurrentContext() else { return }
for i in 0..<amplitudes.count {
if i%2 == 0 {
//单数
let path = CGMutablePath()
let height = downMaxHeight * CGFloat(abs(amplitudes[i]))
path.addRoundedRect(in: CGRect.init(x: CGFloat(Int(i/2)) * (waveWidth + waveSpace), y: 62, width: 2.5, height: height), cornerWidth: 1.25, cornerHeight: 1.25 >= height/2.0 ? 0 : 1.25)
context.addPath(path)
if isHighlight {
context.setFillColor(highlightAlphaColor.cgColor)
}else {
context.setFillColor(normalAlphaColor.cgColor)
}
context.fillPath()
}else {
//双数
let path = CGMutablePath()
let height = upMaxHeight * CGFloat(abs(amplitudes[i]))
path.addRoundedRect(in: CGRect.init(x: CGFloat(Int(i/2)) * (waveWidth + waveSpace), y: 60 - height, width: 2.5, height: height), cornerWidth: 1.25, cornerHeight: 1.25 >= height/2.0 ? 0 : 1.25)
context.addPath(path)
if isHighlight {
context.setFillColor(highlightColor.cgColor)
}else {
context.setFillColor(normalColor.cgColor)
}
context.fillPath()
}
}
}
}
I don’t know if you want to create waveform from the scratch or custom, but there is a library called FDWaveformView and I have ever use this library in the past. After you install this library to your project, you can add a UIView which inherit FDWaveformView class, then provide the audio file.
Your code will likely look like this
import UIKit
import FDWaveformView
class ViewController: UIViewController {
#IBOutlet weak var mySampleWaveform: FDWaveformView!
override func viewDidLoad() {
super.viewDidLoad()
let thisBundle = Bundle(for: type(of: self))
let url = thisBundle.url(forResource: "myaudio", withExtension: "mp3")
mySampleWaveform.audioURL = url
mySampleWaveform.wavesColor = .green
mySampleWaveform.doesAllowScrubbing = true
mySampleWaveform.doesAllowStretch = true
mySampleWaveform.doesAllowScroll = true
}
}
it will show like this:
This will give you enough understanding about how does a waveform work, and you can custom many things such as color, width, height, etc.

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.

Specific Image Spawn

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

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
}
}
}

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