UIView with custom shaped frame: Animation not behaving as expected - swift

I want to animate a drawer with a custom shape, however the expected popOut() animation appear incorrect.
What part did I missed to make it transition without a blink ?
Why is popIn() displaying ok but not popOut() ?
Edit:
I tried animating the layer with
//pop
self.layer.transform = CATransform3DMakeScale(1, 0, 1)
and
//unPop
let transform = CATransform3DConcat(
CATransform3DMakeScale(1, 0, 1),
CATransform3DMakeTranslation(0, 0 - self.frame.height / 2, 0))
self.layer.transform = transform
It looks great,but I would like the top of my frame to be fixed, and only animate it by making it grow from the bottom...
Initial Result:
Initial Code:
// Points reference
// 1-------5
// | |
// | |
// | |
// | |
// | |
// | |
// | |
// 2\ /4
// \ /
// \ /
// 3
class PowerUpDrawer : UIView {
var triangleHeight : CGFloat
var orignalHeight : CGFloat = 0
var orignalY : CGFloat = 0
var isPoped : Bool = true {
didSet {
if (isPoped) {
pop()
} else {
unPop()
}
}
}
//XXX
override init(frame: CGRect) {
triangleHeight = 25
super.init(frame: frame)
}
//XXX
required init(coder aDecoder: NSCoder) {
triangleHeight = 25
super.init(coder: aDecoder)
orignalHeight = frame.height
orignalY = frame.origin.y
}
override var frame: CGRect {
didSet {
let bezierpath = UIBezierPath()
bezierpath.moveToPoint(CGPoint(x: 0, y: 0)) // 1
bezierpath.addLineToPoint(CGPoint(x: 0, y: frame.height - triangleHeight)) // 2
bezierpath.addLineToPoint(CGPoint(x: frame.width / 2, y: frame.height))// 3
bezierpath.addLineToPoint(CGPoint(x: frame.width, y: frame.height - triangleHeight))// 4
bezierpath.addLineToPoint(CGPoint(x: frame.width, y: 0))// 5
let mask = CAShapeLayer()
mask.frame = bounds
mask.path = bezierpath.CGPath
self.layer.mask = mask
}
}
#IBAction func toggle() {
isPoped = !isPoped
}
func pop() {
UIView.animateWithDuration(1, delay: 0.5, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: nil, animations: { () -> Void in
self.frame = CGRectMake(self.frame.origin.x, self.orignalY, self.frame.width, self.orignalHeight)
}) { (Bool) -> Void in
};
}
func unPop() {
UIView.animateWithDuration(1, delay: 0.5, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: nil, animations: { () -> Void in
self.frame = CGRectMake(self.frame.origin.x, self.orignalY, self.frame.width, self.triangleHeight)
}) { (Bool) -> Void in
};
}
}

This was an interesting problem. Can you try this as your unPop() function. I am offsetting enough height to make it feel like the top end is staying still. You can try any scaleFactor.
func unPop() {
UIView.animateWithDuration(1, delay: 0.5, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: nil, animations: { () -> Void in
let scaleFactor:CGFloat = 0.25
let offset = -(self.frame.height * (1 - scaleFactor))/2
let transform = CATransform3DConcat(
CATransform3DMakeScale(1, scaleFactor, 1),
CATransform3DMakeTranslation(0, offset, 0))
self.layer.transform = transform
}) { (Bool) -> Void in
};
}

Related

Implement Like what's app story progress circle

