Swift Scroll View with slider and MIDI - swift

Im using Scroll View to scroll a picture. I'm using a slider 1 - 26 to retrieve CGPoint from an Array for x axis points. Everything behaves as expected and the image is smooth in transition.
I also set up a button that when pressed count from 1 - 26 and loops to retrieve CGPoint from an Array for x axis points. Everything behaves as expected and the image is smooth in transition.
I have also set up MIDI that sends data 1 - 26 based on velocity this is sent to the slider, and the slider moves as expected. I also using MIDI to retrieve CGPoint from an Array for x axis points. Unfortunately although it wants to scroll the following pictures shows what happens you only get to see 4 images as it scrolls and you can tell by the moving slider when they appear
I'm very confident that the Midi is working correctly as i use it all the time to read from arrays. I have also tried triggering the button from Midi a kind of virtual press. This works as expected but shows the same as triggering directly from midi. It seems as though everything is fine as long as the scroll originates from a IB component.
Under Midi control the scroll bar also stutters but if i set
clipView.wantsLayer = true
The picture does not scroll under midi but the scroll bar moves as expected. Even though the slider and the button work as expected.
I have also tried all the things commented out in the playMidi. As well trying all the different parameters in the IB section.
I am at a point where i really could do with some help please.
Here is my code which hopefully also explains what i have tried.
I have also tried various
needsDisplay = true
on all views
Xcode 8 Swift3 OSX not iOS
Thank you for you time
import Cocoa
import CoreMIDI
class MainWindowController: NSWindowController {
static var subWindowController: MainWindowController!
var pointArray:[Double] = [99.00,0.00,40.00,80.00,120.00,160.00,200.00,240.00,280.00,320.00,360.00,400.00,440.00,480.00,520.00,560.00,600.00,640.00,680.00,720.00,760.00,800.00,840.00,880.00,920.00,960.00,1000.00]
#IBOutlet weak var scrollView: NSScrollView!
#IBOutlet weak var clipView: NSClipView!
#IBOutlet weak var ImageView: NSImageView!
#IBOutlet var scrollerBar: NSScroller!
#IBOutlet weak var sliderControl: NSSlider!
var myPointer = CGPoint.init(x: 0.0, y: 0.0)
var fromArray:Double = 0.0
var buttonCount = 0
override func windowDidLoad() {
super.windowDidLoad()
MainWindowController.subWindowController = self // USED for midi
//Initialize midi client
var midiClient: MIDIClientRef = 0;
var inPort:MIDIPortRef = 0;
var src:MIDIEndpointRef = MIDIGetSource(0);
MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient);
MIDIInputPortCreate(midiClient, "MidiTest_InPort" as CFString, MyMIDIReadProc, nil, &inPort);
MIDIPortConnectSource(inPort, src, &src);
//Set paramameters
scrollerBar.doubleValue = 0.9
let scrollViewSize = NSSize(width: 287, height: 119)
scrollView.setFrameSize(scrollViewSize)
let scrollViewColor = CGColor(red: 0.0, green: 0.0, blue: 0.9, alpha: 0.5)
scrollView.wantsLayer = false
scrollView.layer?.backgroundColor = scrollViewColor
scrollView.drawsBackground = false
let clipViewSize = NSSize(width: 1274, height: 88)
clipView.setFrameSize(clipViewSize)
let clipViewColor = CGColor(red: 0.0, green: 0.0, blue: 0.9, alpha: 0.5)
clipView.wantsLayer = false //If true image freezes but scoller moves with midi
clipView.layer?.backgroundColor = clipViewColor
clipView.drawsBackground = false
let ImageViewSize = NSSize(width: 1274, height: 87)
ImageView.setFrameSize(ImageViewSize)
/* let ImageViewColor = CGColor(red: 0.0, green: 0.9, blue: 0.0, alpha: 0.5)
ImageView.wantsLayer = false
ImageView.layer?.backgroundColor = ImageViewColor*/
}//EO Overide
func playMidi(count:Int){
//print("MIDi",count)
let fromCountIndex = count // Gets 1 to 26 from midi
//Gets double from Array 1 to 26 on Index == 0.0 to 960.0
fromArray = pointArray[fromCountIndex]
/*SCROLL BAR THINGS I HAVE TRIED*/
let scrollerValue = fromArray/960
scrollerBar.doubleValue = scrollerValue
/* scrollerBar.display()
scrollerBar.isContinuous = true */
/*CLIP VIEW SCROLL FROM ARRAY TRIGGERED BY MIDI */
myPointer = CGPoint(x:fromArray,y:0.0)
clipView.scroll(myPointer)
/*scrollView.scroll(clipView, to: myPointer)*/
/*THIS FAKES A BUTTON FIRE BUT STILL SAME PROBLEM*/
/* let mySelector = #selector(myButton(_:))
myButton(mySelector as AnyObject)*/
/* DISPLAY VIEWS I HAVE TRIED*/
// scrollView.scrollsDynamically = true
/*MOVES THE SLIDER USING MIDI*/
sliderControl.integerValue = count
}//eo playMidi
#IBAction func myButton(_ sender: AnyObject) {
fromArray = pointArray[buttonCount]
myPointer = CGPoint(x:fromArray,y:0.0)
clipView.scroll(myPointer)
buttonCount = buttonCount + 1
if (buttonCount > 26){buttonCount = 0}
print("buttonCount",buttonCount)
}
#IBAction func mySlider(_ sender: AnyObject) {
let fromSlider = sender.integerValue * 1
//Gets double from Array 1 to 26 on Index == 0.0 to 960.0 on array set to 40 when using slider moves scroll bar
fromArray = pointArray[fromSlider]
myPointer = CGPoint(x:fromArray,y:0.0)
clipView.scroll(myPointer)
print("fromSlider",fromSlider)
}
}//EnD oF thE wORld

