How to display progress of AVPlayer playback of audio - ios

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

Related

Programmatically created UIView's frame is 0

Cannot access uiview's frame after setting it's layout. Frame, and center is given as 0,0 points.
I should also mention that there are no storyboard's in this project. All views and everything are created programmatically.
I have created a UIView resultView programmatically and added it as a subview in scrollView, which is also added as a subview of view, then set it's constraints, anchors in a method called setupLayout() I call setupLayout() in viewDidLoad() and after that method, I call another method called configureShapeLayer(). Inside configureShapeLayer() I try to access my view's center as:
let center = resultView.center // should give resultView's center but gives 0
Then by using this center value I try to add two bezierPaths to have a status bar kind of view. But since resultView's center is not updated at that point, it appears as misplaced. Please see the pic below:
I also tried calling setupLayout() in loadView() then calling configureShapeLayer() in viewDidLoad() but nothing changed.
So I need a way to make sure all views are set in my view, all constraints, and layouts are applied before calling configureShapeLayer(). But how can I do this?
I also tried calling configureShapeLayer() in both viewWillLayoutSubviews() and viewDidLayoutSubviews() methods but it made it worse, and didnt work either.
Whole View Controller File is given below: First views are declared, then they are added into the view in prepareUI(), at the end of prepareUI(), another method setupLayout() is called. After it completes setting layout, as can be seen from viewDidLoad, finally configureShapeLayer() method is called.
import UIKit
class TryViewController: UIViewController {
let score: CGFloat = 70
lazy var percentage: CGFloat = {
return score / 100
}()
// MARK: View Declarations
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .white
return scrollView
}()
private let iconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let percentageLabel: UILabel = {
let label = UILabel()
label.text = ""
label.textAlignment = .center
label.font = TextStyle.systemFont(ofSize: 50.0)
return label
}()
// This one is the one should have status bar at center.
private let resultView: UIView = {
let view = UIView()
view.backgroundColor = .purple
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
prepareUI()
configureShapeLayer()
}
private func prepareUI() {
resultView.addSubviews(views: percentageLabel)
scrollView.addSubviews(views: iconImageView,
resultView)
view.addSubviews(views: scrollView)
setupLayout()
}
private func setupLayout() {
scrollView.fillSuperview()
iconImageView.anchor(top: scrollView.topAnchor,
padding: .init(topPadding: 26.0))
iconImageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.31).isActive = true
iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 0.67).isActive = true
iconImageView.anchorCenterXToSuperview()
//percentageLabel.frame = CGRect(x: 0, y: 0, width: 105, height: 60)
//percentageLabel.center = resultView.center
percentageLabel.anchorCenterXToSuperview()
percentageLabel.anchorCenterYToSuperview()
let resultViewTopConstraintRatio: CGFloat = 0.104
resultView.anchor(top: iconImageView.bottomAnchor,
padding: .init(topPadding: (view.frame.height * resultViewTopConstraintRatio)))
resultView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.533).isActive = true
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor, multiplier: 1.0).isActive = true
resultView.anchorCenterXToSuperview()
configureShapeLayer()
}
private func configureShapeLayer() {
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
let center = resultView.center // should give resultView's center but gives 0
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
resultView.layer.addSublayer(trackLayer)
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
scoreLayer.lineCap = .round
scoreLayer.strokeEnd = 0
resultView.layer.addSublayer(scoreLayer)
}
}
You will be much better off creating a custom view. That will allow you to "automatically" update your bezier paths when the view size changes.
It also allows you to keep your drawing code away from your controller code.
Here is a simple example. It adds a button above the "resultView" - each time it's tapped it will increment the percentage by 5 (percentage starts at 5 for demonstration):
//
// PCTViewController.swift
//
// Created by Don Mag on 12/6/18.
//
import UIKit
class MyResultView: UIView {
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
var percentage = CGFloat(0.0) {
didSet {
self.percentageLabel.text = "\(Int(percentage * 100))%"
self.setNeedsLayout()
}
}
let percentageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = ""
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 40.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(trackLayer)
layer.addSublayer(scoreLayer)
addSubview(percentageLabel)
NSLayoutConstraint.activate([
percentageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
override func layoutSubviews() {
super.layoutSubviews()
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
trackLayer.frame = self.bounds
scoreLayer.frame = self.bounds
let centerPoint = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
trackLayer.lineCap = kCALineCapRound
// trackLayer.lineCap = .round
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
scoreLayer.lineCap = kCALineCapRound
// scoreLayer.lineCap = .round
}
}
class PCTViewController: UIViewController {
let resultView: MyResultView = {
let v = MyResultView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .purple
return v
}()
let btn: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Add 5 percent", for: .normal)
b.backgroundColor = .blue
return b
}()
var pct = 5
#objc func incrementPercent(_ sender: Any) {
pct += 5
resultView.percentage = CGFloat(pct) / 100.0
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(btn)
view.addSubview(resultView)
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.widthAnchor.constraint(equalToConstant: 300.0),
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor),
resultView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
btn.addTarget(self, action: #selector(incrementPercent), for: .touchUpInside)
resultView.percentage = CGFloat(pct) / 100.0
}
}
Result:
This is not the ideal fix, but try calling configureShapeLayer() func on main thread, like this:
DispatchQueue.main.async {
configureShapeLayer()
}
I had problem like that once and was something like that.

