The basic idea is to use bezier paths to outline the precise shape you're looking for. You can then use gesture to change that shape and use display link to animate the returning of the stretched circle back to its circular form:
@IBDesignable
class RefreshView: UIView {
private let shapeLayer = CAShapeLayer()
@IBInspectable
var fillColor: UIColor = UIColor.lightGrayColor() {
didSet {
shapeLayer.fillColor = fillColor.CGColor
}
}
@IBInspectable
var strokeColor: UIColor = UIColor.clearColor() {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
@IBInspectable
var lineWidth: CGFloat = 0.0 {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
/// Center of main circle is in center top
private var pullDownCenter: CGPoint {
return CGPoint(x: bounds.size.width / 2.0, y: bounds.size.width / 2.0)
}
/// Radius of view spans width of view
private var radius: CGFloat {
return bounds.size.width / 2.0
}
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
updatePath()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureView()
}
/// Update the path, add shape layer, and add gesture recognizer
private func configureView() {
shapeLayer.fillColor = fillColor.CGColor
shapeLayer.strokeColor = strokeColor.CGColor
shapeLayer.lineWidth = lineWidth
updatePath()
layer.addSublayer(shapeLayer)
let pan = UIPanGestureRecognizer(target: self, action: #selector(RefreshView.handlePan(_:)))
addGestureRecognizer(pan)
}
/// Update path
private func updatePath() {
shapeLayer.path = stretchyCirclePathWithCenter(pullDownCenter, radius: radius, yOffset: yOffset).CGPath
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
yOffset = yOffsetMax
}
// MARK: Gesture Recognizer
private var yOffset: CGFloat = 0.0 { didSet { updatePath() } }
private var yOffsetMax: CGFloat { return bounds.size.width * 1.5 }
private var yOldOffset: CGFloat = 0.0
func handlePan(gesture: UIPanGestureRecognizer) {
if gesture.state == .Began {
yOldOffset = yOffset
} else if gesture.state == .Changed {
yOffset = yOldOffset + max(0, min(gesture.translationInView(gesture.view).y, yOffsetMax))
} else if gesture.state == .Ended || gesture.state == .Cancelled {
animateBackToCircle()
}
}
// MARK: Animation
private var displayLink: CADisplayLink?
private var duration: CGFloat?
private var startTime: CFAbsoluteTime?
private var originalOffset: CGFloat?
private func animateBackToCircle() {
displayLink = CADisplayLink(target: self, selector: #selector(RefreshView.handleDisplayLink(_:)))
duration = 0.5
originalOffset = yOffset
startTime = CFAbsoluteTimeGetCurrent()
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let percent = CGFloat(CFAbsoluteTimeGetCurrent() - startTime!) / duration!
if percent < 1.0 {
yOffset = originalOffset! * (1.0 - sin(percent * CGFloat(M_PI_2)))
} else {
self.displayLink?.invalidate()
self.displayLink = nil
updatePath()
}
}
// MARK: Stretch circle path
private func stretchyCirclePathWithCenter(center: CGPoint, radius: CGFloat, yOffset: CGFloat = 0.0) -> UIBezierPath {
func pointWithCenter(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
}
if yOffset == 0 {
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2.0 * CGFloat(M_PI), clockwise: true)
}
let lowerRadius = radius * (1 - yOffset / yOffsetMax * 0.5)
let yOffsetTop = yOffset / 4
let yOffsetBottom = yOffset / 1.5
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(M_PI), endAngle: 0, clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x + lowerRadius, y:center.y + yOffset), controlPoint1: CGPoint(x: center.x + radius, y:center.y + yOffsetTop), controlPoint2: CGPoint(x: center.x + lowerRadius, y:center.y + yOffset - yOffsetBottom))
path.addArcWithCenter(CGPoint(x: center.x, y:center.y + yOffset), radius: lowerRadius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x - radius, y:center.y), controlPoint1: CGPoint(x: center.x - lowerRadius, y:center.y + yOffset - yOffsetBottom), controlPoint2: CGPoint(x: center.x - radius, y:center.y + yOffsetTop))
return path
}
}
This renders something like:

Clearly, feel free to play around with the path as you see fit, add additional flourishes like a spinning arrow or whatever, etc. But this illustrates the basics of constructing bezier path, stretching it with gesture, and animating it back to its circular shape.