NSLayoutConsstraint constant not affecting view after UITapGestureRecognizer was tapped - ios

i have a view (self.view) that is masked with another view (not a layer) using the
UIView.mask property. on self.view i installed a UIPanGestureRecognizer so when i pan across the screen the mask gets smaller and larger accordingly. in addition i installed on self.view a UITapGestureRecognizer which adds animatable UIImageViews to the screen and they are animating across a UIBezierPath. i am updating the mask size with constraints.
the problem is that after i tap the screen to add animatable views the changes i make on the mask constraint stop taking affect. i can see in the log that i do indeed change the constant of the constraint and that the UIPanGestureRecognizer is still working.
so i mean that the mask view constraint stop affecting its view. why is that? thanks
video illustration: https://youtu.be/UtNuc8nicgs
here is the code:
class UICircle: UIView {
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.backgroundColor = .yellow
self.isUserInteractionEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var diameterConstraint: NSLayoutConstraint?
var animating = false
func updateSize(_ delta: CGFloat, animated: Bool = false) {
if animating { return }
if animated {
animating = true
diameterConstraint?.constant = UIScreen.main.bounds.height * 2.1
let duration: TimeInterval = 0.6
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.fromValue = self.layer.cornerRadius
animation.toValue = UIScreen.main.bounds.height * 2.1 / 2
animation.duration = duration
self.layer.add(animation, forKey: nil)
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
self.superview?.layoutIfNeeded()
}, completion: { (success) in
if success {
self.animating = false
}
})
} else {
let newSize = diameterConstraint!.constant + (delta * 2.85)
if newSize > 60 && newSize < UIScreen.main.bounds.height * 2.1 {
diameterConstraint?.constant = newSize
}
}
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superv = superview {
self.makeSquare()
self.centerHorizontallyTo(superv)
let c = NSLayoutConstraint.init(item: self, attribute: .centerY, relatedBy: .equal, toItem: superv, attribute: .bottom, multiplier: 1, constant: -40)
c.isActive = true
diameterConstraint = self.constrainHeight(superv.frame.height * 2.1)
}
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.width / 2
}
}
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var circle = UICircle()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
self.view.clipsToBounds = true
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tap.delegate = self
self.view.addGestureRecognizer(tap)
setupCircle()
}
func setupCircle() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
panGesture.delegate = self
self.view.addGestureRecognizer(panGesture)
self.view.mask = circle
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
var panStarted = false
func handlePan(_ pan: UIPanGestureRecognizer) {
let delta = pan.translation(in: self.view).y
if pan.state == .began {
if delta > 0 {
panStarted = true
circle.updateSize(-delta)
}
} else if pan.state == .changed {
if panStarted {
circle.updateSize(-delta)
}
} else if pan.state == .ended || pan.state == .cancelled {
if panStarted {
circle.updateSize(self.view.frame.height * 2.1, animated: true)
}
panStarted = false
}
pan.setTranslation(.zero, in: self.view)
}
func handleTap() {
let num = Int(5 + drand48() * 10)
(1 ... num).forEach { (_) in
addView()
}
}
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
func addView() {
var image: UIImageView!
let dd = drand48()
if dd < 0.5 {
image = UIImageView(image: #imageLiteral(resourceName: "heart1"))
} else {
image = UIImageView(image: #imageLiteral(resourceName: "heart2"))
}
image.isUserInteractionEnabled = false
image.contentMode = .scaleAspectFit
let dim: CGFloat = 20 + CGFloat(10 * drand48())
image.constrainHeight(dim)
image.constrainWidth(dim)
let animation = CAKeyframeAnimation(keyPath: "position")
let duration = Double(1.5 * self.view.frame.width / CGFloat((60 + drand48() * 40))) // duration = way / speed
animation.path = getPath().cgPath
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
image.layer.add(animation, forKey: nil)
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + duration + 1) {
DispatchQueue.main.async {
image.removeFromSuperview()
}
}
if drand48() < 0.3 {
UIView.animate(withDuration: 0.2 + 0.1 * drand48() , delay: TimeInterval(drand48() * 1), options: [.curveEaseOut, .repeat, .autoreverse], animations: {
image.transform = CGAffineTransform.init(scaleX: 1.5, y: 1.5)
}, completion: nil)
}
self.view.addSubview(image)
}
func getPath() -> UIBezierPath {
let path = UIBezierPath()
let startPoint = CGPoint.init(x: -30, y: self.view.frame.height / 2)
path.move(to: startPoint)
let r = CGFloat(400 * drand48())
let cp1 = CGPoint.init(x: self.view.frame.width * 0.33, y: self.view.frame.height * 0.25 - r)
let cp2 = CGPoint.init(x: self.view.frame.width * 0.66, y: self.view.frame.height * 0.75 + r)
let endPoint = CGPoint.init(x: self.view.frame.width + 30, y: self.view.frame.height / 2)
path.addCurve(to: endPoint, controlPoint1: cp1, controlPoint2: cp2)
return path
}
}
extension UIView {
#discardableResult
func makeSquare() -> NSLayoutConstraint {
self.turnOffMaskResizing()
let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self, attribute: NSLayoutAttribute.height, multiplier: 1.0, constant: 0)
NSLayoutConstraint.activate([constraint])
return constraint
}
#discardableResult
func centerHorizontallyTo(_ toItem: UIView, padding: CGFloat) -> NSLayoutConstraint {
self.turnOffMaskResizing()
let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: toItem, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: padding)
NSLayoutConstraint.activate([constraint])
return constraint
}
#discardableResult
func constrainHeight(_ height: CGFloat, priority: UILayoutPriority = 1000) -> NSLayoutConstraint {
self.turnOffMaskResizing()
let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.height, multiplier: 0, constant: height)
constraint.priority = priority
NSLayoutConstraint.activate([constraint])
return constraint
}
#discardableResult
func constrainWidth(_ width: CGFloat) -> [NSLayoutConstraint] {
self.turnOffMaskResizing()
let constraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[item(width)]", metrics: ["width" : width], views: ["item" : self])
NSLayoutConstraint.activate(constraints)
return constraints
}
func turnOffMaskResizing() {
self.translatesAutoresizingMaskIntoConstraints = false
}
}