Running CAEmitterLayer only once

I want to run CAEmitterLayer only once, I was thinking of stopping birthRate but I can't do it. What I want is for it to run only once when tapping on the screen. I've been trying with delegates but I can't get it to work. And could you please tell me if my code is efficient.
import UIKit
import PlaygroundSupport
class Emitter {
static func get(with image: UIImage) -> CAEmitterLayer {
let emitter = CAEmitterLayer()
emitter.emitterShape = kCAEmitterLayerLine
emitter.emitterCells = generateEmitterCells(image: image)
print("emit")
emitter.setValue(0.0, forKey: "em")
return emitter
}
static func generateEmitterCells(image: UIImage) -> [CAEmitterCell] {
var cells = [CAEmitterCell]()
let cell = CAEmitterCell()
cell.contents = image.cgImage
cell.birthRate = 0.1
cell.lifetime = 20
cell.velocity = 250
cell.emissionRange = (10 * (.pi/180))
cell.scale = 0.9
cell.scaleRange = 0.3
cell.velocityRange = 100
cells.append(cell)
print("cells")
return cells
}
}
class ViewController : UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
let view2 = UIView(frame: self.view.frame)
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 580)
super.view.backgroundColor = UIColor.white
self.view.addSubview(view2)
}
#objc func handleTap() {
rain()
}
func rain() {
let emitter = Emitter.get(with: UIImage(named: "Group 1493")!)
emitter.emitterPosition = CGPoint(x: view.frame.width / 2, y: view.frame.height + 25)
emitter.emitterSize = CGSize(width: view.frame.width, height: 2)
self.view.layer.addSublayer(emitter)
}
}
let controller = ViewController()
PlaygroundPage.current.liveView = controller.view
You could set lifetime propertie to 0 after first splash
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
emitter.lifetime = 0
}
You need to do a few things.
You need to CAEmitterLayer's beginTime: emitter.beginTime = CACurrentMediaTime()
To stop new particles birth you need to set emitter.birthRate = 0. You can delay it using GCD to create this splash effect.

Why is my UIVisualEffectView show in viewHierarchy but not in my device?

