How to make waved progress bar in swift? - ios

I am beginner IOS developer and often see in design patterns that people use Heart Beat or Waves as the progression bar (i.e. song progress). Sometimes these progress bars go around theAlbum Art etc.
How can i achieve such thing? I am aware of UISlider combined with AVAudioPlayer but couldn't find anything to achieve such as following for song slider.

You could make a custom View, and draw the vertical lines manually. The main procedure for reference:
import UIKit
class WavedProgressView: UIView {
var lineMargin:CGFloat = 2.0
var volumes:[CGFloat] = [0.5,0.3,0.2,0.6,0.4,0.5,0.8,0.6,0.4]
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.darkGray
}
override var frame: CGRect {
didSet{
self.drawVerticalLines()
}
}
var lineWidth:CGFloat = 3.0{
didSet{
self.drawVerticalLines()
}
}
func drawVerticalLines() {
let linePath = CGMutablePath()
for i in 0..<self.volumes.count {
let height = self.frame.height * volumes[i]
let y = (self.frame.height - height) / 2.0
linePath.addRect(CGRect(x: lineMargin + (lineMargin + lineWidth) * CGFloat(i), y: y, width: lineWidth, height: height))
}
let lineLayer = CAShapeLayer()
lineLayer.path = linePath
lineLayer.lineWidth = 0.5
lineLayer.strokeColor = UIColor.white.cgColor
lineLayer.fillColor = UIColor.white.cgColor
self.layer.sublayers?.removeAll()
self.layer.addSublayer(lineLayer)
}
}
The effect is:
And please make it more perfect by yourself, like: handling event, applying default and highlighted color etc.

Related

issue adding layer around a circular view's border

I am adding a circular layer around my circular view and setting its position to be the center of the view but the layer gets added at a different position. The following is the code.
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerFrame = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.frame = outerLayerFrame
let path = UIBezierPath(ovalIn: outerLayerFrame)
outerLayer.path = path.cgPath
outerLayer.position = self.center
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}
Can anyone please tell me what is wrong here.
You want to set your layer(s) properties when the view is instantiated / initialized, but its size can (and usually does) change between then and when auto-layout adjusts it to the current device dimensions.
Unlike the UIView itself, layers do not auto-resize.
So, you want to set framing and layer paths in layoutSubviews():
class CustomView: UIView {
let outerLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// layoutSubviews is where you want to set your size
// and corner radius values
override func layoutSubviews() {
super.layoutSubviews()
// make self "round"
self.layer.cornerRadius = self.bounds.height/2
// add a round path for outer layer
let path = UIBezierPath(ovalIn: bounds)
outerLayer.path = path.cgPath
}
private func configure() {
self.backgroundColor = UIColor.systemBlue
// setup layer properties here
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
// add the sublayer here
self.layer.addSublayer(outerLayer)
}
}
Result (with the view set to 120 x 120 pts):
So, I realized, it's the position that is making the layer not aligned in center.
Here is the solution, I figured-
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerBounds = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.bounds = outerLayerBounds // should be bounds not frame
let path = UIBezierPath(ovalIn: outerLayerBounds)
outerLayer.path = path.cgPath
outerLayer.position = CGPoint(x: outerLayerBounds.midX , y: outerLayerBounds.midY) //self.center doesn't center the layer, had to calculate the center of the layer manually
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}

Swift callback function for draw()?

I have a customView where I overwrite drawRect:. I also have some cases, where I want to display a UILabel ontop of my customView:
if self.ticks.count < 50 {
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.addSubview(label)
}
However if I call this code before the drawRect is called, then my label doesn't show up. Is there a callback method like viewDidDraw or something?
EDIT
Someone asked for it, so here is all the code that is now doing what I need it to do. I think the answer to the question is to override layoutSubviews.
#IBDesignable open class CustomView: UIView {
#objc open var ticks:[Tick] = []
let label = UILabel()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
required public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup(){
self.backgroundColor = UIColor.clear
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.label.numberOfLines = 0
self.label.textAlignment = .center
self.addSubview(label)
}
override open func layoutSubviews() {
self.label.frame = self.bounds
}
#objc open func setSessions(sessions:[Session]){
self.ticks = []
for session in sessions.reversed(){
if self.ticks.count < 250{
self.ticks = self.ticks + (session.getShots() as! [Shot])
}else{
break
}
}
if self.ticks.count < 50 {
self.label.isHidden = false
}else{
self.label.isHidden = true
}
self.bringSubview(toFront: self.label)
}
override open func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let width = rect.width - 10;
let height = rect.height - 10;
let center = CGPoint(x: width/2+5, y: height/2+5)
let big_r = min(width, height)/2
context.setLineWidth(1.5)
UIColor.white.setStroke()
//draw the rings
for i in stride(from: 1, to: 7, by: 1){
let r = big_r * CGFloat(i) / 6.0;
context.addArc(center:center, radius:r, startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.strokePath()
}
//draw the ticks
if self.ticks.count > 49 {
UIColor.red().setFill()
for (i, tick) in self.ticks.prefix(250).enumerated(){
let radius = min((100.0-CGFloat(shot.score))/30.0 * big_r, big_r);
let x = Utilities.calculateX(tick, radius)
let y = Utilities.calculateY(tick, radius)
let point = CGPoint(x: x+center.x,y: y+center.y)
context.addArc(center:point, radius:CGFloat(DOT_WIDTH/2), startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.fillPath()
}
}
}
}
You can make your custom view redraw by calling setNeedsDisplay() when needed. After that, you can add the label
You can set a delegation for the View class and call the delegate method at the end of the draw function. However, it seems that if you call that directly, the draw function will only end until the delegate function get processed. Which means that the code will be executed before the new drawing refresh on the screen. In that case, you may need to call it asynchronously.
// at end of draw function
DispatchQueue.main.async {
self.delegate?.drawEnd()
}

