Custom UISlider doesn't go to end - ios

I have created a UISlider that has tick marks and snaps to each tick when the value is changed. The problem is that the slider doesn't naturally go to the ends and the left and right tick are visible outside of the circle. I have extended the UISlider class to go to the ends but the animation conflicts with moving the slider to the next tick and the circle ends up off the screen. How can I keep the snap to tick animation and have the circle in the slider go all the way to the end? Here is my code
class CustomSlider: UISlider {
override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect
{
let unadjustedThumbrect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
let thumbOffsetToApplyOnEachSide:CGFloat = unadjustedThumbrect.size.width / 2.0
let minOffsetToAdd = -thumbOffsetToApplyOnEachSide
let maxOffsetToAdd = thumbOffsetToApplyOnEachSide
let offsetForValue = minOffsetToAdd + (maxOffsetToAdd - minOffsetToAdd) * CGFloat(value / (self.maximumValue - self.minimumValue))
var origin = unadjustedThumbrect.origin
origin.x += offsetForValue
return CGRect(origin: origin, size: unadjustedThumbrect.size)
}
}
#IBOutlet weak var slider: CustomSlider!
let tickArray : [Float] = [Float(18),Float(20),Float(22),Float(24),Float(26),Float(28),Float(30)]
override func viewDidLoad() {
super.viewDidLoad()
self.slider.value = tickArray[0]
self.slider.minimumValue = tickArray[0]
self.slider.maximumValue = tickArray[6]
self.slider.isContinuous = false
var tick : UIView
for i in 0..<tickArray.count-1 {
tick = UIView(frame: CGRect(x: (self.slider.frame.size.width/6) * CGFloat(i), y: (slider.frame.size.height - 13) / 2, width: 2, height: 13))
tick.backgroundColor = "8E8E93".hexColor
slider.insertSubview(tick, belowSubview: slider)
}
tick = UIView(frame: CGRect(x: self.slider.frame.size.width-2, y: (slider.frame.size.height - 13) / 2, width: 2, height: 13))
tick.backgroundColor = "8E8E93".hexColor
slider.insertSubview(tick, belowSubview: slider)
}
#IBAction func sliderValueChanged(_ sender: UISlider) {
if sender.value > tickArray[5] {
slider.value = tickArray[6]
} else if sender.value > tickArray[4] {
slider.value = tickArray[5]
} else if sender.value > tickArray[3] {
slider.value = tickArray[4]
} else if sender.value > tickArray[2] {
slider.value = tickArray[3]
} else if sender.value > tickArray[1] {
slider.value = tickArray[2]
} else if sender.value >
tickArray[0] {
slider.value = tickArray[1]
} else {
slider.value = tickArray[0]
}
}

I fixed my problem by hiding the left and right ticks when the slider goes to the edges. This took away the need for the slider extension.

Related

Glitchy UIScrollView Infinite Scroll