I am facing an incomprehensible problem.
I have a login UIViewController and a ProgressLoading UIVisualEffectView.
I want to print the loading when I am making an API call and waiting for response.
Here is myProgressLoading Class
import UIKit
class ProgressLoading: UIVisualEffectView {
var text: String? {
didSet {
label.text = text
}
}
let activityIndictor: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
let label: UILabel = UILabel()
let blurEffect = UIBlurEffect(style: .light)
let vibrancyView: UIVisualEffectView
init(text: String) {
self.text = text
self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
self.vibrancyView.backgroundColor = UIColor(white: 0.2, alpha: 0.7)
super.init(effect: blurEffect)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
self.text = ""
self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
self.vibrancyView.backgroundColor = UIColor(white: 0.2, alpha: 0.7)
super.init(coder: aDecoder)
self.setup()
}
func setup() {
contentView.addSubview(vibrancyView)
contentView.addSubview(activityIndictor)
contentView.addSubview(label)
activityIndictor.startAnimating()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview = self.superview {
let width = superview.frame.size.width / 2.3
let height: CGFloat = 50.0
self.frame = CGRect(x: superview.frame.size.width / 2 - width / 2,
y: superview.frame.height / 2 - height / 2,
width: width,
height: height)
vibrancyView.frame = self.bounds
let activityIndicatorSize: CGFloat = 40
activityIndictor.frame = CGRect(x: 5,
y: height / 2 - activityIndicatorSize / 2,
width: activityIndicatorSize,
height: activityIndicatorSize)
activityIndictor.color = UIColor.white
layer.cornerRadius = 8.0
layer.masksToBounds = true
label.text = text
label.textAlignment = NSTextAlignment.center
label.frame = CGRect(x: activityIndicatorSize + 5,
y: 0,
width: width - activityIndicatorSize - 15,
height: height)
label.textColor = UIColor.white
label.font = UIFont.boldSystemFont(ofSize: 16)
}
}
func show() {
self.isHidden = false
}
func hide() {
self.isHidden = true
}
}
Here is how I work with my progressLoading, a show and hide methods and declare with a text.
override func viewDidLoad() {
super.viewDidLoad()
progressLoading = ProgressLoading(text: "Loggin in...")
progressLoading?.hide()
self.view.addSubview(progressLoading!)
}
func startAnimatingLoading(viewModel: Login.ViewModel) {
self.progressLoading?.show()
}
func stopAnimatingLoading(viewModel: Login.ViewModel) {
self.progressLoading?.hide()
}
My problem is that when I wait for the response, I show the loading programatically, but nothing appears in my device. (if you wonder, I simulate long API callback by just making a breakpoint and stay on the breakpoint)
I looked inside the viewHierarchy and the Loading is right here in front of everything exactly how I want it to be.
Here is my ViewHierarchy :
Is there something I didn't get with the viewHierarchy ?
How is this possible that something is shown on the view hierarchy but not inside my device ?
Thanks you for your help, I just don't get it !
Are you making the API call on the main thread? If you start the animation and then block the main thread the progress indicator won't appear - the system needs at least one draw cycle to actually display the indicator. Move your simulated API call to a background thread and then show your indicator before the call and hide it when API responds. Hope this helps!

How to make waved progress bar in swift?

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.

How to implement range slider in Swift