How to display progress of AVPlayer playback of audio

I'm building a music app that will have a feed of posts that contain url to music files. When a cell in UITableView becomes fully visible playback of a corresponding music track starts.
This is a layout I have now.
Waveform is being generated by a server and I receive its data as an array of type [Float].
Complete waveform is a UIView that has a bar subviews with a height of a corresponding item from array from a server.
Here is a WaveformPlot Source:
class WaveformPlot: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//MARK: Populate plot with data from server response
func populateWithData(from dataSet: [Float]){
DispatchQueue.main.async {
var offset: CGFloat = 0
for index in 0..<dataSet.count {
let view = UIView(frame: CGRect(x: offset,
y: self.frame.height / 2,
width: self.frame.width / CGFloat(dataSet.count),
height: -(CGFloat)((dataSet[index]) / 3)))
let view2 = UIView(frame: CGRect(x: offset,
y: self.frame.height / 2,
width: self.frame.width / CGFloat(dataSet.count),
height: (CGFloat)((dataSet[index]) / 3)))
//Upper row of bars adjust
view.backgroundColor = UIColor.orange
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 0.25
view.clipsToBounds = true
view.round(corners: [.topLeft, .topRight], radius: 2)
//Lower row of bars adjust
view2.backgroundColor = UIColor.orange
view2.layer.borderColor = UIColor.black.cgColor
view2.layer.borderWidth = 0.25
view2.round(corners: [.bottomLeft, .bottomRight], radius: 2)
view2.layer.opacity = 0.6
self.addSubview(view)
self.addSubview(view2)
offset += self.frame.width / CGFloat(dataSet.count)
}
//Create gradient
let gradientView = UIView(frame: CGRect(x: 0, y: self.frame.height / 2, width: self.frame.width, height: -self.frame.height / 2))
let gradientLayer = CAGradientLayer()
gradientLayer.frame = gradientView.bounds
gradientLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor]
gradientView.layer.insertSublayer(gradientLayer, at: 0)
gradientView.layer.opacity = 0.9
self.addSubview(gradientView)
}
}
func clearPlot(){
for item in self.subviews{
item.removeFromSuperview()
}
}
}
Source of a player I've implemented.
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var currentItemURL: String?
static var shared = AVPlayer()
static func playItem(musicTrack: MusicTrack) {
StreamMusicPlayer.shared = AVPlayer(url: musicTrack.trackURL)
StreamMusicPlayer.currentItemURL = musicTrack.trackURL.absoluteString
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
The problem is that it is required to show progress of a track currently playing in a corresponding cell waveform.
It has to be similar to this:
What approach should i choose? I'm out of ideas how to implement this.
Any help appreciated.
The method described by war4l is a way of showing the progress. But to actually get the progress you need to add a periodic time observer. Something like this.
let interval = CMTime(value: 1, timescale: 2)
StreamMusicPlayer.shared.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
let seconds = CMTimeGetSeconds(progressTime)
if let duration = self.StreamMusicPlayer.shared.currentItem?.duration {
let totalDurationInSeconds = CMTimeGetSeconds(duration)
// Now you have current time and the duration so can display this somehow
}
})

Subview of Custom UIView has wrong x frame position