I think it is because you add new objects to that view which will affect the constraints, and they break. What I propose is to add circle as a subview so it is not related to the other objects.
This is what I tried and it worked
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tap.delegate = self
self.view.addGestureRecognizer(tap)
setupCircle()
}
func setupCircle() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
panGesture.delegate = self
self.view.addSubview(circle)
self.circle.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
self.circle.clipsToBounds = true
self.view.addGestureRecognizer(panGesture)
}
EDIT:Added images of change what will happen in your hierarchy
Before tap
After tap
Your mask seems removed after the tap - But I am not sure how to fix that, still do not see reason why can't you add subview

This is proof of my concept, took and reworked CircleMaskView from https://stackoverflow.com/a/33076583/4284508. This does what you need. It is little bit mess, so do not take it as a done thing. I use your class to get frame and radius for the other mask, so you will need to get rid of it somehow and compute radius and frame in some other manner. But it will serve
/// Apply a circle mask on a target view. You can customize radius, color and opacity of the mask.
class CircleMaskView {
private var fillLayer = CAShapeLayer()
var target: UIView?
var fillColor: UIColor = UIColor.gray {
didSet {
self.fillLayer.fillColor = self.fillColor.cgColor
}
}
var radius: CGFloat? {
didSet {
self.draw()
}
}
var opacity: Float = 0.5 {
didSet {
self.fillLayer.opacity = self.opacity
}
}
/**
Constructor
- parameter drawIn: target view
- returns: object instance
*/
init(drawIn: UIView) {
self.target = drawIn
}
/**
Draw a circle mask on target view
*/
func draw() {
guard let target = target else {
print("target is nil")
return
}
var rad: CGFloat = 0
let size = target.frame.size
if let r = self.radius {
rad = r
} else {
rad = min(size.height, size.width)
}
let path = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:size.width, height:size.height), cornerRadius: 0.0)
let circlePath = UIBezierPath(roundedRect: CGRect(x:size.width / 2.0 - rad / 2.0, y:0, width:rad, height:rad), cornerRadius: rad)
path.append(circlePath)
path.usesEvenOddFillRule = true
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = self.fillColor.cgColor
fillLayer.opacity = self.opacity
target.layer.addSublayer(fillLayer)
}
func redraw(withCircle circle: UICircle) {
guard let target = target else {
print("target is nil")
return
}
var rad: CGFloat = 0
let size = target.frame.size
if let r = self.radius {
rad = r
} else {
rad = min(size.height, size.width)
}
let path = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:size.width, height:size.height), cornerRadius: 0.0)
let circlePath = UIBezierPath(roundedRect: circle.frame, cornerRadius: circle.diameterConstraint!.constant)
path.append(circlePath)
path.usesEvenOddFillRule = true
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = self.fillColor.cgColor
fillLayer.opacity = self.opacity
target.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
target.layer.addSublayer(fillLayer)
}
/**
Remove circle mask
*/
func remove() {
self.fillLayer.removeFromSuperlayer()
}
}
var circle = UICircle()
var circleMask: CircleMaskView?
var subviewC = UIView()
override func viewDidLoad() {
super.viewDidLoad()
self.subviewC.clipsToBounds = true
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tap.delegate = self
self.view.addGestureRecognizer(tap)
view.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
subviewC.backgroundColor = .clear
subviewC.frame = view.frame
self.view.addSubview(subviewC)
self.view.addSubview(circle)
circle.backgroundColor = .clear
setupCircle()
}
func setupCircle() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
panGesture.delegate = self
self.subviewC.addGestureRecognizer(panGesture)
circleMask = CircleMaskView(drawIn: subviewC)
circleMask?.opacity = 1.0
circleMask?.draw()
}
override func viewDidLayoutSubviews() {
circleMask?.redraw(withCircle: circle)
}
func handlePan(_ pan: UIPanGestureRecognizer) {
let delta = pan.translation(in: self.view).y
if pan.state == .began {
if delta > 0 {
panStarted = true
circle.updateSize(-delta)
circleMask?.redraw(withCircle: circle)
}
} else if pan.state == .changed {
if panStarted {
circle.updateSize(-delta)
circleMask?.redraw(withCircle: circle)
}
} else if pan.state == .ended || pan.state == .cancelled {
if panStarted {
circle.updateSize(self.view.frame.height * 2.1, animated: true)
circleMask?.redraw(withCircle: circle)
}
panStarted = false
}
pan.setTranslation(.zero, in: self.view)
}

Related

CAShapeLayer with different Colors

