I have a UISlider and I am trying to make the position go when the user taps to a certain time, instead of moving the thumb.
I tried to work it through this topic and this answer and I came to this approach. This is what I tried:
var slider: UISlider! // and maxValue, etc added in viewDidLoad
func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
var pointTapped: CGPoint = gestureRecognizer.locationInView(self.view)
var positionOfSlider: CGPoint = slider.frame.origin
var widthOfSlider: CGFloat = slider.frame.size.width
var newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(slider.maximumValue) / widthOfSlider)
slider.setValue(Float(newValue), animated: true)
}
But, it is not letting me anywhere on slider and get the tapped value. It only lets me hold the thumb and slide it, but not tapping.
Well the error is legit. If you check the operands for setValue you'll see several possible calling sequences. The one you probably want expects a Float (as opposed to a CGFloat) - but also requires an animation: boolean flag.
Try something like:
let floatNewValue = Float(newValue)
durationSlider.setValue(floatNewValue,animated: true)
UISlider's method is func setValue(_ value: Float, animated animated: Bool).
You'll need to cast your CGFloat to a Float.
So durationSlider.setValue(Float(newValue), animated: true)
Related
I have a CALayer with a custom animation on it, working via an #NSManaged property, and overriding:
class func defaultValue(forKey key: String) -> Any?
class func needsDisplay(forKey key: String) -> Bool
func action(forKey key: String) -> CAAction?
func display()
However, I sometimes want to bypass the animation and have the property immediately step to the new value. In my CALayer sub-class I tried this:
#NSManaged private var radius: CGFloat
func animate(to radius: CGFloat) {
self.radius = radius
}
func step(to radius: CGFloat) {
// Inspired by https://stackoverflow.com/a/34941743
CATransaction.begin()
CATransaction.setDisableActions(true) // Prevents animation occurring
self.radius = radius
CATransaction.commit()
setNeedsDisplay() // Has no effect
}
When animate(to:) is called, display() is called repeatedly and my custom drawing code can do it's thing. When step(to:) is called, the CATransaction code does prevent an animation from occurring, but no drawing is ever performed at all.
I can get it to behave as desired, but it feels quite hacky:
func step(to radius: CGFloat) {
// func action(forKey key: String) -> CAAction? uses .animationDuration
// when constructing a CABasicAnimation
let duration = animationDuration
defer { animationDuration = duration }
animationDuration = 0
self.radius = radius
}
What is the correct method to give the caller the ability to choose whether the property animates from one value to the next, or steps immediately? A subsequent change to radius should respect the previous value, whether it was stepped or animated to.
You say you have implemented action(forKey:) (quite rightly). So on those occasions when you don't want this property to be animated, return nil from that method. The drawing will still take place, but without animation.
Alternatively, you could return super.action(forKey:key). That might be a little more sane, but the outcome is the same.
You may ask (and I hope you do): How can I throw some kind of switch that action(forKey:) can consult in order to know which kind of occasion this is? One possibility is to set a property of the layer using key-value coding.
CALayer has a wonderful feature that you are allowed to call setValue(_:forKey:) or value(forKey:) for any key; it doesn't have to be a "real" key that already exists.
So you could call setValue(false, forKey:"shouldAnimate") on the layer before setting the property. And your action(forKey:) can then consult value(forKey:"shouldAnimate"), and see whether it is false (as opposed to true or nil) — and if it is, it returns nil to prevent the animation.
I have a UISlider that can seek in music.
Customer requests the value change of this slider not to be announced by VoiceOver.
Now the default behaviour of VoiceOver for UISlider is to announce the percent value and lower the music volume for that time. This is not good for me.
If I change the accessibilityValue to #"", then it makes a sound effect and also lowers the music volume. This is also bad.
I tried using the UIAccessibilityTraitStartsMediaSession and the UIAccessibilityTraitPlaysSound accessibility traits, but they don't effect this behavior.
What should I do?
Using the native UISlider is a very good practice but not in your specific use case because you'll always have the sound effect you noticed when its value changes.
I suggest to create a custom accessibility element in a blank project as follows :
First, create your slider in the Xcode interface builder with an outlet connection to your view controller.
Implement a UIAccessibilityElement subclass that will represent your slider.
class a11yMySlider: UIAccessibilityElement {
var minimumValue = 0.0
var maximumValue = 10.0
var value = 5.0
var theSlider = UISlider()
init(in container: Any, with slider: UISlider) {
super.init(accessibilityContainer: container)
theSlider = slider
}
override var accessibilityTraits: UIAccessibilityTraits {
get { return UIAccessibilityTraitAdjustable }
set { }
}
override func accessibilityDecrement() {
value -= (value == minimumValue) ? 0.0 : 1.0
theSlider.value = Float(value)
}
override func accessibilityIncrement() {
value += (value == maximumValue) ? 0.0 : 1.0
theSlider.value = Float(value)
}
}
Introduce your accessibility element in your view controller to simulate the physical slider with VoiceOver.
class SliderNoSoundViewController: UIViewController {
#IBOutlet weak var mySlider: MySlider!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let a11yElt = a11yMySlider.init(in: self.view, with: mySlider)
a11yElt.accessibilityFrame = mySlider.frame
self.view.accessibilityElements = [a11yElt]
}
}
I let you adapt the incoming parameters and the connection to the music playback inside your project but, as is, the slider value changes are not vocalized by VoiceOver as desired.
Moreover, illustrations and code snippets (ObjC and Swift) are also available if you need more information to complete your implementation with VoiceOver.
I need present UICollectionView, with a different element (not only first) without animation.
example
my code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
var newPoint = self.productCollectionView.contentOffset
newPoint.x = (UIScreen.main.bounds.width * CGFloat(self.currentPage)) + 1
self.productCollectionView.contentOffset = newPoint
}
}
Instead of setting contentOffset, call the setContentOffset(_:, animated:) method with an animated: value of false.
Also I would suggest removing the async and moving your code to viewDidLayoutSubviews. That way, your code will run before the user sees anything, but after the initial layout of the view has been achieved. This method can be called many times, so you will have to use a Bool instance property to make sure it only runs once.
I am a beginner in iOS development. I have done everything on Assignment 3 (Graphing Calculator) of the 2016 Stanford CS193P iOS development course on iTunes U (gestures, view controllers, views, segues etc.) apart from actually plotting the x vs. y graph. I am quite confused about where and how to put my code to do this; and where the drawRect is going to get the y value from. I have searched the Internet for a solution for ages. I'm assuming the model to the graph view controller is the CalculatorBrain or maybe the program of the brain but I'm not quite sure as to how the view is going to talk to this controller from the drawRect function to get the y value for a point x. If you could put me on the right track that would be very helpful. I'll paste my graph view controller and graph view code below. Thanks in advance.
//GraphViewController
import UIKit
class GraphViewController: UIViewController {
private var brain = CalculatorBrain()
var program: [AnyObject]?
#IBOutlet weak var graphView: GraphingView! {
didSet {
graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector (GraphingView.changeScale(_:))))
graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(GraphingView.panView(_:))))
graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(GraphingView.doubleTapOrigin(_:))))
}
}
}
//GraphingView
import UIKit
#IBDesignable
class GraphingView: UIView {
#IBInspectable var scale: CGFloat = 50 { didSet { setNeedsDisplay() } }
#IBInspectable var linewidth: CGFloat = 2 { didSet { setNeedsDisplay() } }
var origin: CGPoint! { didSet { setNeedsDisplay() } }
/* Gesture Code
func changeScale (recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .Changed,.Ended:
scale *= recognizer.scale
recognizer.scale = 1.0
default:
break
}
}
func panView (recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .Changed,.Ended:
origin.x += recognizer.translationInView(self).x
origin.y += recognizer.translationInView(self).y
recognizer.setTranslation(CGPoint(x:0,y:0), inView: self)
default:
break
}
}
func doubleTapOrigin (recognizer: UITapGestureRecognizer) {
recognizer.numberOfTapsRequired = 2
switch recognizer.state {
case .Ended :
origin = CGPoint(x: recognizer.locationInView(self).x, y:recognizer.locationInView(self).y)
default: break
}
}
*/
var axesDrawer = AxesDrawer()
override func drawRect(rect: CGRect) {
if origin == nil {
origin = CGPoint(x: bounds.midX, y: bounds.midY)
}
axesDrawer.drawAxesInRect(rect, origin: origin, pointsPerUnit: scale)
var pixelX = 0
while pixelX <= bounds.maxX {
//do some code to get value of y for current x from the brain in the view controller and plot it with UIBezierPath
pixelX += 1/contentScaleFactor
}
}
}
Updated Answer - Using a Function Pointer
The previous answer is one way to do it using a data source. It is modeled after the way a UITableViewController gets its data.
Reading though the assignment, it seems the Prof wants you to use an optional function pointer instead.
Do the following:
Create an optional variable in the GraphingView to hold a function pointer:
var computeYForX: ((Double) -> Double)?
Apply didSet to that variable and have it call self.setNeedsDisplay() when it is set. This will tell iOS that drawRect needs to be called.
In your calculator when it is time to draw, set the computeYForX property of the GraphingView to a (Double) -> Double function in your GraphViewController. If you want to remove the function (for instance, when someone resets the calculator), set the property to nil.
In drawRect, check to make sure computeYForX is not nil before using it to graph the function. Use guard or if let to safely unwrap the function before using it.
Previous Answer - Using a DataSource
You need to add a Data Source to your GraphingView. First define a protocol called GraphDataSource:
protocol GraphDataSource: class {
func computeYforX(x: Double) -> Double
}
Add a dataSource property to the GraphingView:
class GraphingView: UIView {
weak var dataSource: GraphDataSource?
Have your GraphViewController implement that protocol by adding GraphDataSource to the class definition line, by implementing computeYForX(), and by setting itself as the dataSource in didSet for graphView:
class GraphViewController: UIViewController, GraphDataSource {
private var brain = CalculatorBrain()
var program: [AnyObject]?
#IBOutlet weak var graphView: GraphingView! {
didSet {
graphView.dataSource = self
graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector (GraphingView.changeScale(_:))))
graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(GraphingView.panView(_:))))
graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(GraphingView.doubleTapOrigin(_:))))
}
}
func computeYForX(x: Double) -> Double {
// call the brain to calculate y and return it
}
}
Then in drawRect, call computeYForX() on the dataSource when you need a y value:
let y = dataSource?.computeYForX(x)
I used existing functionality in calculator brain. Specifically, inside brain, I created a public method that made a calculation based on a program, stored as any object through the property list typecast (we did that in the save/restore part). All the commands were there - I just packaged it in a method. I use this method for a lot of stuff - so I never rewrite code to evaluate a sequence of operands.
In the public interface, that is just a function that is accessible from any instance of brain. In particular, in preparing for segue to the graph view, I pointed the graph view controller to this function. I also passed the program existing in the brain at that moment, so that any expression valid in the calculator can be plotted. The latter was a goal for me because it makes sense. I wanted to avoid rewriting the function that interprets the operands.
The graph view requires a function (can be brain or any other specification) and a "program" that is required in order to plot expressions. So it is a general class requiring a specific form for describing the operations. I take that as a perfectly "legal" solution.
Drawrect then used the function and the program to draw the curves.
I want to rotate a UIImage, I have managed to do so with the code below, however when I press the rotate button again, the image does not rotate anymore, could someone please explain why?
#IBAction func rotate(sender: UIButton) {
UIView.animateWithDuration(0.2, animations: {
self.shape.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4) * 2)
})
}
You are changing your shape image view's transform to a new, fixed value. If you tap on it again, the transform already has that value. You set the transform to the same value again, which doesn't change anything.
You need to define an instance variable to keep track of the rotation.
var rotation: CGFloat = 0
#IBAction func rotate(sender: UIButton)
{
UIView.animateWithDuration(0.2, animations:
{
self.rotation += CGFloat(M_PI_4) * 2 //changed based on Daniel Storm's comment
self.shape.transform = CGAffineTransformMakeRotation(rotation)
})
}
That way, each time your tap the button you'll change the rotation variable from it's previous value to a new value and rotate to that new angle.