Further to my code as above I have not answered the question but instead found a "get around". I'm really not proud of this but it seems to pose a question that maybe someone might help in answering. As the code above using the Scroll View was not working i have instead tried using CAScrollLayer.
The basic setup is as follow. I have a view inside that view I created a Scroll Layer and inside that a layer. Again as above if I use a timer to fire the scroll view everything works perfectly. And again as above if i use Midi then the scroll doesn't happen. So the work around is. The midi 1-26 reads the point value the but instead of trying to send it to the scroll it send it to a transistion variable.
I have a "Run Button" on the interface this would normally work the scrollLayerScroll. It still does this but at an incredible speed. But instead of generation its own transition variable it uses the one created buy the midi function. There is a piece of code that makes sure only a unique number is sent to the point.
This works but really deserves a prize for the worst bit of coding ever.
I think there is something in the way the timer refreshes everything that i need to extract. I hoping someone may still take pity and help
thanks again
here is the revised code Xcode 8 Swift3 OSX not iOS
import Cocoa
import CoreMIDI
class MainWindowController: NSWindowController {
static var subWindowController: MainWindowController!
var myLayer = CALayer()
var myPointer = CGPoint.init(x: 0.0, y: 0.0)
var fromArray:Double = 0.0
var buttonCount = 0
var pointArray:[Double] = [99.00,0.00,40.00,80.00,120.00,160.00,200.00,240.00,280.00,320.00,360.00,400.00,440.00,480.00,520.00,560.00,600.00,640.00,680.00,720.00,760.00,800.00,840.00,880.00,920.00,960.00,1000.00]
#IBOutlet weak var myCustomView: NSView!
var translation: CGFloat = 0.0
var newTranslation:CGFloat = 0.0
var comp6:CGFloat = 0.0
var comp6a:CGFloat = 0.0
var scrollLayer : CAScrollLayer = {
let scrollLayer = CAScrollLayer() // 8
scrollLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 150.0, height: 200.0) // 9
scrollLayer.position = CGPoint(x: 300/2, y: 180/2) // 10 //not sure about this
scrollLayer.borderColor = CGColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.8)
scrollLayer.borderWidth = 5.0 // 12
scrollLayer.scrollMode = kCAScrollHorizontally // 13
return scrollLayer
}()
var scrollTimer = Timer()
var layer = CALayer()
override func windowDidLoad() {
super.windowDidLoad()
MainWindowController.subWindowController = self // USED for midi
//Initialize midi client
var midiClient: MIDIClientRef = 0;
var inPort:MIDIPortRef = 0;
var src:MIDIEndpointRef = MIDIGetSource(0);
MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient);
MIDIInputPortCreate(midiClient, "MidiTest_InPort" as CFString, MyMIDIReadProc, nil, &inPort);
MIDIPortConnectSource(inPort, src, &src);
//Set up layer
let img = NSImage(named: "smallSky.png")
let imageSize = img?.size
print (imageSize)
let layer = CALayer()
layer.bounds = CGRect(x: 0.0, y: 0.0, width: (imageSize?.width)!, height: (imageSize?.height)!) // 3
layer.position = CGPoint(x: (imageSize?.width)!/4, y: (imageSize?.height)!/2) // 4
layer.contents = img//5
myCustomView.layer?.addSublayer(scrollLayer)
scrollLayer.addSublayer(layer)
myCustomView.layer = scrollLayer
myCustomView.wantsLayer = true
}//EO Overide
#IBAction func runButton(_ sender: AnyObject) {
print("run")
//Sets of timer at very high Speed
scrollTimer = Timer.scheduledTimer(timeInterval: 0.0001, target: self, selector: #selector(MainWindowController.scrollLayerScroll as (MainWindowController) -> () -> ()), userInfo: nil, repeats: true)
}
func scrollLayerScroll(){//FIRED START FROM RUN BUTTON
//Collects data from translation and only sends to newTranslation when number is different
comp6a = translation
if (comp6a != comp6){
newTranslation = translation
print("newTranslation",newTranslation)
let newPoint = CGPoint(x: newTranslation,y:0.0)
scrollLayer.scroll(to: newPoint)
}
comp6 = comp6a
}//eo scrollLayerScroll
func playMidi(count:Int){
let fromCountIndex = count // Gets 1 to 26 from midi
fromArray = pointArray[fromCountIndex]
translation = CGFloat(fromArray)
}//eo playMidi
}//EnD oF thE wORld

