UISlider glitch shows two thumbs? - swift

A UISlider in my app sometimes shows a second thumb after it is dragged. This is completely unintentional and unwanted. I've done some searching and can't find any other references to this phenomenon. Can anyone figure out why this is happening? Is this a bug?
Two thumb slider:
This code is executed when a "show options" button is pressed:
readAndApplySettings() //This sets GameSpeed from a file
var SpeedSlider = UISlider()
SpeedSlider.minimumValue = 1
SpeedSlider.maximumValue = 20
SpeedSlider.setValue(Float(21 - GameState.GameSpeed), animated: true)
SpeedSlider.continuous = true
SpeedSlider.addTarget(self, action: "SpeedSliderValueDidChange:", forControlEvents: .ValueChanged)
// Code setting frame and location omitted
OptionsLabelButton.addSubview(SpeedSlider)
This is the ValueDidChange code:
func SpeedSliderValueDidChange(sender:UISlider) {
GameState.GameSpeed = Int(21 - sender.value)
TimerInterval = Double(GameState.GameSpeed) * 0.0167
tempSpeedLabel.text = "Speed: \(21 - GameState.GameSpeed)"
}

Looks like you have created multiple UISliders, double check you are not calling your function twice. As well, if you are using storyboard to create the UISliders. Make sure you do not have two sliders on top of each other.

Related

Using UserDefaults in viewWillAppear

I'm setting up the Settings page (in SettingsView class) where users can set «On» / «Off» for the background parallax effect. The selection is saved in UserDefaults().string(forKey: "parallaxStatus"). In the viewWillAppear of ViewController class, I checked the parallaxStatus. If the status of the parallax effect is «On», then this effect is displayed. If the status is «Off», then nothing should happen.
The problem appeared when parallaxStatus changed from «On» to «Off» In this case, the parallax effect still displayed before I reload the View. But if parallaxStatus changed from «Off» to «On», the function works well without reloading the View.
Bellow is the code of viewWillAppear function. Thanks for any help or hint.
override func viewWillAppear(_ animated: Bool) {
let parallaxStatus = UserDefaults().string(forKey: "parallaxStatus")
if parallaxStatus == "On" {
let min = CGFloat(-40)
let max = CGFloat(40)
let xMotion = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.x", type: .tiltAlongHorizontalAxis)
xMotion.minimumRelativeValue = min
xMotion.maximumRelativeValue = max
let yMotion = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.y", type: .tiltAlongVerticalAxis)
yMotion.minimumRelativeValue = min
yMotion.maximumRelativeValue = max
let motionEffectGroup = UIMotionEffectGroup()
motionEffectGroup.motionEffects = [xMotion,yMotion]
bgImage.addMotionEffect(motionEffectGroup) } else { }
}
1- You should use a bool value in userDefaults
UserDefaults.standard.bool(forKey: "parallaxStatusOn") // default is false
2- viewWillAppear is called when you dimiss a presented / poped vc so in your case , you use a settingsView not vc verify it's being called by other KVO or any event driven notifier
3- if the state is on and changed to off , verify you remove the motion effects with if the vc is still appeared ( not deallocated btw do it in else of check )
bgImage.motionEffects.forEach { bgImage.removeMotionEffect($0) }

Memory Leak in UIButton extension for fadeOut animation in swift4

I built an extension to the UIButton class to do fadeOut. When I use this I get memory leak warning in the profiler. I am using Swift 4 and Xcode 9.3.
Thanks in advance for any help.
extension UIButton {
func fadeOut() {
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.duration = 0.35
fadeOut.fromValue = 1
fadeOut.toValue = 0.0
fadeOut.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
fadeOut.autoreverses = false
fadeOut.repeatCount = 0
fadeOut.isRemovedOnCompletion = true
self.layer.add(fadeOut, forKey: nil)
}
}
The calling function is given below. Also please note: new, level and card are UIButtons. When I comment out button.fadeout() in the function below the memory leak goes away as per the Xcode profiler. Hope this gives more context. If any other information is required to help analyze, I happy to provide the info.
private func menu_fadeout(){
func menu_fadeout_helper(_ button:UIButton){
button.fadeOut()
button.isHidden = true
button.isEnabled = false
}
menu_fadeout_helper(hint)
menu_fadeout_helper(new)
menu_fadeout_helper(level)
menu_fadeout_helper(card)
}
After staring at the code for a couple minutes, I see the issue. In your function. . .
private func menu_fadeout(){
func menu_fadeout_helper(_ button:UIButton){
button.fadeOut()
button.isHidden = true
button.isEnabled = false
}
menu_fadeout_helper(hint)
menu_fadeout_helper(new)
menu_fadeout_helper(level)
menu_fadeout_helper(card)
}
. . .you never directly reference the UIButtons hint, new, level, and card. Eventually, after pressing the buttons a ton of times, the memory will fill up with nothing and your app will crash. (or worse)
Change the function to this to (supposedly) remove the memory leak.
private func menu_fadeout(){
func menu_fadeout_helper(_ button: UIButton) -> UIButton {
button.fadeOut()
button.isHidden = true
button.isEnabled = false
return button
}
menu_fadeout_helper(self.hint)
menu_fadeout_helper(self.new)
menu_fadeout_helper(self.level)
menu_fadeout_helper(self.card)
}
After lots of poking around it turns out the animation layers cause leaks for various reasons - most have guesses but no precise answers.
To solve my problem I reimplemented the fadeOut function without using the CABasicAnimation and using UIView.animate and made NO other change to the code. The profiler has no issues now - all is good. Thanks!
fyi there seems to inadvertent leaks whenever using stings in the context of buttons etc. If anyone has any pointers or suggestions on that topic would appreciate it.
look for reference cycle (use weak/unowned self in capture lists)
remove all animations from layers
invalidate timer if there is one