I have a CAShapeLayer based on this answer that animates along with a UISlider.
It works fine but as the shapeLayer follows along its just 1 red CAGradientLayer color. What I want is the shapeLayer to change colors based on certain points of the slider. An example is at 0.4 - 0.5 it's red, 0.7-0.8 red, 0.9-0.95 red. Those aren't actual values, the actual values will vary. I figure that any time it doesn't meet the condition to turn red it should probably just be a clear color, which will just show the black track underneath it. The result would look something like this (never mind the shape)
The red colors are based on the user scrubbing the slider and the letting go. The different positions of the slider that determine the red color is based on whatever condition. How can I do this.
UISlider
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.minimumTrackTintColor = .blue
s.maximumTrackTintColor = .white
s.minimumValue = 0
s.maximumValue = 1
s.addTarget(self, action: #selector(onSliderChange), for: .valueChanged)
return s
s.addTarget(self, action: #selector(onSliderEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return s
}()
lazy var progressView: GradientProgressView = {
let v = GradientProgressView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
#objc fileprivate func onSliderChange(_ slider: UISlider) {
let condition: Bool = // ...
let value = slider.value
progressView.setProgress(CGFloat(value), someCondition: condition, slider_X_Position: slider_X_PositionInView())
}
#objc fileprivate func onSliderEnded(_ slider: UISlider) {
let value = slider.value
progressView.resetProgress(CGFloat(value))
}
// ... progressView is the same width as the the slider
func slider_X_PositionInView() -> CGFloat {
let trackRect = slider.trackRect(forBounds: slider.bounds)
let thumbRect = slider.thumbRect(forBounds: slider.bounds,
trackRect: trackRect,
value: slider.value)
let convertedThumbRect = slider.convert(thumbRect, to: self.view)
return convertedThumbRect.midX
}
GradientProgressView:
public class GradientProgressView: UIView {
var shapeLayer: CAShapeLayer = {
// ...
}()
private var trackLayer: CAShapeLayer = {
let trackLayer = CAShapeLayer()
trackLayer.strokeColor = UIColor.black.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
return trackLayer
}()
private var gradient: CAGradientLayer = {
let gradient = CAGradientLayer()
let redColor = UIColor.red.cgColor
gradient.colors = [redColor, redColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
return gradient
}()
// ... add the above layers as subLayers to self ...
func updatePaths() { // added in layoutSubviews
let lineWidth = bounds.height / 2
trackLayer.lineWidth = lineWidth * 0.75
shapeLayer.lineWidth = lineWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
trackLayer.path = path.cgPath
shapeLayer.path = path.cgPath
gradient.frame = bounds
gradient.mask = shapeLayer
shapeLayer.duration = 1
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
}
public func setProgress(_ progress: CGFloat, someCondition: Bool, slider_X_Position: CGFloat) {
// slider_X_Position might help with shapeLayer's x position for the colors ???
if someCondition {
// redColor until the user lets go
} else {
// otherwise always a clearColor
}
shapeLayer.strokeEnd = progress
}
}
public func resetProgress(_ progress: CGFloat) {
// change to clearColor after finger is lifted
}
}
To get this:
We can use a CAShapeLayer for the red "boxes" and a CALayer as a .mask on that shape layer.
To reveal / cover the boxes, we set the frame of the mask layer to a percentage of the width of the bounds.
Here's a complete example:
class StepView: UIView {
public var progress: CGFloat = 0 {
didSet {
setNeedsLayout()
}
}
public var steps: [[CGFloat]] = [[0.0, 1.0]] {
didSet {
setNeedsLayout()
}
}
public var color: UIColor = .red {
didSet {
stepLayer.fillColor = color.cgColor
}
}
private let stepLayer = CAShapeLayer()
private let maskLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
layer.addSublayer(stepLayer)
stepLayer.fillColor = color.cgColor
stepLayer.mask = maskLayer
// mask layer can use any solid color
maskLayer.backgroundColor = UIColor.white.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
stepLayer.frame = bounds
let pth = UIBezierPath()
steps.forEach { pair in
// rectangle for each "percentage pair"
let w = bounds.width * (pair[1] - pair[0])
let b = UIBezierPath(rect: CGRect(x: bounds.width * pair[0], y: 0, width: w, height: bounds.height))
pth.append(b)
}
stepLayer.path = pth.cgPath
// update frame of mask layer
var r = bounds
r.size.width = bounds.width * progress
maskLayer.frame = r
}
}
class StepVC: UIViewController {
let stepView = StepView()
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(slider)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
slider.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let steps: [[CGFloat]] = [
[0.1, 0.3],
[0.4, 0.5],
[0.7, 0.8],
[0.9, 0.95],
]
stepView.steps = steps
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ sender: UISlider) {
// disable CALayer "built-in" animations
CATransaction.setDisableActions(true)
stepView.progress = CGFloat(sender.value)
CATransaction.commit()
}
}
Edit
I'm still not clear on your 0.4 - 0.8 requirement, but maybe this will help get you on your way:
Please note: this is Example Code Only!!!
struct RecordingStep {
var color: UIColor = .black
var start: Float = 0
var end: Float = 0
var layer: CALayer!
}
class StepView2: UIView {
public var progress: Float = 0 {
didSet {
// move the progress layer
progressLayer.position.x = bounds.width * CGFloat(progress)
// if we're recording
if isRecording {
let i = theSteps.count - 1
guard i > -1 else { return }
// update current "step" end
theSteps[i].end = progress
setNeedsLayout()
}
}
}
private var isRecording: Bool = false
private var theSteps: [RecordingStep] = []
private let progressLayer = CAShapeLayer()
public func startRecording(_ color: UIColor) {
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
let l = CALayer()
l.backgroundColor = st.color.cgColor
layer.insertSublayer(l, below: progressLayer)
st.layer = l
theSteps.append(st)
isRecording = true
}
public func stopRecording() {
isRecording = false
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
progressLayer.lineWidth = 3
progressLayer.strokeColor = UIColor.green.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// only set the progessLayer frame if the bounds height has changed
if progressLayer.frame.height != bounds.height + 7.0 {
let r: CGRect = CGRect(origin: .zero, size: CGSize(width: 7.0, height: bounds.height + 7.0))
let pth = UIBezierPath(roundedRect: r, cornerRadius: 3.5)
progressLayer.frame = r
progressLayer.position = CGPoint(x: 0, y: bounds.midY)
progressLayer.path = pth.cgPath
}
theSteps.forEach { st in
let x = bounds.width * CGFloat(st.start)
let w = bounds.width * CGFloat(st.end - st.start)
let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
st.layer.frame = r
}
}
}
class Step2VC: UIViewController {
let stepView = StepView2()
let actionButton: UIButton = {
let b = UIButton()
b.backgroundColor = .lightGray
b.setImage(UIImage(systemName: "play.fill"), for: [])
b.tintColor = .systemGreen
return b
}()
var timer: Timer!
let colors: [UIColor] = [
.red, .systemBlue, .yellow, .cyan, .magenta, .orange,
]
var colorIdx: Int = -1
var action: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(actionButton)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
actionButton.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
actionButton.widthAnchor.constraint(equalToConstant: 80.0),
actionButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
actionButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
}
#objc func timerFunc(_ timer: Timer) {
// don't set progress > 1.0
stepView.progress = min(stepView.progress + 0.005, 1.0)
if stepView.progress >= 1.0 {
timer.invalidate()
actionButton.isHidden = true
}
}
#objc func btnTap(_ sender: UIButton) {
switch action {
case 0:
// this will run for 15 seconds
timer = Timer.scheduledTimer(timeInterval: 0.075, target: self, selector: #selector(timerFunc(_:)), userInfo: nil, repeats: true)
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
case 1:
colorIdx += 1
stepView.startRecording(colors[colorIdx % colors.count])
actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
actionButton.tintColor = .black
action = 2
case 2:
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
default:
()
}
}
}
For future reference, when posting here, it's probably a good idea to fully explain what you're trying to do. Showing code you're working on is important, but if it's really only sorta related to your actual goal, it makes this process pretty difficult.