Related

Tried making a custom ScrollView, but instead of scrolling it's spamming up and down

I tried to create some kind of timeline (with the Vector Illustrator mentality), using UIBezier and UI Label (kind of like in the calendar app) and then use UIPanGestureRecognizer to scroll it up and down. But whenever I scroll it in the simulator, it multiplies itself instead of moving like the images below (I use setNeedsDisplay as the scrollValue changes to redraw the whole mechanism). This is probably a small mistake a I did or maybe my code doesn't work.
I know I could use a UIScrollView or UITableView instead, but I tried making this as a small challenge as a custom made table because using pre-made objects feels limiting for someone like me who is used to CAD drawing or Vector Illustrator.
This image explains what happens in the Simulator:
The code I used is below:
import UIKit
class ViewController: UIViewController {
var tlobject = TimelineView()
let gesto = UIPanGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
// ===== Add TimelineView Object to view
let TLObjectFrame = CGRect(x: 0, y: 40, width: 100, height: 100)
tlobject = TimelineView(frame: TLObjectFrame)
view.addSubview(tlobject)
// ===== ADD TOUCH GESTURE =====
gesto.addTarget(self, action: #selector(touchinput))
view.addGestureRecognizer(gesto)
}
var touchStartLocation: Int = 0
var scrollDistance: Int = 0
var lastScrollDistance: Int = 0
//The following func calculates the distance scrolled/travelled by Touch gesture on the YAxis and sends the result value (scrollDistance) to the Timeline mechanism where it defines the Yposition of every UIBezier. Thanks to Mitchell Hudson on Youtube for helping me figure out how to do it on his Tutorial "06 11 touches value"
#objc func touchinput (sender: UIPanGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.began {
touchStartLocation = Int(sender.location(in: view).y)
lastScrollDistance = scrollDistance
}
if sender.state == UIGestureRecognizer.State.changed {
let touchEndLocation = Int(sender.location(in: view).y)
let currentScrollDistance = touchEndLocation - touchStartLocation
print("deltaY", currentScrollDistance)
var newScrollDistance = lastScrollDistance + currentScrollDistance
scrollDistance = newScrollDistance
tlobject.totalScrollDistance = scrollDistance //send scrollValue to TimelineView
}
if sender.state == UIGestureRecognizer.State.ended {
print("lastScrollDistance", lastScrollDistance)
print("scroll Distance", scrollDistance)
}
}
}
//Created a new View with the TimeLine mechanism
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet{
setNeedsDisplay() //this gets called everytime UIgesture position changes
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
timelinemechanism()
}
func timelinemechanism() {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30
let scrollDistance: Int = totalScrollDistance
let totalElements: Int = lineElements.count
for n in 1...totalElements {
//Get UILabel/UILine Yposition on screen = Array index number * the spacing + scroll distance by touch pan gesture
let yPosition = lineElements[n - 1] * spacing + scrollDistance
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 300, y: yPosition))
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
//lineshape.fillColor = UIColor.white.cgColor
//lineshape.lineWidth = 1
self.layer.addSublayer(lineshape)
let hourlabel = UILabel()
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
//hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = NSTextAlignment.right
self.addSubview(hourlabel)
}
}
}
Inside draw you only have to draw something. You add new subviews/sublayers and do not remove old ones.
Creating a new view every time you change a frame is very resource-intensive. And you don't need that, because you have the same views, you only need to change the position.
Instead, you can create your views at start and use layoutSubviews to update your views positions:
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet{
setNeedsLayout() //this gets called everytime UIgesture position changes
}
}
private var lastLayoutTotalScrollDistance: Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
createTimelinemechanism()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var lineShapes = [CAShapeLayer]()
var hourLabels = [UILabel]()
override func layoutSubviews() {
super.layoutSubviews()
let offset = totalScrollDistance - lastLayoutTotalScrollDistance
lastLayoutTotalScrollDistance = totalScrollDistance
lineShapes.forEach { lineShape in
lineShape.frame.origin.y += CGFloat(offset)
}
hourLabels.forEach { hourLabel in
hourLabel.frame.origin.y += CGFloat(offset)
}
}
func createTimelinemechanism() {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30
let totalElements: Int = lineElements.count
for n in 1...totalElements {
//Get UILabel/UILine Yposition on screen = Array index number * the spacing + scroll distance by touch pan gesture
let yPosition = lineElements[n - 1] * spacing
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 300, y: yPosition))
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
//lineshape.fillColor = UIColor.white.cgColor
//lineshape.lineWidth = 1
// disable default layer position animation
lineshape.actions = [
"position": NSNull(),
]
self.layer.addSublayer(lineshape)
lineShapes.append(lineshape)
let hourlabel = UILabel()
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
//hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = NSTextAlignment.right
self.addSubview(hourlabel)
hourLabels.append(hourlabel)
}
}
}
More generally, you can just list all the subviews/sublayers and not keep them in separate containers.
I spent a bit more time with your question since my first thought was wrong. Let me start by saying that your approach here is not the right way to go about this. But it looks to me like you're playing with different aspects of the framework just to learn your way around and I can respect that. I spent many years working on as vector drawing program (Macromedia FreeHand) and even wrote a book about drawing with Quartz 2D back in 2006 so I understand the desire to draw it yourself.
I've reworked your example using "raw" drawing at the CGContext level. I was playing with your code in a Playground so I restructured the view creation a bit too (just so it shows up in the Playground nicely). You should be able to copy and paste this into an iOS playground and see the results.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let gesto = UIPanGestureRecognizer()
let timelineView = TimelineView()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = true
self.view.backgroundColor = UIColor.red
self.view.bounds = CGRect(x:0, y:0, width: 320, height: 700)
// ===== Add TimelineView Object to view
view.addSubview(timelineView)
timelineView.frame = CGRect(x: 20, y: 20, width: 280, height: 660)
timelineView.backgroundColor = UIColor.white
debugPrint(timelineView.bounds)
// ===== ADD TOUCH GESTURE =====
gesto.addTarget(self, action: #selector(touchinput))
timelineView.addGestureRecognizer(gesto)
}
var touchStartLocation: Int = 0
var scrollDistance: Int = 0
var lastScrollDistance: Int = 0
//The following func calculates the distance scrolled/travelled by Touch gesture on the YAxis and sends the result value (scrollDistance) to the Timeline mechanism where it defines the Yposition of every UIBezier. Thanks to Mitchell Hudson on Youtube for helping me figure out how to do it on his Tutorial "06 11 touches value"
#objc func touchinput (sender: UIPanGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.began {
touchStartLocation = Int(sender.location(in: view).y)
lastScrollDistance = scrollDistance
}
if sender.state == UIGestureRecognizer.State.changed {
let touchEndLocation = Int(sender.location(in: view).y)
let currentScrollDistance = touchEndLocation - touchStartLocation
print("deltaY", currentScrollDistance)
scrollDistance = lastScrollDistance + currentScrollDistance
timelineView.totalScrollDistance = scrollDistance //send scrollValue to TimelineView
}
if sender.state == UIGestureRecognizer.State.ended {
print("lastScrollDistance", lastScrollDistance)
print("scroll Distance", scrollDistance)
}
}
}
//Created a new View with the TimeLine mechanism
class TimelineView: UIView {
var totalScrollDistance: Int = 0 {
didSet {
setNeedsDisplay() //this gets called everytime UIgesture position changes
}
}
override func draw(_ rect: CGRect) {
if let cgContext = UIGraphicsGetCurrentContext() {
drawTimeline(cgContext: cgContext)
}
}
func drawTimeline(cgContext: CGContext) {
let numElements = 10
let spacing = 30
let scrollDistance = totalScrollDistance
for n in 0..<numElements {
let yPosition = n * spacing + scrollDistance
cgContext.saveGState()
cgContext.setLineWidth(1.0)
cgContext.setStrokeColor(UIColor.blue.cgColor)
cgContext.move(to: CGPoint(x: 60, y: yPosition))
cgContext.addLine(to: CGPoint(x: 300, y: yPosition))
cgContext.strokePath()
let label : NSString = "\(n):00" as NSString
label.draw(at: CGPoint(x: 5, y: yPosition - 20),
withAttributes: [.foregroundColor : UIColor.blue])
cgContext.restoreGState()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()
The drawRect of the custom view grabs the current CGContext and passes it to the routine that does the drawing. Using something like UIBezierPath will work, of course (you saw that it did) but it has overhead (creating an actual object, copying the object into the context graphics state on each drawing, etc) that you don't necessarily need.
I'm not sure what you were doing with CAShapeLayer. You'd typically use that if you had a shape that you want to animate around the screen. I suppose you felt that, in scrolling, you might want to do that. But again this is something where you'd want to create the shape layer outside of the drawing path, keep ahold of it, manipulate it outside of the drawing path, then let the system handle worry about putting it on the screen appriopriately.
Your instincts on text are pretty good. You really don't want to handle Text drawing yourself in a system as complex as iOS. There's Unicode issues, glyph substitution, positional forms, ligatures, bi-di text... a whole host of challenges for drawing text on iOS that it's best to leave to things like UILabel. But you want to keep building your view hierarchy separate from drawing in your view hierarchy. drawRect can be called any time even a pixel of your view needs to be redrawn and adding a new subview each time is not the best way to go. In my reworked example, I'm drawing the text using NSString - It's still not the "right" way to do it but it's fairly low level while still giving the framework a chance to do some of the text handling.
In the end you would want to work with the frameworks instead of against them. You'd want to use something like UIScrollView because it will handle a thousand details (bouncing at the boundaries, ease-in/ease-out animation, touch point tracking, fast and slow scrolling, etc) but for a learning experience your code is just fine and I hope you enjoy working with iOS more!

How can I change a variable value using button/slider/touch action?

I am a student and very new to coding (Xcode with Swift language in this case).
I was trying to change a variable using a button/slider/gesture like this:
There are two variables (firstVariable & secondVariable) and set up a UIslider input that should define the value of the variables
I wanted the variables to be the same value of the slider/touchgesture but the variables remain 4 and 6 even after scrolling the slider in the simulator.
I think its because they are out of the scope of the slider function but how can I link them?
class MyViewController : UIViewController {
var firstVariable = 4
override func viewDidLoad() {
super.viewDidLoad()
var secondVariable = 6
print(secondVariable)
print(firstVariable)
let myslider = UISlider()
//create a UISLider
myslider.maximumValue = 100
myslider.minimumValue = 0
myslider.value = 30
myslider.frame = CGRect(x: 150, y: 200, width: 200, height: 40)
myslider.addTarget(self, action: #selector(sliderinput), for: UIControl.Event.valueChanged)
self.view.addSubview(myslider)
}
#objc func sliderinput(sender: UISlider) {
firstVariable = Int(sender.value)
secondVariable = Int(sender.value)
}
}
Thanks in advance!
First of all, take your secondVariable outside of the viewDidLoad as you cannot access it outside of viewDidLoad. To learn about scope read this. And you need to print the value of the variables to see the values are actually changing.
#objc func sliderinput(sender: UISlider) {
firstVariable = Int(sender.value)
secondVariable = Int(sender.value)
print(secondVariable)
print(firstVariable)
}
Sorry, this is the real context. I wanted to make an infinite timeline like the iOS calendar day tab or like a video editing software timeline for example.
I haven’t yet figured out how to get the distance from the beginning and end of the touch and how to link it to the scrollDistance variable on the beginning so I could create a scroll down effect and later a pinchgesturerecognizer to set the spacing/zoom between the lines.
I know I could use a UIscrollview or UIcollectionview but I felt it was good idea to create a custom one xD
Once again thanks for everyones support! :D
import UIKit
class ViewController: UIViewController {
let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let spacing: Int = 30 //spacing between elements
var scrollDistance = 90 // 90 for example, this should be the pangesture distance
//UIPanGestureRecogn function to determine the Y scroll distance from touch began to ended
#objc func touchinput (sender: UIPanGestureRecognizer) {
let touchlocation = sender.location(in: view)
var touchStart: CGPoint
var touchEnd: CGPoint
print("touch current location is", touchlocation)
if sender.state == UIGestureRecognizer.State.began {
touchStart = sender.location(in: view)
print("touch start", touchStart)
} else if sender.state == UIGestureRecognizer.State.ended {
touchEnd = sender.location(in: view)
print("touch end", touchEnd)
}
//I haven’t figure out how to get the distance between start and end and link it to the scrollDistance
//let scrollDistance = touchEnd.y - touchStart.y
}
override func viewDidLoad() {
super.viewDidLoad()
// ===================== Timeline a partir daqui =========================
let totalElements: Int = lineElements.count
for n in 1...totalElements {
let yPosition = lineElements[n - 1] * spacing + scrollDistance //Get Line heigh index times the spacing + scroll distance by touch pan gesture
let hourlabel = UILabel()
let linepath = UIBezierPath()
linepath.move(to: CGPoint(x: 60, y: yPosition))
linepath.addLine(to: CGPoint(x: 350, y: yPosition))
linepath.lineWidth = 8
let lineshape = CAShapeLayer()
lineshape.path = linepath.cgPath
lineshape.strokeColor = UIColor.blue.cgColor
lineshape.fillColor = UIColor.white.cgColor
self.view.layer.addSublayer(lineshape)
hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
hourlabel.text = "\(n):00"
hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
hourlabel.textColor = UIColor.blue
hourlabel.textAlignment = .right
self.view.addSubview(hourlabel)
}
// ===== GESTURE =====
let gesto = UIPanGestureRecognizer()
gesto.addTarget(self, action: #selector(touchinput))
view.addGestureRecognizer(gesto)
}
}
´´´

UIViewPropertyAnimator's bounce effect

Let's say I have an animator that moves a view from (0, 0) to (-120, 0):
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8)
animator.addAnimations {
switch state:
case .normal: view.frame.origin.x = 0
case .swiped: view.frame.origin.x = -120
}
}
I use it together with UIPanGestureRecognizer, so that I can resize the view continuously along with the finger movements.
The issue comes when I want to add some sort of bouncing effect at the start or at the end of the animation. NOT just the damping ratio, but the bounce effect. The easiest way to imagine this is Swipe-To-Delete feature of UITableViewCell, where you can drag "Delete" button beyond its actual width, and then it bounces back.
Effectively what I want to achieve, is the way to set fractionComplete property outside of [0, 1] segment, so when the fraction is 1.2, the offset becomes 144 instead of its 120 maximum.
And right now the maximum value for fractionComplete is exactly 1.
Below are some examples to have this issue visualized:
What I currently have:
What I want to achieve:
EDIT (19 January):
Sorry for my delayed reply. Here are some clarifications:
I don't use UIView.animate(...), and use UIViewPropertyAnimator instead for a very specific reason: it handles for me all the timings, curves and velocities.
For example, you dragged the view halfway through. This means that duration of the remaining part should be two times less than total duration. Or if you dragged though the 99% of the distance, it should complete the remaining part almost instantly.
As an addition, UIViewPropertyAnimator has such features as pause (when user starts dragging once again), or reverse (when user started dragging to the left, but after that he changed his mind and moved the finger to the right), that I also benefit from.
All this is not available for simple UIView animations, or requires TONS of effort at best. It is only capable of simple transitions, and this is not the case.
That's why I have to use some sort of animator.
And as I mentioned in the comments thread in the answer that was removed by its publisher, the most complex part for me here is to simulate the friction effect: the further you drag, the less the view actually moves. Just as when you're trying to drag any UIScrollView outside of it's content.
Thanks for your effort guys, but I don't think any of these 2 answers is relevant. I will try to implement this behaviour using UIDynamicAnimator whenever I have time. Probably in the nearest week or two. I will publish my approach in case I have any decent results.
EDIT (20 January):
I just uploaded a demo project to the GitHub, which includes all the transitions that I have in my project. So now you can actually have an idea why do I need to use animators and how I use them: https://github.com/demon9733/bouncingview-prototype
The only file you are actually interested in is MainViewController+Modes.swift. Everything related to transitions and animations is contained there.
What I need to do is to enable user to drag the handle area beyond "Hide" button width with a damping effect. "Hide" button will appear on swiping the handle area to the left.
P.S. I didn't really test this demo, so it can have bugs that I don't have in my main project. So you can safely ignore them.
you need to allow pan gesture to get to needed x position and at the end of pan an animation is needed to be triggered
one way to do this would be:
var initial = CGRect.zero
override func viewDidLayoutSubviews() {
initial = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initial
let open = initial.offsetBy(dx: -120, dy: 0)
// 1 manage panning along x direction
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 2 animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = closed
})
} else {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = open
})
}
}
}
Edit 20 Jan
For simulating dampening effect and make use of UIViewPropertyAnimator specifically,
var initialOrigin = CGRect.zero
override func viewDidLayoutSubviews() {
initialOrigin = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initialOrigin
let open = initialOrigin.offsetBy(dx: -120, dy: 0)
// 1. to simulate dampening
var multiplier: CGFloat = 1.0
if animatedView?.frame.origin.x ?? CGFloat(0) > closed.origin.x || animatedView?.frame.origin.x ?? CGFloat(0) < open.origin.x {
multiplier = 0.2
} else {
multiplier = 1
}
// 2. animate panning
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x * multiplier, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 3. animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = closed.origin.x
})
animate.startAnimation()
} else {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = open.origin.x
})
animate.startAnimation()
}
}
}
Here is possible approach (simplified & a bit scratchy - only bounce, w/o button at right, because it would much more code and actually only a matter of frames management)
Due to long delay of UIPanGestureRecognizer at ending, I prefer to use UILongPressGestureRecognizer, as it gives faster feedback.
Here is demo result
The Storyboard of used below ViewController has only gray-background-rect-container view, everything else is done in code provided below.
class ViewController: UIViewController {
#IBOutlet weak var container: UIView!
let imageView = UIImageView()
var initial: CGFloat = .zero
var dropped = false
private func excedesLimit() -> Bool {
// < set here desired bounce limits
return imageView.frame.minX < -180 || imageView.frame.minX > 80
}
#IBAction func pressHandler(_ sender: UILongPressGestureRecognizer) {
let location = sender.location(in: imageView.superview).x
if sender.state == .began {
dropped = false
initial = location - imageView.center.x
}
else if !dropped {
if (sender.state == .changed) {
imageView.center = CGPoint(x: location - initial, y: imageView.center.y)
dropped = excedesLimit()
}
if sender.state == .ended || dropped {
initial = .zero
// variant with animator
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
let stickTo: CGFloat = self.imageView.frame.minX < -100 ? -100 : 0 // place for button at right
self.imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: self.imageView.frame.origin.y), size: self.imageView.frame.size)
}
animator.isInterruptible = true
animator.startAnimation()
// uncomment below - variant with UIViewAnimation
// UIView.beginAnimations("bounce", context: nil)
// UIView.setAnimationDuration(0.2)
// UIView.setAnimationTransition(.none, for: imageView, cache: true)
// UIView.setAnimationBeginsFromCurrentState(true)
//
// let stickTo: CGFloat = imageView.frame.minX < -100 ? -100 : 0 // place for button at right
// imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: imageView.frame.origin.y), size: imageView.frame.size)
// UIView.setAnimationDelegate(self)
// UIView.setAnimationDidStop(#selector(makeBounce))
// UIView.commitAnimations()
}
}
}
// #objc func makeBounce() {
// let bounceAnimation = CABasicAnimation(keyPath: "position.x")
// bounceAnimation.duration = 0.1
// bounceAnimation.repeatCount = 0
// bounceAnimation.autoreverses = true
// bounceAnimation.fillMode = kCAFillModeBackwards
// bounceAnimation.isRemovedOnCompletion = true
// bounceAnimation.isAdditive = false
// bounceAnimation.timingFunction = CAMediaTimingFunction(name: "easeOut")
// imageView.layer.add(bounceAnimation, forKey:"bounceAnimation");
// }
override func viewDidLoad() {
super.viewDidLoad()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(named: "cat")
imageView.contentMode = .scaleAspectFill
imageView.layer.borderColor = UIColor.red.cgColor
imageView.layer.borderWidth = 1.0
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
container.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1).isActive = true
imageView.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1).isActive = true
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler(_:)))
pressGesture.minimumPressDuration = 0
pressGesture.allowableMovement = .infinity
imageView.addGestureRecognizer(pressGesture)
}
}