Im trying to implement an infinitely paging UIScrollView based on the Advanced ScrollView Techniques from WWDC 2011. The problem that Im facing is that as I scroll the screen keeps on Jumping backwards instead of advancing forward in the array. Is there any way to create this effect. Below is the code I have implemented thus far.
import Foundation
import UIKit
class CustomScrollView:UIScrollView{
var label1:CustomLabel!
var label2:CustomLabel!
var label3:CustomLabel!
var labels:[UILabel]!
var visibleLabels:[UILabel]!
var recycledPages:Set<CustomLabel>!
var visiblePages:Set<CustomLabel>!
override init(frame: CGRect) {
super.init(frame: frame)
indicatorStyle = .white
recycledPages = Set<CustomLabel>()
visiblePages = Set<CustomLabel>()
var firstScreenPostion:CGRect = CGRect(origin: CGPoint(x: 0 * bounds.width, y: 0), size: bounds.size)
var secondeScreenPosition:CGRect = CGRect(origin: CGPoint(x: 1 * bounds.width, y: 0), size: bounds.size)
var thirdScreenPosition:CGRect = CGRect(origin: CGPoint(x: 2 * bounds.width, y: 0), size: bounds.size)
label1 = CustomLabel(frame: firstScreenPostion)
label1.backgroundColor = .red
label1.text = "1"
label2 = CustomLabel(frame: secondeScreenPosition)
label2.backgroundColor = .green
label2.text = "2"
label3 = CustomLabel(frame: thirdScreenPosition)
label3.backgroundColor = .blue
label3.text = "3"
visibleLabels = [label1,label2,label3]
labels = [label1,label2,label3]
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func recenterIfNecessary(){
let currentOffset = contentOffset
let contentWidth = contentSize.width
let centerOffset = (contentWidth - bounds.width)/2
let distanceFromCenter = abs(currentOffset.x - centerOffset)
if distanceFromCenter > contentWidth/4{
self.contentOffset = CGPoint(x: centerOffset, y: 0)
for label in visibleLabels{
var center = label.center
center.x += centerOffset - currentOffset.x
label.center = center
}
}
}
override func layoutSubviews() {
recenterIfNecessary()
let visibleBounds = bounds
let minimumVisibleX = bounds.minX
let maximumVisbleX = bounds.maxX
tilePages(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
// tileLabelsFromMinX(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
}
func insertLabel()->UILabel{
let recycledLabels = visibleLabels.filter { (label) -> Bool in
return label.superview == nil
}
let label = recycledLabels.last ?? UILabel(frame: bounds)
self.addSubview(label)
return label
}
func placeNewLabelOnRight(rightEdge: CGFloat)->CGFloat{
let label = self.insertLabel()
visibleLabels.append(label) // add rightmost label at the end of the array
label.frame.origin.x = rightEdge;
label.frame.origin.y = 0
label.text = labels.last?.text
return label.frame.maxX
}
func placeNewLabelOnLeft(leftEdge:CGFloat)->CGFloat{
let label = self.insertLabel()
self.visibleLabels.insert(label, at: 0) // add leftmost label at the beginning of the array
label.frame.origin.x = leftEdge - frame.size.width;
label.frame.origin.y = bounds.size.height - frame.size.height;
label.text = labels[0].text
return label.frame.minX
}
//function used in video
// func tileLabelsFromMinX(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
// // the upcoming tiling logic depends on there already being at least one label in the visibleLabels array, so
// // to kick off the tiling we need to make sure there's at least one label
// if (self.visibleLabels.count == 0)
// {
// self.placeNewLabelOnRight(rightEdge: minimumVisibleX);
// }
// print("visible labels.count: \(visibleLabels.count)")
//
// // add labels that are missing on right side
// // UILabel *lastLabel = [self.visibleLabels lastObject];
// var lastLabel = visibleLabels.last!
// var rightEdge = lastLabel.frame.maxX
// while (rightEdge < maximumVisibleX){
// rightEdge = self.placeNewLabelOnRight(rightEdge: rightEdge)
// }
//
// // add labels that are missing on left side
// var firstLabel = self.visibleLabels[0]
// var leftEdge = firstLabel.frame.minX
// while (leftEdge > minimumVisibleX){
// leftEdge = self.placeNewLabelOnLeft(leftEdge:leftEdge)
// }
//
// // remove labels that have fallen off right edge
// // lastLabel = [self.visibleLabels lastObject];
//
// while (lastLabel.frame.origin.x > maximumVisibleX){
// lastLabel.removeFromSuperview()
// self.visibleLabels.removeLast()
// lastLabel = self.visibleLabels.last!
// }
//
// // remove labels that have fallen off left edge
// firstLabel = self.visibleLabels[0];
// while (firstLabel.frame.maxX < minimumVisibleX){
// firstLabel.removeFromSuperview()
// self.visibleLabels.removeFirst()
// firstLabel = self.visibleLabels[0];
// }
// }
func tilePages(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
let visibleBounds = bounds
var firstNeededPageIndex:Int = Int(floorf(Float(minimumVisibleX/visibleBounds.width)))
var lastNeededPageIndex:Int = Int(floorf(Float((maximumVisibleX - 1)/visibleBounds.width)))
firstNeededPageIndex = max(firstNeededPageIndex, 0)
lastNeededPageIndex = min(lastNeededPageIndex, labels.count - 1)
//Recycle no-longer needed pages
for page in visiblePages{
if page.index < Int(firstNeededPageIndex) || page.index > Int(lastNeededPageIndex){
recycledPages.insert(page)
page.removeFromSuperview()
}
}
visiblePages.subtract(recycledPages)
//add missing pages
for i in firstNeededPageIndex...lastNeededPageIndex{
if !isDisplaying(pageForIndex: i){
let page:CustomLabel = dequeueRecycledPage() ?? CustomLabel()
print("index i: \(i)")
self.configurePage(page: page, forIndex: i)
self.addSubview(page)
visiblePages.insert(page)
}
}
}
func isDisplaying(pageForIndex index:Int)->Bool{
for page in visiblePages{
if page.index == index{
return true
}
}
return false
}
func configurePage(page:CustomLabel,forIndex index:Int){
page.index = index
page.text = "current index: \(index)"
let width = bounds.width
let newX:CGFloat = CGFloat(index) * width
page.backgroundColor = labels[index].backgroundColor
page.frame = CGRect(origin: CGPoint(x: newX, y: 0), size: bounds.size)
}
func dequeueRecycledPage()->CustomLabel?{
let page = recycledPages.first
if let page = page{
recycledPages.remove(page)
return page
}
return nil
}
}

How to make Circular audio visualizer in swift?

I want to make a visualizer like this Circular visualizer, click the green flag to see the animation.
In my project first I draw a circle, I calculate the points on the circle to draw the visualizer bars, I rotate the view to make the bars feels like circle. I use StreamingKit to stream live radio. StreamingKit provides the live audio power in decibels. Then I animate the visualizer bars. But when I rotate the view the height and width changes according to the angle I rotate. But the bounds value not change (I know the frame depends on superViews).
audioSpectrom Class
class audioSpectrom: UIView {
let animateDuration = 0.15
let visualizerColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
var barsNumber = 0
let barWidth = 4 // width of bar
let radius: CGFloat = 40
var radians = [CGFloat]()
var barPoints = [CGPoint]()
private var rectArray = [CustomView]()
private var waveFormArray = [Int]()
private var initialBarHeight: CGFloat = 0.0
private let mainLayer: CALayer = CALayer()
// draw circle
var midViewX: CGFloat!
var midViewY: CGFloat!
var circlePath = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
self.layer.addSublayer(mainLayer)
barsNumber = 10
}
override func layoutSubviews() {
mainLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
drawVisualizer()
}
//-----------------------------------------------------------------
// MARK: - Drawing Section
//-----------------------------------------------------------------
func drawVisualizer() {
midViewX = self.mainLayer.frame.midX
midViewY = self.mainLayer.frame.midY
// Draw Circle
let arcCenter = CGPoint(x: midViewX, y: midViewY)
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
let circleShapeLayer = CAShapeLayer()
circleShapeLayer.path = circlePath.cgPath
circleShapeLayer.fillColor = UIColor.blue.cgColor
circleShapeLayer.strokeColor = UIColor.clear.cgColor
circleShapeLayer.lineWidth = 1.0
mainLayer.addSublayer(circleShapeLayer)
// Draw Bars
rectArray = [CustomView]()
for i in 0..<barsNumber {
let angle = ((360 / barsNumber) * i) - 90
let point = calculatePoints(angle: angle, radius: radius)
let radian = angle.degreesToRadians
radians.append(radian)
barPoints.append(point)
let rectangle = CustomView(frame: CGRect(x: barPoints[i].x, y: barPoints[i].y, width: CGFloat(barWidth), height: CGFloat(barWidth)))
initialBarHeight = CGFloat(self.barWidth)
rectangle.setAnchorPoint(anchorPoint: CGPoint.zero)
let rotationAngle = (CGFloat(( 360/barsNumber) * i)).degreesToRadians + 180.degreesToRadians
rectangle.transform = CGAffineTransform(rotationAngle: rotationAngle)
rectangle.backgroundColor = visualizerColor
rectangle.layer.cornerRadius = CGFloat(rectangle.bounds.width / 2)
rectangle.tag = i
self.addSubview(rectangle)
rectArray.append(rectangle)
var values = [5, 10, 15, 10, 5, 1]
waveFormArray = [Int]()
var j: Int = 0
for _ in 0..<barsNumber {
waveFormArray.append(values[j])
j += 1
if j == values.count {
j = 0
}
}
}
}
//-----------------------------------------------------------------
// MARK: - Animation Section
//-----------------------------------------------------------------
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
DispatchQueue.main.async {
UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: {
for i in 0..<self.barsNumber {
let channelValue: Int = Int(arc4random_uniform(2))
let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i])))
let barView = self.rectArray[i] as? CustomView
guard var barFrame = barView?.frame else { return }
// calculate the bar height
let barH = (self.frame.height / 2 ) - self.radius
// scale the value to 40, input value of this func range from 0-60, 60 is low and 0 is high. Then calculate the height by minimise the scaled height from bar height.
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
if channelValue == 0 {
barFrame.size.height = calc0
} else {
barFrame.size.height = calc1
}
if barFrame.size.height < 4 || barFrame.size.height > ((self.frame.size.height / 2) - self.radius) {
barFrame.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barView?.frame = barFrame
}
}, completion: nil)
}
}
func calculatePoints(angle: Int, radius: CGFloat) -> CGPoint {
let barX = midViewX + cos((angle).degreesToRadians) * radius
let barY = midViewY + sin((angle).degreesToRadians) * radius
return CGPoint(x: barX, y: barY)
}
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
extension UIView{
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position : CGPoint = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
self.layer.position = position;
self.layer.anchorPoint = anchorPoint;
}
}
I drag a empty view to storyBoard and give custom class as audioSpectrom.
ViewController
func startAudioVisualizer() {
visualizerTimer?.invalidate()
visualizerTimer = nil
visualizerTimer = Timer.scheduledTimer(timeInterval: visualizerAnimationDuration, target: self, selector: #selector(self.visualizerTimerFunc), userInfo: nil, repeats: true)
}
#objc func visualizerTimerFunc(_ timer: CADisplayLink) {
let lowResults = self.audioPlayer!.averagePowerInDecibels(forChannel: 0)
let lowResults1 = self.audioPlayer!.averagePowerInDecibels(forChannel: 1)
audioSpectrom.animateAudioVisualizerWithChannel(level0: -lowResults, level1: -lowResults1)
}
OUTPUT
Without animation
With animation
In my observation, the height value and width value of frame changed when rotates. Means when I give CGSize(width: 4, height: 4) to bar, then when I rotate using some angle it changes the size of frame like CGSize(width: 3.563456, height: 5.67849) (not sure for the value, it's an assumption).
How to resolve this problem?
Any suggestions or answers will be appreciated.
Edit
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
DispatchQueue.main.async {
UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: {
for i in 0..<self.barsNumber {
let channelValue: Int = Int(arc4random_uniform(2))
let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i])))
var barView = self.rectArray[i] as? CustomView
guard let barViewUn = barView else { return }
let barH = (self.frame.height / 2 ) - self.radius
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
let kSavedTransform = barViewUn.transform
barViewUn.transform = .identity
if channelValue == 0 {
barViewUn.frame.size.height = calc0
} else {
barViewUn.frame.size.height = calc1
}
if barViewUn.frame.height < CGFloat(4) || barViewUn.frame.height > ((self.frame.size.height / 2) - self.radius) {
barViewUn.frame.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barViewUn.transform = kSavedTransform
barView = barViewUn
}
}, completion: nil)
}
}
Output
Run the below code snippet show the output
<img src="https://i.imgflip.com/227xsa.gif" title="made at imgflip.com"/>
GOT IT!!
circular-visualizer
There are two (maybe three) issues in your code:
1. audioSpectrom.layoutSubviews()
You create new views in layoutSubviews and add them to the view hierarchy. This is not what you are intened to do, because layoutSubviews is called multiple times and you should use it only for layouting purposes.
As a dirty work-around, I modified the code in the func drawVisualizer to only add the bars once:
func drawVisualizer() {
// ... some code here
// ...
mainLayer.addSublayer(circleShapeLayer)
// This will ensure to only add the bars once:
guard rectArray.count == 0 else { return } // If we already have bars, just return
// Draw Bars
rectArray = [CustomView]()
// ... Rest of the func
}
Now, it almost looks good, but there are still some dirt effects with the topmost bar. So you'll have to change
2. audioSectrom.animateAudioVisualizerWithChannel(level0:level1:)
Here, you want to recalculate the frame of the bars. Since they are rotated, the frame also is rotated, and you'd have to apply some mathematical tricks. To avoid this adn make your life more easy, you save the rotated transform, set it to .identity, modify the frame, and then restore the original rotated transform. Unfortunately, this causes some dirt effects with rotations of 0 or 2pi, maybe caused by some rounding issues. Never mind, there is a much more simple solution:
Instead of modifiying the frame, you better modify the bounds.
frame is measured in the outer (in your case: rotated) coordinate system
bounds is measured in the inner (non-transformed) coordinate system
So I simply replaced all the frames with bounds in the function animateAudioVisualizerWithChannel and also removed the saving and restoring of the transformation matrix:
func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) {
// some code before
guard let barViewUn = barView else { return }
let barH = (self.bounds.height / 2 ) - self.radius
let scaled0 = (CGFloat(level0) * barH) / 60
let scaled1 = (CGFloat(level1) * barH) / 60
let calc0 = barH - scaled0
let calc1 = barH - scaled1
if channelValue == 0 {
barViewUn.bounds.size.height = calc0
} else {
barViewUn.bounds.size.height = calc1
}
if barViewUn.bounds.height < CGFloat(4) || barViewUn.bounds.height > ((self.bounds.height / 2) - self.radius) {
barViewUn.bounds.size.height = self.initialBarHeight + CGFloat(wavePeak)
}
barView = barViewUn
// some code after
}
3. Warnings
By the way, you should get rid of all the warnings in your code. I didn't clean up my answer code to keep it comparable with the orginal code.
For example, in var barView = self.rectArray[i] as? CustomView you don't need the conditional cast, because the array already contains CustomView objects.
So, all the barViewUn stuff is unnecessary.
Much more to find and to clean up.