I have a custom UIView like so:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var turquoiseCircle: AnimationCircleView!
var redCircle: AnimationCircleView!
var blueCircle: AnimationCircleView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
setupAnimation()
}
let initialWidthScaleFactor: CGFloat = 0.06
func setupAnimation() {
let circleWidth = frame.size.width*initialWidthScaleFactor
let yPos = frame.size.height/2 - circleWidth/2
turquoiseCircle = AnimationCircleView(frame: CGRect(x: 0, y: yPos, width: circleWidth, height: circleWidth))
turquoiseCircle.circleColor = UIColor(red: 137/255.0, green: 203/255.0, blue: 225/255.0, alpha: 1.0).cgColor
turquoiseCircle.backgroundColor = UIColor.lightGray
addSubview(turquoiseCircle)
redCircle = AnimationCircleView(frame: CGRect(x: 100, y: yPos, width: circleWidth, height: circleWidth))
addSubview(redCircle)
blueCircle = AnimationCircleView(frame: CGRect(x: 200, y: yPos, width: circleWidth, height: circleWidth))
blueCircle.circleColor = UIColor(red: 93/255.0, green: 165/255.0, blue: 213/255.0, alpha: 1.0).cgColor
addSubview(blueCircle)
}
}
I created a light blue colored UIView in interface builder and set the above view MainLogoAnimationView as its subclass. If I run the app I get this:
It seems that for the turquoiseCircle the frame hasn't been placed at x = 0 as I set it. Not sure what I am doing wrong here. Is it because it is placed before the MainLogoAnimationView is fully initialized so being placed incorrectly? I have tried setting the frames of the circles in layoutSubviews() and this also didn't make any difference. I seem to run into the problem a lot with views being misplaced and think I am missing something fundamental here. What am I doing wrong?
UPDATE:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var turquoiseCircle: AnimationCircleView!
var redCircle: AnimationCircleView!
var blueCircle: AnimationCircleView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
setupAnimation()
}
let initialWidthScaleFactor: CGFloat = 0.2
func setupAnimation() {
blueCircle = AnimationCircleView()
blueCircle.translatesAutoresizingMaskIntoConstraints = false
blueCircle.backgroundColor = UIColor.lightGray
blueCircle.circleColor = UIColor.blue.cgColor
addSubview(blueCircle)
redCircle = AnimationCircleView()
redCircle.translatesAutoresizingMaskIntoConstraints = false
redCircle.backgroundColor = UIColor.lightGray
addSubview(redCircle)
}
override func layoutSubviews() {
super.layoutSubviews()
let circleWidth = bounds.size.width*initialWidthScaleFactor
let yPos = frame.size.height/2 - circleWidth/2
blueCircle.frame = CGRect(x: 0, y: yPos, width: circleWidth, height: circleWidth)
redCircle.frame = CGRect(x: frame.size.width/2 - circleWidth/2, y: yPos, width: circleWidth, height: circleWidth)
}
}
and :
import UIKit
class AnimationCircleView: UIView {
var circleColor = UIColor(red: 226/255.0, green: 131/255.0, blue: 125/255.0, alpha: 1.0).cgColor {
didSet {
shapeLayer.fillColor = circleColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
let shapeLayer = CAShapeLayer()
func setup() {
let circlePath = UIBezierPath(ovalIn: self.bounds)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor
shapeLayer.frame = self.bounds
layer.addSublayer(shapeLayer)
}
}
It's definitely the case that the setting up in init will provide self.frame/self.bounds values for whatever the view's initialized values are and not the correct final values when laid out in the view hierarchy. Probably the best way to handle this is like you tried, to add the views in init, and use property to keep a reference to them (you already are doing this) and then set the frame in -layoutSubviews().
The reason it probably didn't work in -layoutSubviews() is for each view after creation you need to call .translatesAutoresizingMaskIntoConstraints = false. Otherwise with programmatically-created views the system will create constraints for the values when added as a subview, overriding any setting of frames. Try setting that and see how it goes.
EDIT: For resizing the CAShapeLayer, instead of adding as a sublayer (which would require manually resizing) since AnimationCirleView view is specialized the view can be backed by CAShapeLayer. See the answer at Overriding default layer type in UIView in swift

CAShapeLayer position incorrect

I'm having issues drawing a circle using CAShapeLayer. I have a custom view MainLogoAnimationView code as follows:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var redCircle: AnimationCircleView!
// MARK: Object Lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
let circleWidth = frame.size.width*0.25
let yPos = frame.size.height/2 - circleWidth/2
redCircle = AnimationCircleView(frame: CGRect(x: 10, y: yPos, width: circleWidth, height: circleWidth))
redCircle.backgroundColor = UIColor.yellow
addSubview(redCircle)
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
In interface builder I created a light blue UIView and selected MainLogoAnimationView as the subview.
The AnimationCircleView is like so:
import UIKit
class AnimationCircleView: UIView {
// MARK: Object lifecycle
var circleColor = UIColor.red.withAlphaComponent(0.5).cgColor {
didSet {
shapeLayer.fillColor = circleColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
let shapeLayer = CAShapeLayer()
func setup() {
let circlePath = UIBezierPath(ovalIn: frame)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor
shapeLayer.frame = self.frame
shapeLayer.backgroundColor = UIColor.gray.cgColor
layer.addSublayer(shapeLayer)
}
}
Screenshot:
Is seems the CAShapeLayer frame is incorrect and also the red circle is in the wrong position. It should be inside the yellow square. What am I doing wrong here? Any pointers would be really appreciated! Thanks!
UPDATE:
In your AnimationCircleView inside setup change
shapeLayer.frame = self.frame
to
shapeLayer.frame = self.bounds
This should ensure the gray colour is drawn inside the yellow rectangle.
Edit:
Similar to the answer above, changing
let circlePath = UIBezierPath(ovalIn: frame)
to
let circlePath = UIBezierPath(ovalIn: bounds)
will solve your problem and draw circle inside the gray area.
CAShapeLayer frame is calculated relative to the position of the view it is added on, soo setting it equal to the frame, adds the CAShapeLayer at a x and y position offset from the origin by the value of the x and y passed in self.frame.

Resources