skLabelNodes only working sometimes

So I added two label nodes to my code, and want one to be hidden while the other is shown, and then vice versa once the game begins.
class GameScene: SKScene {
// Declarations
let startLabel:SKLabelNode = SKLabelNode()
let scoreLabel:SKLabelNode = SKLabelNode()
var score = 0
override func didMoveToView(view: SKView) {
// Properties
startLabel.fontSize = 20
startLabel.fontColor = UIColor.blackColor()
startLabel.position = CGPointMake(self.frame.width*0.5, self.frame.height*0.5)
startLabel.text = "Touch Paddle To Begin"
startLabel.hidden = false
self.addChild(startLabel)
scoreLabel.fontSize = 20
scoreLabel.fontColor = UIColor.blackColor()
scoreLabel.position = CGPointMake(self.frame.width*0.5, self.frame.height*0.75)
scoreLabel.text = "\(score)"
scoreLabel.hidden = true
self.addChild(scoreLabel)
Then, when the Paddle is touched, a moveBall() function is initiated and the game begins. This is where I swapped the labels.
func moveBall() {
// Setting Labels
self.startLabel.hidden = true
self.scoreLabel.hidden = false
// Starting game
self.ball.physicsBody?.dynamic = true
However, it only works sometimes. It will work perfectly, and then without changing anything I'll run it again and only one label will show up. Then again and the other label will show up, or the startLabel will show for only a frame then disappear.
Disclaimer: I do not really know how to code, just got an idea for a game and am trying to make it a reality. Apologies if solution is something simple. Also any advice for my code would be appreciated. Thanks!
From the information given it should work correctly. However you should call the moveBall() in the touchesBegan method. Not the update method.
Also if you want it to be called if the user touches the labelNode use the location of touches began. (But I would rather use a sprite instead of a label coz labels are a bit unresponsive coz sometimes finger doesn't touch the label.
So call the moveBall() in the correct method. Give some more info to give a more accurate answer.

UIScrollView in tvOS

I have a view hierarchy similar to the one in the image below (blue is the visible part of the scene):
So I have a UIScrollView with a lot of elements, out of which I am only showing the two button since they are relevant to the question. The first button is visible when the app is run, whereas the other one is positioned outside of the initially visible area. The first button is also the preferredFocusedView.
Now I am changing focus between the two buttons using a UIFocusGuide, and this works (checked it in didUpdateFocusInContext:). However, my scroll view does not scroll down when Button2 gets focused.
The scroll view is pinned to superview and I give it an appropriate content size in viewDidLoad of my view controller.
Any ideas how to get the scroll view to scroll?
Take a look at UIScrollView.panGestureRecognizer.allowedTouchTypes. It is an array of NSNumber with values based on UITouchType or UITouch.TouchType (depending on language version). By default allowedTouchTypes contains 2 values - direct and stylus. It means that your UIScrollView instance will not response to signals from remote control. Add the following line to fix it:
Swift 4
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]
Swift 4.2 & 5
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)]
Also, don't forget to set a correct contentSize for UIScrollView:
self.scrollView.contentSize = CGSize(width: 1920.0, height: 2000.0)
Finally I solved this by setting scrollView.contentSize to the appropriate size in viewDidLoad.
You need to add pan gesture recognizer. I learned from here: http://www.theappguruz.com/blog/gesture-recognizer-using-swift. I added more code to make it not scrolling strangely, e.g. in horizontal direction.
var currentY : CGFloat = 0 //this saves current Y position
func initializeGestureRecognizer()
{
//For PanGesture Recoginzation
let panGesture: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("recognizePanGesture:"))
self.scrollView.addGestureRecognizer(panGesture)
}
func recognizePanGesture(sender: UIPanGestureRecognizer)
{
let translate = sender.translationInView(self.view)
var newY = sender.view!.center.y + translate.y
if(newY >= self.view.frame.height - 20) {
newY = sender.view!.center.y //make it not scrolling downwards at the very beginning
}
else if( newY <= 0){
newY = currentY //make it scrolling not too much upwards
}
sender.view!.center = CGPoint(x:sender.view!.center.x,
y:newY)
currentY = newY
sender.setTranslation(CGPointZero, inView: self.view)
}