I'm trying to do the below progress
Using MKMagneticProgress, I was able to apply the bottom space, but now I'm stuck on how to do the spaces in the circle, for example in this image we have the progress of 7 items out of 10.
What I want is to update the MKMagneticProgress code to add the spaces.
the code taken from MKMagneticProgress with my update (I added the total)
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 8.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: CGFloat = 1.0
#IBInspectable open var total: CGFloat {
set {
self._total = newValue
self.setProgress(progress: self.progress)
}
get {
return self._total
}
}
/// Returns the current progress.
#IBInspectable open private(set) var progress: CGFloat {
set {
progressShape?.strokeEnd = newValue
}
get {
return progressShape.strokeEnd
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 2.0
private var backgroundShape: CAShapeLayer!
private var progressShape: CAShapeLayer!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = 0.0
progressShape.strokeEnd = 0.1
layer.addSublayer(progressShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
public func setProgress(progress: CGFloat, animated: Bool = true) {
let actualProgress = progress/self._total
if actualProgress > 1.0 {
return
}
var start = progressShape.strokeEnd
if let presentationLayer = progressShape.presentation(){
if let count = progressShape.animationKeys()?.count, count > 0 {
start = presentationLayer.strokeEnd
}
}
let duration = abs(Double(progress - start)) * completeDuration
percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
progressShape.strokeEnd = actualProgress
if animated {
progressAnimation.fromValue = start
progressAnimation.toValue = actualProgress
progressAnimation.duration = duration
progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
}
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
progressShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
progressShape.path = pathForShape(rect: rect).cgPath
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
progressShape?.strokeColor = progressShapeColor.cgColor
progressShape?.lineWidth = lineWidth - inset
progressShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
switch orientation {
case .left:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}
My output:
Thanks.
I was able to figure out how to update the code to add multiple arcs here is the updated code
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 5.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: Int = 1
#IBInspectable open var total: Int {
set {
self._total = newValue
self.addProgressShapes()
}
get {
return self._total
}
}
/// Returns the current progress.
private var _progress: CGFloat = 0.0
#IBInspectable open private(set) var progress: CGFloat {
set {
self._progress = newValue
}
get {
return self._progress
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 1.0
private var backgroundShape: CAShapeLayer!
private var progressShapes: [CAShapeLayer]!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
private func addProgressShapes(){
self.progressShapes = []
let progressSize = 1.0
var size:CGFloat = CGFloat(progressSize / Double(self.total))
let padingPercent = 0.2
let pading: Double = padingPercent * Double(self.total)/10 * Double(progressSize / Double(self.total - 1))
size = size - CGFloat(pading)
print("")
print("size: \(size) | pading: \(pading)")
print("------------------------")
var start: CGFloat = 0.0
var end: CGFloat = size
print("start: \(start) | end: \(end) ")
print("------------------------")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
for _ in 1..<self._total {
start = CGFloat(end + CGFloat(pading))
end = CGFloat(Double(start + size))
print("------------------------")
print("start: \(start) | end: \(end) ")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
}
}
private func setProgressShapeFrame(){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.frame = bounds
let rect = rectForShape()
shape.path = pathForShape(rect: rect).cgPath
}
}
private func updateShapesWidth(){
guard self.progressShapes != nil else { return }
for i in 0..<self._total {
let shape = self.progressShapes[i]
let color = CGFloat(i) >= self.progress ? UIColor.white.withAlphaComponent(0.1).cgColor:progressShapeColor.cgColor
shape.strokeColor = color
shape.lineWidth = lineWidth - inset
shape.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
}
}
private func transformShapes(_ transform: CATransform3D){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.transform = transform
}
}
public func setProgress(progress: CGFloat, animated: Bool = true) {
self._progress = progress
// let actualProgress = progress/self._total
// if actualProgress > 1.0 {
// return
// }
//
// var start = progressShape.strokeEnd
// if let presentationLayer = progressShape.presentation(){
// if let count = progressShape.animationKeys()?.count, count > 0 {
// start = presentationLayer.strokeEnd
// }
// }
//
// let duration = abs(Double(progress - start)) * completeDuration
// percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
// progressShape.strokeEnd = actualProgress
//
// if animated {
// progressAnimation.fromValue = start
// progressAnimation.toValue = actualProgress
// progressAnimation.duration = duration
// progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
// }
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
setProgressShapeFrame()
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
self.updateShapesWidth()
switch orientation {
case .left:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}

Need help whenIntegrating complex UIView with SwiftUI failed

I tried to test a waving animated UIView that is runloop based on SwiftUI using ''UIViewRepresentable'' but it does not appear to be animating at all.
Using UIViewRepresentable Protocol to connect swiftui to UIView.
Swift UI Code:
import SwiftUI
struct WaveView: UIViewRepresentable {
func makeUIView(context: Context) -> WaveUIView {
WaveUIView(frame: .init(x: 0, y: 0, width: 300, height: 300))
}
func updateUIView(_ view: WaveUIView, context: Context) {
view.start()
}
}
struct WaveView_Previews: PreviewProvider {
static var previews: some View {
WaveView()
}
}
The "Waving" UIView that I tested working on UIViewController way of doing it.
import Foundation
import UIKit
class WaveUIView:UIView {
/// wave curvature (default: 1.5)
open var waveCurvature: CGFloat = 1.5
/// wave speed (default: 0.6)
open var waveSpeed: CGFloat = 0.6
/// wave height (default: 5)
open var waveHeight: CGFloat = 5
/// real wave color
open var realWaveColor: UIColor = UIColor.red {
didSet {
self.realWaveLayer.fillColor = self.realWaveColor.cgColor
}
}
/// mask wave color
open var maskWaveColor: UIColor = UIColor.red {
didSet {
self.maskWaveLayer.fillColor = self.maskWaveColor.cgColor
}
}
/// float over View
open var overView: UIView?
/// wave timmer
fileprivate var timer: CADisplayLink?
/// real aave
fileprivate var realWaveLayer :CAShapeLayer = CAShapeLayer()
/// mask wave
fileprivate var maskWaveLayer :CAShapeLayer = CAShapeLayer()
/// offset
fileprivate var offset :CGFloat = 0
fileprivate var _waveCurvature: CGFloat = 0
fileprivate var _waveSpeed: CGFloat = 0
fileprivate var _waveHeight: CGFloat = 0
fileprivate var _starting: Bool = false
fileprivate var _stoping: Bool = false
/**
Init view
- parameter frame: view frame
- returns: view
*/
override init(frame: CGRect) {
super.init(frame: frame)
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = 0
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
// test
self.backgroundColor = UIColor.blue
}
/**
Init view with wave color
- parameter frame: view frame
- parameter color: real wave color
- returns: view
*/
public convenience init(frame: CGRect, color:UIColor) {
self.init(frame: frame)
self.realWaveColor = color
self.maskWaveColor = color.withAlphaComponent(0.4)
realWaveLayer.fillColor = self.realWaveColor.cgColor
maskWaveLayer.fillColor = self.maskWaveColor.cgColor
self.layer.addSublayer(self.realWaveLayer)
self.layer.addSublayer(self.maskWaveLayer)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
Add over view
- parameter view: overview
*/
open func addOverView(_ view: UIView) {
overView = view
overView?.center = self.center
overView?.frame.origin.y = self.frame.height - (overView?.frame.height)!
self.addSubview(overView!)
}
/**
Start wave
*/
open func start() {
if !_starting {
_stop()
_starting = true
_stoping = false
_waveHeight = 0
_waveCurvature = 0
_waveSpeed = 0
timer = CADisplayLink(target: self, selector: #selector(wave))
timer?.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
}
/**
Stop wave
*/
open func _stop(){
if (timer != nil) {
timer?.invalidate()
timer = nil
}
}
open func stop(){
if !_stoping {
_starting = false
_stoping = true
}
}
/**
Wave animation
*/
#objc func wave() {
// when view is not visible
// if overView?.window == nil {
// print("not playing cause not visible")
// return
// }
if _starting {
print("started")
if _waveHeight < waveHeight {
_waveHeight = _waveHeight + waveHeight/100.0
var frame = self.bounds
frame.origin.y = frame.size.height-_waveHeight
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature + waveCurvature / 100.0
_waveSpeed = _waveSpeed + waveSpeed / 100.0
} else {
_starting = false
}
}
if _stoping {
if _waveHeight > 0 {
_waveHeight = _waveHeight - waveHeight/50.0
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature - waveCurvature / 50.0
_waveSpeed = _waveSpeed - waveSpeed / 50.0
} else {
_stoping = false
_stop()
}
}
offset += _waveSpeed
let width = frame.width
let height = CGFloat(_waveHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: height))
var y: CGFloat = 0
let maskpath = CGMutablePath()
maskpath.move(to: CGPoint(x: 0, y: height))
let offset_f = Float(offset * 0.045)
let waveCurvature_f = Float(0.01 * _waveCurvature)
for x in 0...Int(width) {
y = height * CGFloat(sinf( waveCurvature_f * Float(x) + offset_f))
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
maskpath.addLine(to: CGPoint(x: CGFloat(x), y: -y))
}
if (overView != nil) {
let centX = self.bounds.size.width/2
let centY = height * CGFloat(sinf(waveCurvature_f * Float(centX) + offset_f))
let center = CGPoint(x: centX , y: centY + self.bounds.size.height - overView!.bounds.size.height/2 - _waveHeight - 1 )
overView?.center = center
}
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.closeSubpath()
self.realWaveLayer.path = path
maskpath.addLine(to: CGPoint(x: width, y: height))
maskpath.addLine(to: CGPoint(x: 0, y: height))
maskpath.closeSubpath()
self.maskWaveLayer.path = maskpath
}
}
I expect the SwiftUI to have the view animating and correctly have the frame/border changes according animation. But it is not animating at all right now.
Following is the animated view with UIViewController:
override func viewWillAppear(_ animated: Bool) {
cardView.start()
}
func viewdidload(){
let frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width * 0.8, height: view.bounds.height * 0.5)
cardView = HomeCardView(frame: frame, color: .gray)
cardView.addOverView(someUIView())
cardView.realWaveColor = UIColor.white.withAlphaComponent(0.7)
cardView.maskWaveColor = UIColor.white.withAlphaComponent(0.3)
cardView.waveSpeed = 1.2
cardView.waveHeight = 10
view.addSubview(cardView)
}
You forget to addOverview in update uimethod
func updateUIView(_ view: WaveUIView, context: Context) {
let overView: UIView = UIView(frame: CGRect.init(x: 0, y: 0, width: 300, height: 300))
overView.backgroundColor = UIColor.green
view.addOverView(overView)
view.start()
}

Swift animation chaining using CGAffineTransforms

I'm trying to create a background animation for my app. What I'm trying to achieve for part of it is:
1) An image (random colour / shape chosen from an array) appears randomly on the view. 2) It then grows in size rapidly (from not visible, to visible). 3) Then will slowly move in a given direction, while rotating, for 10ish seconds. 4) Then it will scale back down to a non-visible size and be removed from the view.
What I'm finding is that the shape will appear correctly in steps 1 and 2. Then, the shape will jump to a random position on the screen at the start of animation/step 3 (transition3 in the below code). While it moves, it also decreases in size. Then for step 4 it jumps back to it's original position in step 1 & 2, before shrinking off the screen as intended.
I cannot for the life of me work out what's going on here. I'm hoping I haven't missed something embarrassingly obvious.
Thanks in advance for any help!
class BackgroundAnimation {
func animation(animationView: UIView) {
let colourArray = [
UIColor(red:0.99, green:0.80, blue:0.05, alpha:1.0),
UIColor(red:0.06, green:0.22, blue:0.29, alpha:1.0),
UIColor(red:0.95, green:0.18, blue:0.30, alpha:1.0),
UIColor(red:0.35, green:0.77, blue:0.92, alpha:1.0),
UIColor(red:0.95, green:0.61, blue:0.19, alpha:1.0)
]
let imageArray = [
"Cross",
"Circle",
"Halfmoon",
"Square",
"Triangle"
]
//Animation constants
let initialDimensions = 10
let pathLength: CGFloat = 100
let pathDuration = 10
let scaleFactor: CGFloat = 5
let scaleDuration = 1
//Select the random image and random colour that is to be animated.
let image = UIImage(named: imageArray[Int.random(in: 0...imageArray.count - 1)])
let imageView = UIImageView(image: image)
imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = colourArray[Int.random(in: 0...colourArray.count - 1)]
imageView.contentMode = .scaleAspectFit
imageView.frame = CGRect(x: 0, y: 0, width: initialDimensions, height: initialDimensions)
animationView.insertSubview(imageView, at: 0)
//create a random start location and angle of direction
let startPointX = CGFloat.random(in: 0...animationView.frame.width)
let startPointY = CGFloat.random(in: 0...animationView.frame.height)
let pathAngle = CGFloat.random(in: 0...(CGFloat.pi * 2))
imageView.center = CGPoint(x: startPointX, y: startPointY)
//Calculate the endpoint from path angle and length
let translationX = pathLength * sin(pathAngle)
let tanslationY = -(pathLength * cos(pathAngle))
//Define the transitions for the aniamtion
var transition1 = CGAffineTransform.identity
transition1 = transition1.scaledBy(x: scaleFactor, y: scaleFactor)
var transition2 = CGAffineTransform.identity
transition2 = transition2.translatedBy(x: translationX, y: tanslationY)
transition2 = transition2.rotated(by: CGFloat.random(in: CGFloat.pi * 1/4 ... CGFloat.pi * 3/4))
var transition3 = CGAffineTransform.identity
transition3 = transition3.scaledBy(x: 0.001, y: 0.001)
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition1
}, completion: {finished in
UIView.animate(withDuration: TimeInterval(pathDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition2
}, completion: { finished in
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition3
}, completion: { finished in
imageView.removeFromSuperview()
})
})
})
}
}
change your position with imageView.center OR imageView.frame.origin
imageView.frame.origin = CGPoint(x: translationX, y: tanslationY)
but you should calculate the destination, then set frame.origin to the destination
In ref to Ehsan's helpful response:
This has fixed the movement issue, but the imageView is getting smaller when I try to apply the rotation in the second animation block. Updated code below:
class BackgroundAnimation {
func animation(animationView: UIView) {
let colourArray = [
UIColor(red:0.99, green:0.80, blue:0.05, alpha:1.0),
UIColor(red:0.06, green:0.22, blue:0.29, alpha:1.0),
UIColor(red:0.95, green:0.18, blue:0.30, alpha:1.0),
UIColor(red:0.35, green:0.77, blue:0.92, alpha:1.0),
UIColor(red:0.95, green:0.61, blue:0.19, alpha:1.0)
]
let imageArray = [
"Cross",
"Circle",
"Halfmoon",
"Square",
"Triangle"
]
//Animation constants
let initialDimensions = 10
let pathLength: CGFloat = 100
let pathDuration = 10
let scaleFactor: CGFloat = 3
let scaleDuration = 1
//Select the random image and random colour that is to be animated.
let image = UIImage(named: imageArray[Int.random(in: 0...imageArray.count - 1)])
let imageView = UIImageView(image: image)
imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = colourArray[Int.random(in: 0...colourArray.count - 1)]
imageView.contentMode = .scaleAspectFit
imageView.frame = CGRect(x: 0, y: 0, width: initialDimensions, height: initialDimensions)
animationView.insertSubview(imageView, at: 0)
//create a random start location and angle of direction
let startPointX = CGFloat.random(in: 0...animationView.frame.width * 0.9)
let startPointY = CGFloat.random(in: 0...animationView.frame.height * 0.9)
let pathAngle = CGFloat.random(in: 0...(CGFloat.pi * 2))
imageView.center = CGPoint(x: startPointX, y: startPointY)
//Calculate the endpoint from path angle and length
let translationX = pathLength * sin(pathAngle)
let translationY = (pathLength * cos(pathAngle))
let endPointX = startPointX + translationX
let endPointY = startPointY + translationY
//Define the transitions for the aniamtion
var transition1 = CGAffineTransform.identity
transition1 = transition1.scaledBy(x: scaleFactor, y: scaleFactor)
var transition2 = CGAffineTransform.identity
transition2 = transition2.rotated(by: CGFloat.random(in: CGFloat.pi * 1/4 ... CGFloat.pi * 3/4))
var transition3 = CGAffineTransform.identity
transition3 = transition3.scaledBy(x: 0.001, y: 0.001)
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition1
}, completion: {finished in
UIView.animate(withDuration: TimeInterval(pathDuration), delay: 0.5, options: [.beginFromCurrentState, .curveLinear], animations: {
imageView.frame.origin = CGPoint(x: endPointX, y: endPointY)
imageView.transform = transition2
}, completion: { finished in
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0.5, options: .beginFromCurrentState, animations: {
imageView.transform = transition3
}, completion: { finished in
imageView.removeFromSuperview()
})
})
})
}
}