How is adding these UIButton extension functions making my app perform better?

After failing to get a live image filter on a Metal view to perform better, I was curious if my UI had some kind of impact. I have an extension on UIButton that adds a blur and shadow to them. If I comment out the stuff inside the functions, and run it, the GPU works twice as hard and the battery impact races beyond high in the debug navigator.
extension UIButton
{
func addCircularBlur(radius:CGFloat)
{
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
blur.frame = self.bounds
blur.layer.cornerRadius = radius * self.bounds.size.width
blur.clipsToBounds = true
blur.isUserInteractionEnabled = false
blur.autoresizingMask = [.flexibleRightMargin, .flexibleLeftMargin, .flexibleBottomMargin, .flexibleTopMargin, .flexibleWidth, .flexibleHeight]
self.insertSubview(blur, at: 0)
if let imageView = self.imageView{
self.bringSubview(toFront: imageView)
}
}
func addShadow()
{
self.imageView?.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
self.imageView?.layer.shadowOffset = CGSize(width:0.0, height:0.0)
self.imageView?.layer.shadowOpacity = 0.1
self.imageView?.layer.shadowRadius = 2.0
self.imageView?.layer.masksToBounds = false
self.imageView?.layer.cornerRadius = 4.0
}
}
Here's how I'm calling these functions. This example uses a button that is from Interface Builder, but it can be added to a button programmatically:
#IBOutlet weak var takePhotoBtn: UIButton!
// inside viewDidLoad:
takePhotoBtn.addCircularBlur(radius:0.5)
takePhotoBtn.addShadow()
What about this is making my device perform better when the blur and shadows are ON?