iOS how to make slider stop at discrete points

I would like to make a slider stop at discrete points that represent integers on a timeline. What's the best way to do this? I don't want any values in between. It would be great if the slider could "snap" to position at each discrete point as well.
The steps that I took here were pretty much the same as stated in jrturton's answer but I found that the slider would sort of lag behind my movements quite noticeably. Here is how I did this:
Put the slider into the view in Interface Builder.
Set the min/max values of the slider. (I used 0 and 5)
In the .h file:
#property (strong, nonatomic) IBOutlet UISlider *mySlider;
- (IBAction)sliderChanged:(id)sender;
In the .m file:
- (IBAction)sliderChanged:(id)sender
{
int sliderValue;
sliderValue = lroundf(mySlider.value);
[mySlider setValue:sliderValue animated:YES];
}
After this in Interface Builder I hooked up the 'Touch Up Inside' event for the slider to File's Owner, rather than 'Value Changed'. Now it allows me to smoothly move the slider and snaps to each whole number when my finger is lifted.
Thanks #jrturton!
UPDATE - Swift:
#IBOutlet var mySlider: UISlider!
#IBAction func sliderMoved(sender: UISlider) {
sender.setValue(Float(lroundf(mySlider.value)), animated: true)
}
Also if there is any confusion on hooking things up in the storyboard I have uploaded a quick example to github: https://github.com/nathandries/StickySlider
To make the slider "stick" at specific points, your viewcontroller should, in the valueChanged method linked to from the slider, determine the appropriate rounded from the slider's value and then use setValue: animated: to move the slider to the appropriate place. So, if your slider goes from 0 to 2, and the user changes it to 0.75, you assume this should be 1 and set the slider value to that.
What I did for this is first set an "output" variable of the current slider value to an integer (its a float by default). Then set the output number as the current value of the slider:
int output = (int)mySlider.value;
mySlider.value = output;
This will set it to move in increments of 1 integers. To make it move in a specific range of numbers, say for example, in 5s, modify your output value with the following formula. Add this between the first two lines above:
int output = (int)mySlider.value;
int newValue = 5 * floor((output/5)+0.5);
mySlider.value = newValue;
Now your slider "jumps" to multiples of 5 as you move it.
I did as Nathan suggested, but I also want to update an associated UILabel displaying the current value in real time, so here is what I did:
- (IBAction) countdownSliderChanged:(id)sender
{
// Action Hooked to 'Value Changed' (continuous)
// Update label (to rounded value)
CGFloat value = [_countdownSlider value];
CGFloat roundValue = roundf(value);
[_countdownLabel setText:[NSString stringWithFormat:#" %2.0f 秒", roundValue]];
}
- (IBAction) countdownSliderFinishedEditing:(id)sender
{
// Action Hooked to 'Touch Up Inside' (when user releases knob)
// Adjust knob (to rounded value)
CGFloat value = [_countdownSlider value];
CGFloat roundValue = roundf(value);
if (value != roundValue) {
// Almost 100% of the time - Adjust:
[_countdownSlider setValue:roundValue];
}
}
The drawback, of course, is that it takes two actions (methods) per slider.
Swift version with ValueChanged and TouchUpInside.
EDIT: Actually you should hook up 3 events in this case:
yourSlider.addTarget(self, action: #selector(presetNumberSliderTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
I've just pasted my code, but you can easily see how it's done.
private func setSizesSliderValue(pn: Int, slider: UISlider, setSliderValue: Bool)
{
if setSliderValue
{
slider.setValue(Float(pn), animated: true)
}
masterPresetInfoLabel.text = String(
format: TexturingViewController.createAndSendPresetNumberNofiticationTemplate,
self.currentPresetNumber.name.uppercased(),
String(self.currentPresetNumber.currentUserIndexHumanFriendly)
)
}
#objc func presetNumberSliderTouchUp(_ sender: Any)
{
guard let slider = sender as? NormalSlider else{
return
}
setupSliderChangedValuesGeneric(slider: slider, setSliderValue: true)
}
private func setupSliderChangedValuesGeneric(slider: NormalSlider, setSliderValue: Bool)
{
let rounded = roundf((Float(slider.value) / Float(presetNumberStep))) * Float(presetNumberStep)
// Set new preset number value
self.currentPresetNumber.current = Int(rounded)
setSizesSliderValue(pn: Int(rounded), slider: slider, setSliderValue: setSliderValue)
}
#IBAction func presetNumberChanged(_ sender: Any)
{
guard let slider = sender as? NormalSlider else{
return
}
setupSliderChangedValuesGeneric(slider: slider, setSliderValue: false)
}