UICollisionBehavior detection between round subViews in square parentViews

I have a square containerView with a roundImageView inside of it. The containerView is added to the UIDynamicAnimator. When the corners of the containerViews collide off of each other I need them to bounce off of the roundImageView, same as this question. Inside the the customContainerView I override collisionBoundsType ... return .ellipse but the collision is still occurs from the square and not the circle, and the views are overlapping each other.
customView:
class CustomContainerView: UIView {
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
}
code:
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior : UIDynamicItemBehavior!
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let redView = createContainerView(with: .red)
let blueView = createContainerView(with: .blue)
let yellowView = createContainerView(with: .yellow)
let purpleView = createContainerView(with: .purple)
let greenView = createContainerView(with: .green)
view.addSubview(redView)
view.addSubview(blueView)
view.addSubview(yellowView)
view.addSubview(purpleView)
view.addSubview(greenView)
arr = [redView, blueView, yellowView, purpleView, greenView]
}
func createContainerView(with color: UIColor) -> UIView {
let containerView = CustomContainerView()
containerView.backgroundColor = .brown
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
containerView.center = view.center
let roundImageView = UIImageView()
roundImageView.translatesAutoresizingMaskIntoConstraints = false
roundImageView.backgroundColor = color
containerView.addSubview(roundImageView)
roundImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundImageView.layer.masksToBounds = true
roundImageView.layoutIfNeeded()
roundImageView.layer.cornerRadius = roundImageView.frame.height / 2
roundImageView.layer.borderWidth = 1
roundImageView.layer.borderColor = UIColor.white.cgColor
return containerView
}
Looks like collision behavior doesn't like .ellipse type when the views are positioned exactly on top of each other.
Running your code a few times gives different results (as expected)... sometimes, all 5 views end up in a full vertical stack, other times it ends up with some overlap, and other times (after waiting a few seconds) the views settle with a couple visible and the others way below the bottom of the view - I've seen their y-positions get to > 40,000.
I made a few modifications to your code to see what's happening...
I added more views and gave each one a shape layer showing the ellipse bounds.
Then, instead of starting with them all at identical positions, I created a couple "rows" so it looks like this:
Then, on each tap, I reset the original positions and toggle the UIDynamicItemCollisionBoundsType between ellipse and rectangle, and then call addAnimatorAndBehaviors() again.
Here's how it looks on sample .ellipse run:
and on sample .rectangle run:
As we can see, the .ellipse bounds are being used.
Here's the code I used to play with this:
class CustomContainerView: UIView {
var useEllipse: Bool = false
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return useEllipse ? .ellipse : .rectangle
}
}
class ViewController: UIViewController {
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior : UIDynamicItemBehavior!
let infoLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
// add info label
infoLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(infoLabel)
infoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
infoLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// add a tap recognizer to start the Animator Behaviors
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
positionViews()
}
func positionViews() -> Void {
// let's make rows of the views,
// instead of starting with them all on top of each other
// we'll do 3-views over 2-views
let w = arr[0].frame.width * 1.1
let h = arr[0].frame.height * 1.1
var x: CGFloat = 0
var y: CGFloat = 0
var idx: Int = 0
y = h
while idx < arr.count {
x = view.center.x - w
for _ in 1...3 {
if idx < arr.count {
arr[idx].center = CGPoint(x: x, y: y)
}
x += w
idx += 1
}
y += h
x = view.center.x - w * 0.5
for _ in 1...2 {
if idx < arr.count {
arr[idx].center = CGPoint(x: x, y: y)
}
x += w
idx += 1
}
y += h
}
}
#objc func gotTap(_ g: UIGestureRecognizer) -> Void {
positionViews()
arr.forEach { v in
v.useEllipse.toggle()
}
infoLabel.text = arr[0].useEllipse ? "Ellipse" : "Rectangle"
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let clrs: [UIColor] = [
.red, .green, .blue,
.purple, .orange,
.cyan, .yellow, .magenta,
.systemTeal, .systemGreen,
]
clrs.forEach { c in
let v = createContainerView(with: c)
view.addSubview(v)
arr.append(v)
}
}
func createContainerView(with color: UIColor) -> CustomContainerView {
let containerView = CustomContainerView()
containerView.backgroundColor = UIColor.brown.withAlphaComponent(0.2)
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
view.addSubview(containerView)
let roundImageView = UIImageView()
roundImageView.translatesAutoresizingMaskIntoConstraints = false
roundImageView.backgroundColor = color
containerView.addSubview(roundImageView)
roundImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundImageView.layer.masksToBounds = true
roundImageView.layoutIfNeeded()
roundImageView.layer.cornerRadius = roundImageView.frame.height / 2
roundImageView.layer.borderWidth = 1
roundImageView.layer.borderColor = UIColor.white.cgColor
// let's add a CAShapeLayer to show the ellipse bounds
let c = CAShapeLayer()
c.fillColor = UIColor.clear.cgColor
c.lineWidth = 1
c.strokeColor = UIColor.black.cgColor
c.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath
containerView.layer.addSublayer(c)
return containerView
}
}
Edit
Change the while loop in positionViews() to this... tap to reset and run the animation a number of times and see what happens when all the views start with the same frame:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
Then, use this while loop, where we start the views at the same x-position, but increment the y-position for each view (just by 0.1 points):
while idx < arr.count {
x = view.center.x - w
// increment the y position for each view -- just a tad
y += 0.1
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
Another Edit
Worth noting, the fact that the ellipse collision bounds is round (1:1 ratio), also affects things.
If we change the size of the view frames just slightly, we get very different results.
Try it with:
let size = CGSize(width: 50.1, height: 50)
and start them all with the exact same center point:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
and you'll see the views spread out immediately.
One more Edit - to help visualize the differences
Here's another example - this time, I've numbered the views and set a "every 1/10th second" timer to update a label with the current center of each view.
Also added segmented controls to select collisionBoundsType and overlaying the views exactly on top of each other or offsetting them slightly:
class CustomContainerView: UIView {
var useEllipse: Bool = false
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return useEllipse ? .ellipse : .rectangle
}
}
// extension to left-pad a string up-to length
extension RangeReplaceableCollection where Self: StringProtocol {
func paddingToLeft(upTo length: Int, using element: Element = " ") -> SubSequence {
return repeatElement(element, count: Swift.max(0, length-count)) + suffix(Swift.max(count, count-length))
}
}
class CollisionVC: UIViewController {
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior: UIDynamicItemBehavior!
let infoLabel = UILabel()
// add segmented controls for collisionBoundsType and "Spread Layout"
let seg1 = UISegmentedControl(items: ["Ellipse", "Rectangle"])
let seg2 = UISegmentedControl(items: ["Overlaid", "Offset"])
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
[seg1, seg2, infoLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
infoLabel.numberOfLines = 0
infoLabel.font = .monospacedSystemFont(ofSize: 14.0, weight: .light)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
seg1.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
seg1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
seg2.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
seg2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
seg1.selectedSegmentIndex = 0
seg2.selectedSegmentIndex = 0
// add a tap recognizer to start the Animator Behaviors
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
// run a Timer... every 1/10th second we'll fill the infoLabel with
// collisionBoundsType and a list of center points
// for all subviews
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if self.animator != nil {
var s = ""
for i in 0..<self.arr.count {
let c = self.arr[i].center
let xs = String(format: "%0.2f", c.x)
let ys = String(format: "%0.2f", c.y)
s += "\n\(i) - x: \(String(xs.paddingToLeft(upTo: 7))) y: \(String(ys.paddingToLeft(upTo: 9)))"
}
s += "\nAnimator is running: " + (self.animator.isRunning ? "Yes" : "No")
self.infoLabel.text = s
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
positionViews()
}
func positionViews() -> Void {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
arr.forEach { v in
v.center = CGPoint(x: view.center.x + x, y: view.safeAreaInsets.top + 100.0 + y)
// if seg2 == Overlaid, position all views exactly on top of each other
// else, Offset the x,y center of each one by 0.1 pts
// Offsetting them allows the animator to use
// "valid" collision adjustments on start
if seg2.selectedSegmentIndex == 1 {
x += 0.1
y += 0.1
}
// set collisionBoundsType
v.useEllipse = seg1.selectedSegmentIndex == 0
}
}
#objc func gotTap(_ g: UIGestureRecognizer) -> Void {
positionViews()
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let clrs: [UIColor] = [
.red, .green, UIColor(red: 1.0, green: 0.85, blue: 0.55, alpha: 1.0),
UIColor(red: 1.0, green: 0.5, blue: 1.0, alpha: 1.0), .orange,
.cyan, .yellow, .magenta,
.systemTeal, .systemGreen,
]
for (c, i) in zip(clrs, (0..<clrs.count)) {
let v = createContainerView(with: c, number: i)
view.addSubview(v)
arr.append(v)
}
}
func createContainerView(with color: UIColor, number: Int) -> CustomContainerView {
let containerView = CustomContainerView()
containerView.backgroundColor = UIColor.brown.withAlphaComponent(0.2)
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
view.addSubview(containerView)
let roundLabel = UILabel()
roundLabel.translatesAutoresizingMaskIntoConstraints = false
roundLabel.backgroundColor = color
roundLabel.text = "\(number)"
roundLabel.textAlignment = .center
containerView.addSubview(roundLabel)
roundLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundLabel.layer.masksToBounds = true
roundLabel.layoutIfNeeded()
roundLabel.layer.cornerRadius = roundLabel.frame.height / 2
roundLabel.layer.borderWidth = 1
roundLabel.layer.borderColor = UIColor.white.cgColor
// let's add a CAShapeLayer to show the ellipse bounds
let c = CAShapeLayer()
c.fillColor = UIColor.clear.cgColor
c.lineWidth = 1
c.strokeColor = UIColor.black.cgColor
c.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath
containerView.layer.addSublayer(c)
return containerView
}
}
Worth noting: when the collisionBoundsType == .ellipse and the views start exactly on top of each other, the collision algorithm can (and usually does) end up pushing a couple views off the bottom, which puts them outside the reference system’s bounds. At that point, the algorithm continues trying to collide those views, pushing them further and further down o the Y axis.
Here is the output after letting it run for a few seconds:
Views 5, 7 and 8 are way out of bounds, and the animator is still running. Those views will continue to be pushed further and further down, presumably until we get an invalid point crash (I haven't let it run long enough to find out).
Also, because the animator ends up doing so much processing on those out-of-bounds views, the collision detection on the remaining views suffers.

How to create swipe to start button with moving arrows

I want to create the exactly the same swipe button like this https://github.com/shadowfaxtech/proSwipeButton .
I was wondering how to change the arrow of the button on user touches
I was doing this for getting swipe action.
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
rightSwipe.direction = .right
view.addGestureRecognizer(rightSwipe)
but the thing is how to add arrows to button which change there position on user touches.
Here is the code I have written for swiping over the button. You assign image to the image view.
func createSwipeButton() {
let button = UIButton.init(type: .custom)
button.backgroundColor = UIColor.brown
button.setTitle("PLACE ORDER", for: .normal)
button.frame = CGRect.init(x: 10, y: 200, width: self.view.frame.size.width-20, height: 100)
button.addTarget(self, action: #selector(swiped(_:event:)), for: .touchDragInside)
button.addTarget(self, action: #selector(swipeEnded(_:event:)), for: .touchUpInside)
self.view.addSubview(button)
let swipableView = UIImageView.init()
swipableView.frame = CGRect.init(x: 0, y: 0, width: 20, height: button.frame.size.height)
swipableView.tag = 20
swipableView.backgroundColor = UIColor.white
button.addSubview(swipableView)
}
#objc func swiped(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender,isSwiping: true)
UIView.animate(withDuration: 0.2) {
swipableView.center = centerPosition
}
}
#objc func swipeEnded(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender, isSwiping: false)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 5, options: .curveEaseInOut, animations: {
swipableView.center = centerPosition
}) { _ in}
}
func location(event: UIEvent, subView: UIView, superView: UIButton, isSwiping: Bool) -> CGPoint {
if let touch = event.touches(for: superView)?.first{
let previousLocation = touch.previousLocation(in: superView)
let location = touch.location(in: superView)
let delta_x = location.x - previousLocation.x;
print(subView.center.x + delta_x)
var centerPosition = CGPoint.init(x: subView.center.x + delta_x, y: subView.center.y)
let minX = subView.frame.size.width/2
let maxX = superView.frame.size.width - subView.frame.size.width/2
centerPosition.x = centerPosition.x < minX ? minX : centerPosition.x
centerPosition.x = centerPosition.x > maxX ? maxX : centerPosition.x
if !isSwiping{
let normalPosition = superView.frame.size.width * 0.5
centerPosition.x = centerPosition.x > normalPosition ? maxX : minX
centerPosition.x = centerPosition.x <= normalPosition ? minX : centerPosition.x
}
return centerPosition
}
return CGPoint.zero
}
Complete project is on github: https://github.com/IamSaurav/SwipeButton
Mmm what about something like this?
You can add an UIImage in the storyboard in the swipeImage var.
The best effect is done if the image has the same color of the text.
import UIKit
#IBDesignable
class UISwipeableLabel: UILabel {
#IBInspectable var swipeImage: UIImage? {
didSet {
configureSwipeImage()
}
}
private var swipeImageView: UIImageView?
private var rightSwipe: UIPanGestureRecognizer?
private var shouldActivateButton = true
override func awakeFromNib() {
super.awakeFromNib()
configureSwipeImage()
clipsToBounds = true
}
}
private extension UISwipeableLabel {
#objc func handleSwipes(_ sender:UIPanGestureRecognizer) {
if let centerX = swipeImageView?.center.x {
let translation = sender.translation(in: self)
let percent = centerX/frame.width
if sender.state == .changed {
if centerX < frame.width - frame.height/2 {
swipeImageView?.center.x = centerX + translation.x
sender.setTranslation(CGPoint.zero, in: swipeImageView)
} else {
swipeImageView?.center.x = frame.width - frame.height/2
if shouldActivateButton {
activateButton()
}
}
}
if sender.state == .ended || sender.state == .cancelled || sender.state == .failed {
if shouldActivateButton {
UIView.animate(withDuration: 0.25 * TimeInterval(percent)) {
self.swipeImageView?.center.x = self.frame.height/2
}
}
}
}
}
func configureSwipeImage() {
if swipeImageView != nil {
swipeImageView?.removeFromSuperview()
}
swipeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.height, height: frame.height))
if let swipeImageView = swipeImageView {
swipeImageView.image = swipeImage
swipeImageView.isUserInteractionEnabled = true
swipeImageView.alpha = 0.5
addSubview(swipeImageView)
rightSwipe = UIPanGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
if let rightSwipe = rightSwipe {
swipeImageView.addGestureRecognizer(rightSwipe)
}
}
}
func activateButton() {
print("*** DO YOUR STUFF HERE ***")
}
}
You start with a UILabel and if you want, change it to use autolayout.