I'm trying to implement Range Slider and I used custom control called NMRangeSlider.
But when I use it, the slider doesn't appear at all. Could it be also because it's all written in Objective-C?
This is how I've currently implemented it:
var rangeSlider = NMRangeSlider(frame: CGRectMake(16, 6, 275, 34))
rangeSlider.lowerValue = 0.54
rangeSlider.upperValue = 0.94
self.view.addSubview(rangeSlider)
To create a custom Range Slider I found a good solution here: range finder tutorial iOS 8 but I needed this in swift 3 for my project. I updated this for Swift 3 iOS 10 here:
in your main view controller add this to viewDidLayOut to show a range slider.
override func viewDidLayoutSubviews() {
let margin: CGFloat = 20.0
let width = view.bounds.width - 2.0 * margin
rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length + 170, width: width, height: 31.0)
}
create the helper function to print slider output below viewDidLayoutSubviews()
func rangeSliderValueChanged() { //rangeSlider: RangeSlider
print("Range slider value changed: \(rangeSlider.lowerValue) \(rangeSlider.upperValue) ")//(\(rangeSlider.lowerValue) \(rangeSlider.upperValue))
}
Create the file RangeSlider.swift and add this to it:
import UIKit
import QuartzCore
class RangeSlider: UIControl {
var minimumValue = 0.0
var maximumValue = 1.0
var lowerValue = 0.2
var upperValue = 0.8
let trackLayer = RangeSliderTrackLayer()//= CALayer() defined in RangeSliderTrackLayer.swift
let lowerThumbLayer = RangeSliderThumbLayer()//CALayer()
let upperThumbLayer = RangeSliderThumbLayer()//CALayer()
var previousLocation = CGPoint()
var trackTintColor = UIColor(white: 0.9, alpha: 1.0)
var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
var thumbTintColor = UIColor.white
var curvaceousness : CGFloat = 1.0
var thumbWidth: CGFloat {
return CGFloat(bounds.height)
}
override init(frame: CGRect) {
super.init(frame: frame)
trackLayer.rangeSlider = self
trackLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(trackLayer)
lowerThumbLayer.rangeSlider = self
lowerThumbLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(lowerThumbLayer)
upperThumbLayer.rangeSlider = self
upperThumbLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(upperThumbLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func updateLayerFrames() {
trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height / 3)
trackLayer.setNeedsDisplay()
let lowerThumbCenter = CGFloat(positionForValue(value: lowerValue))
lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
lowerThumbLayer.setNeedsDisplay()
let upperThumbCenter = CGFloat(positionForValue(value: upperValue))
upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
upperThumbLayer.setNeedsDisplay()
}
func positionForValue(value: Double) -> Double {
return Double(bounds.width - thumbWidth) * (value - minimumValue) /
(maximumValue - minimumValue) + Double(thumbWidth / 2.0)
}
override var frame: CGRect {
didSet {
updateLayerFrames()
}
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
previousLocation = touch.location(in: self)
// Hit test the thumb layers
if lowerThumbLayer.frame.contains(previousLocation) {
lowerThumbLayer.highlighted = true
} else if upperThumbLayer.frame.contains(previousLocation) {
upperThumbLayer.highlighted = true
}
return lowerThumbLayer.highlighted || upperThumbLayer.highlighted
}
func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
return min(max(value, lowerValue), upperValue)
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
// 1. Determine by how much the user has dragged
let deltaLocation = Double(location.x - previousLocation.x)
let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - thumbWidth)
previousLocation = location
// 2. Update the values
if lowerThumbLayer.highlighted {
lowerValue += deltaValue
lowerValue = boundValue(value: lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
} else if upperThumbLayer.highlighted {
upperValue += deltaValue
upperValue = boundValue(value: upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
}
// 3. Update the UI
CATransaction.begin()
CATransaction.setDisableActions(true)
updateLayerFrames()
CATransaction.commit()
sendActions(for: .valueChanged)
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
lowerThumbLayer.highlighted = false
upperThumbLayer.highlighted = false
}
}
Next add the thumb layer subclass file RangeSliderThumbLayer.swift and add this to it:
import UIKit
class RangeSliderThumbLayer: CALayer {
var highlighted = false
weak var rangeSlider: RangeSlider?
override func draw(in ctx: CGContext) {
if let slider = rangeSlider {
let thumbFrame = bounds.insetBy(dx: 2.0, dy: 2.0)
let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)
// Fill - with a subtle shadow
let shadowColor = UIColor.gray
ctx.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 1.0, color: shadowColor.cgColor)
ctx.setFillColor(slider.thumbTintColor.cgColor)
ctx.addPath(thumbPath.cgPath)
ctx.fillPath()
// Outline
ctx.setStrokeColor(shadowColor.cgColor)
ctx.setLineWidth(0.5)
ctx.addPath(thumbPath.cgPath)
ctx.strokePath()
if highlighted {
ctx.setFillColor(UIColor(white: 0.0, alpha: 0.1).cgColor)
ctx.addPath(thumbPath.cgPath)
ctx.fillPath()
}
}
}
}
Finally add the track layer subclass file RangeSliderTrackLayer.swift and add the following to it:
import Foundation
import UIKit
import QuartzCore
class RangeSliderTrackLayer: CALayer {
weak var rangeSlider: RangeSlider?
override func draw(in ctx: CGContext) {
if let slider = rangeSlider {
// Clip
let cornerRadius = bounds.height * slider.curvaceousness / 2.0
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
ctx.addPath(path.cgPath)
// Fill the track
ctx.setFillColor(slider.trackTintColor.cgColor)
ctx.addPath(path.cgPath)
ctx.fillPath()
// Fill the highlighted range
ctx.setFillColor(slider.trackHighlightTintColor.cgColor)
let lowerValuePosition = CGFloat(slider.positionForValue(value: slider.lowerValue))
let upperValuePosition = CGFloat(slider.positionForValue(value: slider.upperValue))
let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
ctx.fill(rect)
}
}
}
Build Run and Get:
UPDATE:
It did not show to me, because it was all white. So the solution, without using any other framework and sticking with this one - you need to set all the views for all the components and then it will display well:
I have tried to import it in Swift as I used it before in Objective-C code, but without any luck. If I set everything properly and add it either in viewDidLoad() or viewDidAppear(), nothing gets displayed. One thing is worth mentioning, though - when I enter View Debug Hierarchy, the slider actually is there on the canvas:
But it's simply not rendered with all the colors that I did set before adding in it to the view. For the record - this is the code I used:
override func viewDidAppear(animated: Bool) {
var rangeSlider = NMRangeSlider(frame: CGRectMake(50, 50, 275, 34))
rangeSlider.lowerValue = 0.54
rangeSlider.upperValue = 0.94
let range = 10.0
let oneStep = 1.0 / range
let minRange: Float = 0.05
rangeSlider.minimumRange = minRange
let bgImage = UIView(frame: rangeSlider.frame)
bgImage.backgroundColor = .greenColor()
rangeSlider.trackImage = bgImage.pb_takeSnapshot()
let trackView = UIView(frame: CGRectMake(0, 0, rangeSlider.frame.size.width, 29))
trackView.backgroundColor = .whiteColor()
trackView.opaque = false
trackView.alpha = 0.3
rangeSlider.trackImage = UIImage(named: "")
let lowerThumb = UIView(frame: CGRectMake(0, 0, 8, 29))
lowerThumb.backgroundColor = .whiteColor()
let lowerThumbHigh = UIView(frame: CGRectMake(0, 0, 8, 29))
lowerThumbHigh.backgroundColor = UIColor.blueColor()
rangeSlider.lowerHandleImageNormal = lowerThumb.pb_takeSnapshot()
rangeSlider.lowerHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
rangeSlider.upperHandleImageNormal = lowerThumb.pb_takeSnapshot()
rangeSlider.upperHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
self.view.addSubview(rangeSlider)
self.view.backgroundColor = .lightGrayColor()
}
Using the method for capturing the UIView as UIImage mentioned in this question:
extension UIView {
func pb_takeSnapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.mainScreen().scale)
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Other solution:
You can also try sgwilly/RangeSlider instead, it's written in Swift and therefore you won't even need a Bridging Header.
try this code :
override func viewDidLayoutSubviews() {
let margin: CGFloat = 20.0
let width = view.bounds.width - 2.0 * margin
rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length,
width: width, height: 31.0)
}
I implemented the range slider using :
https://github.com/Zengzhihui/RangeSlider
In the GZRangeSlider class, there is a method called :
private func setLabelText()
In that method, just put :
leftTextLayer.frame = CGRectMake(leftHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftHandleLayer.frame.minY - kTextHeight, kTextWidth, kTextHeight)
rightTextLayer.frame = CGRectMake(rightHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftTextLayer.frame.minY, kTextWidth, kTextHeight)
to animate the lower and upper labels..
This one is working well for me and its in swift.. just try it..

Resources