Button stops moving when creating new button who moves

I'm creating a game where a button who is being created moves from one side of the screen to the other when I click a button called start. The problem is that when I click start before the button who was moving reaches its end point, it stops instead of continuing (and the another created button start moving like expected).
Should I create a new CADisplayLink every time I click the start button? If so, how would I do that? Here's the code:
var button1 = UIButton()
var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime?
let duration = 2.0
var leadingConstraint: NSLayoutConstraint!
var topConstraint: NSLayoutConstraint!
var l1 = false
#IBAction func start(sender: UIButton) {
n1()
}
func n1() {
l1 = false
startTime = CFAbsoluteTimeGetCurrent()
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
if l1 == false { // so it doesn't randomize leading constraint twice
button1 = createButton()
let randomNumber = Int(arc4random_uniform(180) + 30)
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
var percentComplete = CGFloat(elapsed / duration)
if percentComplete >= 1.0 {
percentComplete = 1.0
// self.button1.removeFromSuperview()
displayLink.invalidate()
button1.hidden = true
}
leadingConstraint.constant = CGFloat(randomNumber)
topConstraint.constant = 390 - 350 * percentComplete
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button1.widthAnchor.constraintEqualToConstant(75),
button1.heightAnchor.constraintEqualToConstant(75)
])
l1 = true
}
else{
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
var percentComplete = CGFloat(elapsed / duration)
if percentComplete >= 1.0 {
percentComplete = 1.0
displayLink.invalidate()
button1.hidden = true
}
topConstraint.constant = 390 - 350 * percentComplete
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button1.widthAnchor.constraintEqualToConstant(75),
button1.heightAnchor.constraintEqualToConstant(75)
])
}
}
func buttonPressed(sender: UIButton!) {
button1.hidden = true
displayLink?.invalidate()
}
func createButton() ->UIButton {
let button = UIButton()
button.setImage(UIImage(named: "BlueBall.png")!, forState: UIControlState.Normal)
button.addTarget(self, action: "buttonPressed:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
leadingConstraint = button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: 0)
topConstraint = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 0)
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button.widthAnchor.constraintEqualToConstant(75),
button.heightAnchor.constraintEqualToConstant(75)
])
return button
}
Please help. It would be really appreciated. Thanks in advance. Anton
Okay Anton O; as discussed I post an answer how to detect a touch on a moving UIView. This works for both, CAAnimation and UIView.animationWith..
First I created an extension of CGRect, just for convenience:
extension CGRect {
init(position: CGPoint, size: CGSize) {
self.origin = CGPoint(x: position.x - (size.width / 2.0), y: position.y - (size.height / 2.0))
self.size = size
}
}
Then I created two methods which create and move the view. You can adapt the code then to your needs. (I hold a global variable called nextView to keep reference to the view, can also be extended to an array of course)
Create View:
private func createView(index: Int) {
nextView?.removeFromSuperview()
nextView = UIView()
nextView?.bounds = CGRect(x: 0, y: 0, width: 60, height: 60)
nextView?.backgroundColor = UIColor.redColor()
nextView?.center = CGPoint(x: 30, y: CGRectGetMidY(self.view.bounds))
if let nextView = nextView {
view.addSubview(nextView)
}
}
Move View:
private func moveView() {
guard let nextView = nextView else {
return
}
UIView.animateWithDuration(5.0) { () -> Void in
nextView.center = CGPoint(x: CGRectGetMaxX(self.view.bounds) + 30, y: CGRectGetMidY(self.view.bounds))
}
}
Detect Touch:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
if let touch = touches.first, nextView = nextView {
let touchRect = CGRect(position: touch.locationInView(self.view), size: CGSize(width: 20, height: 20))
guard let viewPosition = nextView.layer.presentationLayer()?.position else {
return
}
let viewRect = CGRect(position: viewPosition, size: nextView.bounds.size)
if CGRectIntersectsRect(touchRect, viewRect) {
print("👍 💯")
} else {
print("👎")
}
}
}
You can extend the methods for your needs and also add some "performance" enhancing checks (like if a view is visible and move on or return right there in the touchesEnded method, 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..

UIView changing its position in swift

How do I make a UIView slide up with a touch of a button from its original position and them bring it back down with a touch of a button? Using Swift and Xcode 6.
I have currently tried this:
#IBOutlet weak var DynView: UIView!
#IBAction func btnUp(sender: AnyObject) {
}
You have to implement an animation changing the DynView position on click. Here's an example:
#IBAction func btnUp(sender: AnyObject) {
let xPosition = DynView.frame.origin.x
let yPosition = DynView.frame.origin.y - 20 // Slide Up - 20px
let width = DynView.frame.size.width
let height = DynView.frame.size.height
UIView.animateWithDuration(1.0, animations: {
dynView.frame = CGRect(x: xPosition, y: yPosition, width: width, height: height)
})
}
Hi create this extends if you want. For Swift
Create File Extends.Swift and add this code
/**
Extension UIView
by DaRk-_-D0G
*/
extension UIView {
/**
Set x Position
:param: x CGFloat
by DaRk-_-D0G
*/
func setX(#x:CGFloat) {
var frame:CGRect = self.frame
frame.origin.x = x
self.frame = frame
}
/**
Set y Position
:param: y CGFloat
by DaRk-_-D0G
*/
func setY(#y:CGFloat) {
var frame:CGRect = self.frame
frame.origin.y = y
self.frame = frame
}
/**
Set Width
:param: width CGFloat
by DaRk-_-D0G
*/
func setWidth(#width:CGFloat) {
var frame:CGRect = self.frame
frame.size.width = width
self.frame = frame
}
/**
Set Height
:param: height CGFloat
by DaRk-_-D0G
*/
func setHeight(#height:CGFloat) {
var frame:CGRect = self.frame
frame.size.height = height
self.frame = frame
}
}
For Use (inherits Of UIView)
inheritsOfUIView.setX(x: 100)
button.setX(x: 100)
view.setY(y: 100)
I kinda combined the two most voted answers into one and updated to Swift 3. So basically created an extension that animates a view moving to a different position:
extension UIView {
func slideX(x:CGFloat) {
let yPosition = self.frame.origin.y
let height = self.frame.height
let width = self.frame.width
UIView.animate(withDuration: 1.0, animations: {
self.frame = CGRect(x: x, y: yPosition, width: width, height: height)
})
}
}
// MARK: - Properties
var bottomViewHeight: CGFloat = 200
var isViewHide = false
private let bottomView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let showHideButton: UIButton = {
let button = UIButton()
button.setTitle("Show / Hide", for: .normal)
button.setTitleColor(.black, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(showHideButtonTapped(_:)), for: .touchUpInside)
return button
}()
// MARK: - Lifecycle
override func loadView() {
super.loadView()
view.addSubview(bottomView)
NSLayoutConstraint.activate([
bottomView.heightAnchor.constraint(equalToConstant: bottomViewHeight),
bottomView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
bottomView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
bottomView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
view.addSubview(showHideButton)
NSLayoutConstraint.activate([
showHideButton.widthAnchor.constraint(equalToConstant: 200),
showHideButton.heightAnchor.constraint(equalToConstant: 50),
showHideButton.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
showHideButton.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
showHideView(isShow: isViewHide)
}
// MARK: - Selectors
#objc func showHideButtonTapped(_ sender: UIButton) {
print("👆 HIDE / SHOW BUTTON")
showHideView(isShow: isViewHide)
}
// MARK: - Functions
private func showHideView(isShow: Bool) {
if isShow {
UIView.animate(withDuration: 0.4) {
self.bottomView.transform = CGAffineTransform(translationX: 0, y: self.bottomViewHeight)
}
} else {
UIView.animate(withDuration: 0.4) {
self.bottomView.transform = CGAffineTransform(translationX: 0, y: 0)
}
}
isViewHide = !isViewHide
}

Resources