UIPanGestureRecognizer on subclass UIView

I've been trying to lean more about subclassing certain objects.
Now I've subclassed a UIView which has a PanGestureRecognizer for swiping left and right.
Can't seem to find the problem. It won't even move the UIView. I've tried looking the lifecycle of an UIView to set the isUserInteractionEnabled to true, but with no result. See the code below:
VC
import UIKit
class SwipeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addNewProfile()
}
private func addNewProfile() {
let swipeView = SwiperView(frame: CGRect(x: self.view.bounds.width / 2 - 150, y: self.view.bounds.height / 2 - 75, width: 300, height: 150))
swipeView.parentView = self.view
swipeView.delegate = self
swipeView.shadow = true
swipeView.isUserInteractionEnabled = true
swipeView.backgroundColor = UIColor.white
swipeView.alpha = 0.0
view.addSubview(swipeView)
UIView.animate(withDuration: 0.3, animations: {
swipeView.alpha = 1.0
}, completion: { (succeed) in
swipeView.isUserInteractionEnabled = true
})
}
}
//MARK: - ChosenSwipeResultDelegate
extension SwipeViewController: ChosenSwipeResultDelegate {
func pickedLeftSide() {
}
func pickedRightSide() {
}
}
SwiperView
import UIKit
protocol ChosenSwipeResultDelegate {
func pickedLeftSide()
func pickedRightSide()
}
#IBDesignable class SwiperView: UIView {
private var _shadow: Bool!
private var _parentView: UIView!
var delegate: ChosenSwipeResultDelegate?
var parentView: UIView {
set {
_parentView = newValue
}
get {
return _parentView
}
}
#IBInspectable var shadow: Bool {
get {
return layer.shadowOpacity > 0.0
}
set {
if newValue == true {
addShadow()
}
}
}
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
if shadow == false {
layer.masksToBounds = true
}
}
}
override func setNeedsLayout() {
super.setNeedsLayout()
isUserInteractionEnabled = true
}
override func awakeFromNib() {
super.awakeFromNib()
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
func dragging(gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: parentView)
let tinderView = gesture.view!
tinderView.center = CGPoint(x: parentView.bounds.width / 2 + translation.x, y: parentView.bounds.height / 2 + translation.y)
let xFromCenter = tinderView.center.x - parentView.bounds.width / 2
let scale = min(100 / abs(xFromCenter), 1)
var rotation = CGAffineTransform(rotationAngle: xFromCenter / 200)
let stretch = rotation.scaledBy(x: scale, y: scale)
tinderView.transform = stretch
if gesture.state == .ended {
if tinderView.center.x < 100 {
print("left")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedLeftSide()
})
} else if tinderView.center.x > parentView.bounds.width - 100 {
print("right")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedRightSide()
})
} else {
print("Not chosen")
rotation = CGAffineTransform(rotationAngle: 0)
let stretch = rotation.scaledBy(x: 1, y: 1)
tinderView.transform = stretch
tinderView.center = CGPoint(x: parentView.bounds.width / 2, y: parentView.bounds.height / 2)
}
}
}
private func addShadow(shadowColor: CGColor = UIColor.black.cgColor, shadowOffset: CGSize = CGSize(width: 1.0, height: 2.0), shadowOpacity: Float = 0.4, shadowRadius: CGFloat = 3.0) {
layer.shadowColor = shadowColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
}
add these code to your SwiperView see if it solves the problem
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}