View don't appear the second time [Swift]

In my swift app I've create these two functions:
func presentAlertView() {
backgroundLogOutView.frame = UIScreen.main.bounds
backgroundLogOutView.backgroundColor = currentTheme.backgroundColor.withAlphaComponent(0.5)
logOutAlertView.frame.size = CGSize(width: 200, height: 200)
logOutAlertView.center = CGPoint(x: view.frame.width / 2, y: view.frame.height / 2 - (navigationController?.navigationBar.frame.size.height)!)
view.addSubview(backgroundLogOutView)
backgroundLogOutView.addSubview(logOutAlertView)
logOutAlertView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
logOutAlertView.alpha = 0
UIView.animate(withDuration: 0.4, animations: {
self.logOutAlertView.alpha = 1
self.logOutAlertView.transform = CGAffineTransform.identity
})
}
#objc func removeAlertView() {
UIView.animate(withDuration: 0.4, animations: {
self.logOutAlertView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.logOutAlertView.alpha = 0
self.backgroundLogOutView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.backgroundLogOutView.alpha = 0
}) {(success: Bool) in
self.logOutAlertView.removeFromSuperview()
self.backgroundLogOutView.removeFromSuperview()
self.tableView.deselectRow(at: [0,4], animated: true)
}
}
By tapping a button the function presentAlertView() is invoked and the views appear then when the function removeAlertView() is invoked the views disapeear. And here all ok.
But when I tapped the button the second time the views don't appear!
Why this happened?
Thank you
self.backgroundLogOutView.alpha = 0
is present in removeAlertView().
self.backgroundLogOutView.alpha = 1
may be missing in presentAlertView().