How do I create a circular collision bound with Swift UIDynamics?

This post has been modified to include a screenshot and code...
It's been four days of trying to solve this, so can someone please clearly point out what's wrong with my implementation? It's my first time lol.
Have a look at my screenshot below.
-I'm using UIKit Dynamics and am trying to trap those 3 smaller balls inside the ROUND bounds of the larger ball, but they are only being kept inside the bounds of the larger ball's rectangle.
Here's the relative code:
The large bubble is created in IB and is declared in my class:
#IBOutlet weak var homeMainBubble: UIButton!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var collision: UICollisionBehavior!
var ballOne : UIImageView!
var ballTwo : UIImageView!
var ballThree : UIImageView!
var circlePath : UIBezierPath!
override func viewDidAppear(_ animated: Bool) {
circlePath = UIBezierPath(arcCenter: CGPoint(x: homeMainBubble.frame.midX,y: homeMainBubble.frame.midY), radius: homeMainBubble.frame.width/2, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)}
In viewDidAppear(), I create a UIBezierPath()
//Adds or removes fake balls inside 'mainBubble()' when swiped up or down---------------------
func addOrRemoveFakeBalls(fakeBalls: String) {
let ballOneIs = positionOneBubble()
let ballImages = ["playBubble", "statsBubble", "settingsBubble", "cheatsBubble"]
let ballOneImage = ballImages[ballOneIs.tag - 1] //Determines the image to used for 'fake ball 1' based on the bubble in position one
var ballTwoImage : String = ""
var ballThreeImage : String = ""
if ballOneImage == "playBubble" { //Derivitive from ballOneImage, this determines the ball to the left & ensures correct array looping.
ballTwoImage = ballImages[3]
} else {ballTwoImage = ballImages[ballOneIs.tag - 2]}
if ballOneImage == "cheatsBubble" { //Derivitive from ballOneImage, this determines the ball to the right & ensures correct array looping.
ballThreeImage = ballImages[0]
} else {ballThreeImage = ballImages[ballOneIs.tag]}
switch fakeBalls {
case "Add":
ballOne = Ellipse(frame: CGRect(x: 125, y: 50, width: largeBubbleWidth, height: largeBubbleWidth))
ballOne.image = UIImage(named: ballOneImage)
ballOne.layer.zPosition = 1
ballOne.alpha = 0.3
ballTwo = Ellipse(frame: CGRect(x: 100, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballTwo.image = UIImage(named: ballTwoImage)
ballTwo.layer.zPosition = 1
ballTwo.alpha = 0.3
ballThree = Ellipse(frame: CGRect(x: 150, y: 50, width: mediumBubbleWidth, height: mediumBubbleWidth))
ballThree.image = UIImage(named: ballThreeImage)
ballThree.layer.zPosition = 1
ballThree.alpha = 0.3
homeMainBubble.addSubview(ballOne)
homeMainBubble.addSubview(ballTwo)
homeMainBubble.addSubview(ballThree)
animator = UIDynamicAnimator(referenceView: homeMainBubble)
gravity = UIGravityBehavior(items: [ballOne, ballTwo, ballThree])
gravity.magnitude = 1
animator.addBehavior(gravity)
collision = UICollisionBehavior(items: [ballOne, ballTwo, ballThree])
collision.addBoundary(withIdentifier: "Circle" as NSCopying, for: circlePath)
collision.collisionDelegate = self
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
case "Remove":
animator.removeAllBehaviors()
gravity.removeItem(ballOne)
gravity.removeItem(ballTwo)
gravity.removeItem(ballThree)
collision.removeAllBoundaries()
ballOne.removeFromSuperview()
ballTwo.removeFromSuperview()
ballThree.removeFromSuperview()
default: break
}
}
I've temporarily added collision.translatesReferenceBoundsIntoBoundary = true so that the balls don't drop off the screen, but with that commented out, why does the 'circlePath' UIBezierPath() not hold my smaller balls inside?
I have pretty much tried every approach and had every outcome EXCEPT for what I want :P.
What am I missing in order to keep the 3 smaller balls trapped inside the large ball's image (not rectangle) bounds?
THANK YOU for saving my week! :)
If you are using UIDynamics then you can follow the advice in this other question & answer
Namely, looks like you'll want to define collisionBoundingPath to be a UIBezierPath that is round.
override public var collisionBoundingPath: UIBezierPath {
let radius = min(bounds.size.width, bounds.size.height) / 2.0
return UIBezierPath(arcCenter: CGPointZero, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
}
From there you can customize the behavior of the smaller balls hitting the outer edge through the UICollisionBehaviorDelegate (again, see linked question and answer).
Update
With more code posted, another issue I think you are having is the UICollisionBehavior isn't including the "main ball". You are only adding collision behavior between the 3 sub-balls, which is likely why they are not properly interacting with the big ball as you'd want. Hopefully that helps!