Image Slideshow Swift ios

I'm new to ios development. I am trying to make a simple fullscreen image slide show. On swipe left, the slideshow should show the next image, and swipe right the slideshow should show the previous image.
I have it working, however, if I swipe in quick succession, I get a blank screen, almost as if the animations aren't keeping up, and then when I wait a moment and swipe again the image views speed up into place and works normally again. Any idea what I'm doing wrong? What is the best practice when it comes to implementing an image carousel like this with a dynamic amount of images (here they're hardcoded)?
import UIKit
var imageArr = ["imageOne.jpg", "imageTwo.jpg", "imageThree.jpg", "imageFour.jpg", "imageFive.jpg"]
var imageIndex = 0;
class ViewController: UIViewController {
var currImage = UIImageView()
var rightImage = UIImageView()
var leftImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var bounds:CGRect = UIScreen.mainScreen().bounds
var width:CGFloat = bounds.size.width
var height:CGFloat = bounds.size.height
currImage.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
currImage.image = UIImage(named: imageArr[imageIndex])
rightImage.frame = CGRect(x: width, y: 0.0, width: width, height: height)
rightImage.image = UIImage(named: imageArr[imageIndex + 1])
leftImage.frame = CGRect(x: -width, y: 0.0, width: width, height: height)
leftImage.image = UIImage(named: imageArr[imageArr.count - 1])
self.view.addSubview(currImage)
self.view.addSubview(rightImage)
self.view.addSubview(leftImage)
var swipeLeft = UISwipeGestureRecognizer(target: self, action: "handleSwipe:")
swipeLeft.direction = UISwipeGestureRecognizerDirection.Left
self.view.addGestureRecognizer(swipeLeft)
var swipeRight = UISwipeGestureRecognizer(target: self, action: "handleSwipe:")
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
self.view.addGestureRecognizer(swipeRight)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
let transitionManager = TransitionManager()
func handleSwipe(gesture: UIGestureRecognizer) {
var bounds:CGRect = UIScreen.mainScreen().bounds
var width:CGFloat = bounds.size.width
var height:CGFloat = bounds.size.height
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
if (swipeGesture.direction == UISwipeGestureRecognizerDirection.Left ) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.currImage.frame = CGRect(x: -width, y: 0.0, width: width, height: height)
self.rightImage.frame = CGRect(x: 0.0, y:0.0, width: width, height: height)
}, completion: { finished in
if (!finished) { return }
imageIndex++
imageIndex = imageIndex <= imageArr.count-1 ? imageIndex : 0
var leftIndex = imageIndex - 1 < 0 ? imageArr.count - 1 : imageIndex - 1
self.leftImage.image = UIImage(named: imageArr[leftIndex])
self.leftImage.frame = CGRect(x: -width, y: 0.0, width: width, height: height)
var tempImg = self.currImage
self.currImage = self.rightImage
self.rightImage = tempImg
self.rightImage.frame = CGRect(x: width, y: 0.0, width: width, height: height)
var rightIndex = imageIndex + 1 > imageArr.count - 1 ? 0 : imageIndex + 1
self.rightImage.image = UIImage(named: imageArr[rightIndex])
})
}
if (swipeGesture.direction == UISwipeGestureRecognizerDirection.Right) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.currImage.frame = CGRect(x: width, y: 0.0, width: width, height: height)
self.leftImage.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
}, completion: { finished in
imageIndex--
imageIndex = imageIndex < 0 ? imageArr.count - 1 : imageIndex
var rightIndex = imageIndex + 1 > imageArr.count - 1 ? 0 : imageIndex + 1
self.rightImage.image = UIImage(named: imageArr[rightIndex])
self.rightImage.frame = CGRect(x: width, y: 0.0, width: width, height: height)
var tempImg = self.currImage
self.currImage = self.tempImg
self.leftImage = tempCurr
self.leftImage.frame = CGRect(x: -width, y: 0.0, width: width, height: height)
var leftIndex = imageIndex - 1 < 0 ? imageArr.count - 1 : imageIndex - 1
self.leftImage.image = UIImage(named: imageArr[leftIndex])
})
}
}
}
}
Any help is much appreciated!
#IBOutlet weak var imageView:UIImageView!
var i=Int()
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(imageChange), userInfo: nil, repeats: true)
// Do any additional setup after loading the view.
}
#objc func imageChange(){
self.imageView.image=images[i]
if i<images.count-1{
i+=1
}
else{
i=0
}
}
I have tried CollectionView for the carousel slideshow, but it didn't work out for me. I didn't like the hackish ways I had to do to make it show images in one row and I also didn't like the fact that it cannot return the active image (there is some workaround here as well, but they don't seem reliable). So, naturally, I ended up building a custom slideshow carousel for my purpose. I will share the code here, so hopefully, it can help(or at least guide someone) with a similar problem.
NOTE: My carousel is full width, singleImagePerScreen carousel, with a swipe recognizer to swipe through images and delegate function that is triggered when an image is active(I use it to display active image - "1 of 5").
TESTED ON: SWIFT 5, XCode 12.2, iOS 14.2
// ImageCarouselView class
import UIKit
class ImageCarouselView: UIView {
private let images: [UIImage?]
private var index = 0
private let screenWidth = UIScreen.main.bounds.width
var delegate: ImageCarouselViewDelegate?
lazy var previousImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var currentImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var nextImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var previousImageLeadingConstraint: NSLayoutConstraint = {
return previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
}()
lazy var currentImageLeadingConstraint: NSLayoutConstraint = {
return currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
}()
lazy var nextImageLeadingConstraint: NSLayoutConstraint = {
return nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
}()
init(_ images: [UIImage?]) {
self.images = images
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
setupLayout()
setupImages()
setupSwipeRecognizer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupLayout() {
self.subviews.forEach({ $0.removeFromSuperview() })
addSubview(previousImageView)
addSubview(currentImageView)
addSubview(nextImageView)
previousImageLeadingConstraint = previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
currentImageLeadingConstraint = currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
nextImageLeadingConstraint = nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
NSLayoutConstraint.activate([
previousImageLeadingConstraint,
previousImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
previousImageView.widthAnchor.constraint(equalToConstant: screenWidth),
currentImageLeadingConstraint,
currentImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
currentImageView.widthAnchor.constraint(equalToConstant: screenWidth),
nextImageLeadingConstraint,
nextImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
nextImageView.widthAnchor.constraint(equalToConstant: screenWidth),
])
}
private func setupImages() {
currentImageView.image = images[self.index]
guard images.count > 1 else { return }
if (index == 0) {
previousImageView.image = images[images.count - 1]
nextImageView.image = images[index + 1]
}
if (index == (images.count - 1)) {
previousImageView.image = images[index - 1]
nextImageView.image = images[0]
}
}
private func setupSwipeRecognizer() {
guard images.count > 1 else { return }
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
leftSwipe.direction = .left
rightSwipe.direction = .right
self.addGestureRecognizer(leftSwipe)
self.addGestureRecognizer(rightSwipe)
}
#objc private func handleSwipes(_ sender: UISwipeGestureRecognizer) {
if (sender.direction == .left) {
showNextImage()
}
if (sender.direction == .right) {
showPreviousImage()
}
}
private func showPreviousImage() {
previousImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.nextImageView = self.currentImageView
self.currentImageView = self.previousImageView
self.previousImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == 0 ? self.images.count - 1 : self.index - 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.previousImageView.image = self.index == 0 ? self.images[self.images.count - 1] : self.images[self.index - 1]
self.setupLayout()
})
}
private func showNextImage() {
nextImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = -screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.previousImageView = self.currentImageView
self.currentImageView = self.nextImageView
self.nextImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == (self.images.count - 1) ? 0 : self.index + 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.nextImageView.image = self.index == (self.images.count - 1) ? self.images[0] : self.images[self.index + 1]
self.setupLayout()
})
}
func imageView(image: UIImage? = nil, contentMode: UIImageView.ContentMode) -> UIImageView {
let view = UIImageView()
view.image = image
view.contentMode = contentMode
view.translatesAutoresizingMaskIntoConstraints = false
return view
}
}
// ImageCarouselViewDelegate
import UIKit
protocol ImageCarouselViewDelegate: NSObjectProtocol {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int)
}
// Usage
let slideshowView = ImageCarouselView(images) // initialize
self.slideshowView.delegate = self // set delegate in viewDidLoad()
extension YourViewController: ImageCarouselViewDelegate {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int) {
// do something with index
}
}
You can add collection view, add image in your custom collectionview cell, after that do checked Paging Enabled on props panel for collectionview. Use timer for auto slide

Resources