Why doesn't UIView.animateWithDuration affect this custom view?

I designed a custom header view that masks an image and draws a border on the bottom edge, which is an arc. It looks like this:
Here's the code for the class:
class HeaderView: UIView
{
private let imageView = UIImageView()
private let dimmerView = UIView()
private let arcShape = CAShapeLayer()
private let maskShape = CAShapeLayer() // Masks the image and the dimmer
private let titleLabel = UILabel()
#IBInspectable var image: UIImage? { didSet { self.imageView.image = self.image } }
#IBInspectable var title: String? { didSet {self.titleLabel.text = self.title} }
#IBInspectable var arcHeight: CGFloat? { didSet {self.setupLayers()} }
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
initMyStuff()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
initMyStuff()
}
override func prepareForInterfaceBuilder()
{
backgroundColor = UIColor.clear()
}
internal func initMyStuff()
{
backgroundColor = UIColor.clear()
titleLabel.font = Font.AvenirNext_Bold(24)
titleLabel.text = "TITLE"
titleLabel.textColor = UIColor.white()
titleLabel.layer.shadowColor = UIColor.black().cgColor
titleLabel.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
titleLabel.layer.shadowRadius = 0.0;
titleLabel.layer.shadowOpacity = 1.0;
titleLabel.layer.masksToBounds = false
titleLabel.layer.shouldRasterize = true
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
dimmerView.frame = self.bounds
dimmerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
addSubview(dimmerView)
addSubview(titleLabel)
// Add the shapes
self.layer.addSublayer(arcShape)
self.layer.addSublayer(maskShape)
self.layer.masksToBounds = true // This seems to be unneeded...test more
// Set constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView .autoPinEdgesToSuperviewEdges()
titleLabel.autoCenterInSuperview()
}
func setupLayers()
{
let aHeight = arcHeight ?? 10
// Create the arc shape
arcShape.path = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight)
arcShape.strokeColor = UIColor.white().cgColor
arcShape.lineWidth = 1.0
arcShape.fillColor = UIColor.clear().cgColor
// Create the mask shape
let maskPath = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight, closed: true)
maskPath.moveTo(nil, x: bounds.size.width, y: bounds.size.height)
maskPath.addLineTo(nil, x: bounds.size.width, y: 0)
maskPath.addLineTo(nil, x: 0, y: 0)
maskPath.addLineTo(nil, x: 0, y: bounds.size.height)
//let current = CGPathGetCurrentPoint(maskPath);
//print(current)
let mask_Dimmer = CAShapeLayer()
mask_Dimmer.path = maskPath.copy()
maskShape.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
maskShape.path = maskPath
// Apply the masks
imageView.layer.mask = maskShape
dimmerView.layer.mask = mask_Dimmer
}
override func layoutSubviews()
{
super.layoutSubviews()
// Let's go old school here...
imageView.frame = self.bounds
dimmerView.frame = self.bounds
setupLayers()
}
}
Something like this will cause it to just snap to the new size without gradually changing its frame:
UIView.animate(withDuration: 1.0)
{
self.headerView.arcHeight = self.new_headerView_arcHeight
self.headerView.frame = self.new_headerView_frame
}
I figure it must have something to do with the fact that I'm using CALayers, but I don't really know enough about what's going on behind the scenes.
EDIT:
Here's the function I use to create the arc path:
class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
// let startAngle = radians(180) + angle;
// let endAngle = radians(360) - angle;
let path = CGMutablePath();
path.addArc(nil, x: arcCenter.x, y: arcCenter.y, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false);
if(closed == true)
{path.addLineTo(nil, x: startPoint.x, y: startPoint.y);}
return path;
}
BONUS:
Setting the arcHeight property to 0 results in no white line being drawn. Why?
The Path property can't be animated. You have to approach the problem differently. You can draw an arc 'instantly', any arc, so that tells us that we need to handle the animation manually. If you expect the entire draw process to take say 3 seconds, then you might want to split the process to 1000 parts, and call the arc drawing function 1000 times every 0.3 miliseconds to draw the arc again from the beginning to the current point.
self.headerView.arcHeight is not a animatable property. It is only UIView own properties are animatable
you can do something like this
let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode
let expectedFramesPerSecond = 60
var diff : CGFloat = 0
func update() {
let diffUpdated = self.headerView.arcHeight - self.new_headerView_arcHeight
let done = (fabs(diffUpdated) < 0.1)
if(!done){
self.headerView.arcHeight -= diffUpdated/(expectedFramesPerSecond*0.5)
self.setNeedsDisplay()
}
}