IOS Segmented Control: Can't Change Selected Segment - ios

I created a segmented control in my storyboard that looks like this:
Then I created an IBAction for when the control's value changes:
#IBAction func segmentValChanged(_ sender: UISegmentedControl) {
print("touched")
if (sender.selectedSegmentIndex == 0) {
sender.selectedSegmentIndex = 1;
}
else{
sender.selectedSegmentIndex = 0;
}
}
The only issue is that this function is never called!
When I click the Jokes (Beta) button, nothing happens.
However, when I created an IBOutlet for the control and tried to change the selected segment in my viewDidLoad(), it worked:
segmentedController.selectedSegmentIndex = 1
I know I probably missed something obvious, since I've never used a segmented control before.
Thanks so much if anyone can point out what this is.
Cheers!
import UIKit
import Alamofire
class ViewController: UIViewController {
var currentImage: CurrentImage!
var parameters: Parameters!
#IBOutlet weak var segmentedController: UISegmentedControl!
#IBAction func segmentValChanged(_ sender: UISegmentedControl) {
print("touched")
if (sender.selectedSegmentIndex == 0) { // switch to 1
sender.selectedSegmentIndex = 1;
}
else{
sender.selectedSegmentIndex = 0;
}
}
override func viewDidLoad() {
super.viewDidLoad()
segmentedController.selectedSegmentIndex = 1
self.segmentedController.addTarget(self, action: #selector(segmentValChanged(_:)), for: .valueChanged)
}
}
And this is what it looks like when I right click the view:

Whenever I see this it is almost always the same problem... If the segmented control is being drawn outside of its parent view, then it will not respond to taps. This is most likely the problem you are having. (This can also happen with UIButtons.)
Ways to test if this is the problem:
set clipsToBounds of the segmented control's parent to true. If the control no longer shows on the screen, then it is being drawn outside of it's parent view. segmentedControl.superview?.clipsToBounds = true
add a border to the segmented control's parent. If the control is being drawn outside the border then there you go. segmentedControl.superview?.layer.borderWidth = 1
Solve the problem by expanding the size of the parent view so the control is being drawn within it.
On a side note, your IBAction is incorrect. When the value changed action is called, the segmented control will already have changed its value and the new segment will be highlighted. You don't have to do it manually. This is another indicator that your control isn't being drawn in the bounds of its parent view.

Related

How do I change the color of two UIButtons when I press them?

I am trying to use two buttons to toggle between an animated sliding view. When the UIView loads, I want button1 to be UIColor.darkGrey and button2 to be UIColor.lightGrey. Then, when I press button2 I want button2 to become UIColor.darkGrey and button1 to become UIColor.lightGrey. If I press button1, I want button1 to be UIColor.darkGrey and button2 to be UIColor.lightGrey.
It seems simple; Using the storyboard, I connected a UIButton for button1 and button2 as an outlet. Then I connected each as actions. In each of the actions, I included the following code:
#IBAction func button1Action(_ sender: UIButton) {
button2.titleLabel?.textColor = UIColor.lightGray
button1.titleLabel?.textColor = UIColor.darkGray
UIView.animate(withDuration: 1){
self.side1.constant = 0
self.side2.constant = 0
self.sideA.constant = 400
self.sideB.constant = -400
self.view.layoutIfNeeded()
}
}
#IBAction func button2Action(_ sender: UIButton) {
button1.titleLabel?.textColor = UIColor.lightGray
button2.titleLabel?.textColor = UIColor.darkGray
view.layoutIfNeeded()
UIView.animate(withDuration: 1){
self.side1.constant = -400
self.side2.constant = 400
self.sideA.constant = 0
self.sideB.constant = 0
self.view.layoutIfNeeded()
}
}
When I press button1, everything works as expected; However, whenever I press button2 both buttons are UIColor.lightGrey. Am I missing something obvious?
You get some methods "for free" with buttons to manage their state. One of them is isSelected. Another is the tag property, so you can figure out which button is which. Since you've only got two buttons, you can get away with just using isSelected to figure out which is which. You can also use computed vars to make your life easier. With those things in mind, here's one approach you could utilize to managing the buttons' states:
Declare a buttons computed var, like so
#IBOutlet var firstButton: UIButton!
#IBOutlet var secondButton: UIButton!
// computed var to access your buttons
private var buttons: [UIButton] {
return [firstButton, secondButton]
}
Set up your buttons in viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
// one setup to configure each button for selected or not selected
buttons.forEach { button in
button.setTitleColor(.darkGray,
for: .selected)
button.setTitleColor(.lightGray,
for: .normal)
}
firstButton.isSelected = true
}
func setTitleColor(_ color: UIColor?, for state: UIControl.State) will remain in effect for the lifetime of the button, so you don't need to fiddle with it after the initial declaration (unless you want to change the button's behavior later).
One #IBAction for both buttons and utilize the tag property to figure out which one is which. Since you've only got two buttons, I'm just using isSelected. Here's what your single #IBAction would look like:
#IBAction func buttonAction(_ sender: UIButton) {
UIView.animate(withDuration: 1.0) {
// flip buttons
self.buttons.forEach { button in
button.isSelected.toggle()
}
if self.firstButton.isSelected {
// tweak your constraints for firstButton.isSelected == true
// tweak your constraints for secondButton.isSelected == false
} else {
// tweak your constraints for firstButton.isSelected == false
// tweak your constraints for secondButton.isSelected == true
}
}
}
Based on your current implementation, you'll need to right click on the UIButtons on storyboard and nuke the existing IBAction connections and reconnect both buttons to the method above.

How to Focus Accessibility On A Particular Segment in A UISegmentedControl

OK. This answer helps a lot. I can select an accessibility item when a screen is shown. I simply add
UIAccessibility.post(notification: .layoutChanged, argument: <a reference to the UI item to receive focus>)
to the end of my viewWillAppear() method, and the item receives focus.
However, in one of my screens, the item I want to receive focus is a UISegmentedControl, and, when focused, it always selects the first item, no matter which one is selected. Since I followed the excellent suggestion here, I have an accessibility label for each item in the control, and I'd like my focus to begin on whichever segment is selected.
Is there a way to do this? As a rule, I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Thanks!
UPDATE: Just to add insult to injury, I am also having an issue with the item I want selected being selected, then a second later, the screen jumps the selection to the first item. That's probably a topic for a second question.
I created a blank project as follows to reproduce the problem:
The solution is taking the selectedIndex to display the selected segment and providing the appropriate segment object for the VoiceOver notification: easy, isn't it?
I naively thought that getting the subview in the segmented control subviews array with the selectedIndex would do the job but that's definitely not possible because the subviews can move inside this array as the following snapshot highlights (red framed first element for instance):
The only way to identify a unique segment is its frame, so I pick up the segmented control index and the frame of the selected segment to pass them to the previous view controller.
That will allow to display (index) and read out (frame that identifies the object for the notification) the appropriate selected segment when this screen will appear after the transition.
Hereafter the code snippets for the view controller that contains the 'Next Screen' button:
class SOFSegmentedControl: UIViewController, UpdateSegmentedIndexDelegate {
var segmentIndex = 0
var segmentFrame = CGRect.zero
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let segueName = segue.identifier {
if (segueName == "SegmentSegue") {
if let destVC = segue.destination as? SOFSegmentedControlBis {
destVC.delegate = self
destVC.segmentIndex = segmentIndex
destVC.segmentFrame = segmentFrame
}
}
}
}
#IBAction func buttonAction(_ sender: UIButton) { self.performSegue(withIdentifier: "SegmentSegue", sender: sender) }
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect) {
segmentIndex = index
segmentFrame = frame
}
}
... and for the view controller that displays the segmented control:
protocol UpdateSegmentedIndexDelegate: class {
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect)
}
class SOFSegmentedControlBis: UIViewController {
#IBOutlet weak var mySegmentedControl: UISegmentedControl!
var delegate: UpdateSegmentedIndexDelegate?
var segmentFrame = CGRect.zero
var segmentIndex = 0
var segmentFrames = [Int:CGRect]()
override func viewDidLoad() {
super.viewDidLoad()
mySegmentedControl.addTarget(self,
action: #selector(segmentedControlValueChanged(_:)),
for: .valueChanged)
mySegmentedControl.selectedSegmentIndex = segmentIndex
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(mySegmentedControl.subviews)
let sortedFrames = mySegmentedControl.subviews.sorted(by: { $0.frame.origin.x < $1.frame.origin.x})
for (index, segment) in sortedFrames.enumerated() { segmentFrames[index] = segment.frame }
if (self.segmentFrame == CGRect.zero) {
UIAccessibility.post(notification: .screenChanged,
argument: mySegmentedControl)
} else {
mySegmentedControl.subviews.forEach({
if ($0.frame == self.segmentFrame) {
UIAccessibility.post(notification: .screenChanged,
argument: $0)
}
})
}
}
#objc func segmentedControlValueChanged(_ notif: NSNotification) {
delegate?.updateSegmentIndex(mySegmentedControl.selectedSegmentIndex,
withFrame: segmentFrames[mySegmentedControl.selectedSegmentIndex]!) }
}
The final result is as follows:
Double tap to go to the next screen.
Select the next element to focus the second segment.
Double tap to select the focused element.
Get back to the previous screen thanks to the Z gesture natively known by iOS with the navigation controller. The delegate passes the index and the frame of the selected segment.
Double tap to go to the next screen.
The segment that was formerly selected is read out by VoiceOver and still selected.
You can now Focus Accessibility On A Particular Segment in A UISegmentedControl following this rationale.
I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Unfortunately, this solution is a hacky one... sorry. However, it works and I couldn't find another one anywhere else: see it as a personal fix unless you get a cleaner one to share? ;o)
UPDATE... That's probably a topic for a second question.
I can't reproduce the behavior of your update: if you create a dedicated topic for this problem, please add the most detailed code and context so as to provide the most accurate solution.
i think this works~!
class VC {
let segment = UISegmentedControl()
func fucusSegment(index: Int) {
let item = segment.accessibilityElement(at: index )
UIAccessibility.post(notification: .layoutChanged, argument: item)
}
}

IOS Swift 4 UIView isHidden with tapGestureRecognizer

I got a weird problem.
I made a simple Single View App with a TabBarController, added Subviews to the first Item (ViewController) and made them hidden with the interface builder properties.
Now I want to recognize taps within the entire view to make them visible one after another and hide them again in the end.
The gestureRecogniser is placed from the interface builder. All outlets are properly connected and do work.
Anyway, when I tap the first time the UIViews won't show but the isHidden property is set correctly. I've tried many things and searched SO but nothing really helped in this case. Force-Wrapping everything into the main thread doesn't help either and shouldn't make a difference in this case anyway.
This is my code:
//
// ViewController.swift
//
import UIKit
class ViewController: UIViewController {
var step = 0
#IBOutlet weak var firstViewOutlet: UIView! // properly connected
#IBOutlet weak var secondViewOutlet: UIView! // properly connected
#IBOutlet weak var thirdViewOutlet: UIView! // properly connected
#IBOutlet var tapRecognizer: UITapGestureRecognizer! // properly connected
override func viewDidLoad() {
super.viewDidLoad()
self.tapRecognizer.addTarget(self, action: #selector (self.tapAction(_:)))
print("viewDidLoad")
// Tried to hide them manually on load. Didn't work so I switched back to the interface builder property (checkbox)
// hideSubviews()
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
}
#objc func tapAction(_ sender:UITapGestureRecognizer){
print("Tapped: \(self.step)") // Always works and shows the correct step value
switch self.step {
case 0:
print("Untap should show! \(self.step)") // Shows after first tap
DispatchQueue.main.async {
self.firstViewOutlet.isHidden = false
print("Value of first view state: \(self.firstViewOutlet.isHidden)") // shows false but visible after SECOND tap
self.step += 1
}
// Alternative doesn't change anything
// self.firstViewOutlet.isHidden = false
case 1:
print("Upkeep should show! \(self.step)") // Shows second first tap
DispatchQueue.main.async {
self.secondViewOutlet.isHidden = false
print("Value of second view state: \(self.secondViewOutlet.isHidden)") // shows false but visible after THIRD tap
self.step += 1
}
// Alternative doesn't change anything
// self.secondViewOutlet.isHidden = false
case 2:
print("Draw should show! \(self.step)") // Shows after third tap
DispatchQueue.main.async {
self.thirdViewOutlet.isHidden = false
print("Value of third view state: \(self.thirdViewOutlet.isHidden)") // shows false but visible after FOURTH tap
self.step += 1
}
// Alternative doesn't change anything
// self.thirdViewOutlet.isHidden = true
case 3:
print("Draw should clear! \(self.step)")
hideSubviews() // works after FIFTH tap
// Alternative doesn't change anything
// self.firstViewOutlet.isHidden = true
// self.secondViewOutlet.isHidden = true
// self.thirdViewOutlet.isHidden = true
// self.step = 0
default:
return
}
}
func hideSubviews(){
DispatchQueue.main.async {
self.thirdViewOutlet.isHidden = true
self.firstViewOutlet.isHidden = true
self.secondViewOutlet.isHidden = true
self.step = 0
}
}
}
Funny enough, after the first cicle everything works as espected.
I even tried to add view.layoutIfNeeded(), view.layoutSubviews(), view.setNeedsDisplay() and view.setNeedsLayout() to the outlets and to self.view but it didn't change anything either.
Any suggestions are appreciated. I bet it's a small and stupid bug that I just can't see.
It seems it's any UI change. Even changing the labels colors doesn't work on the first tap.
Steps to reproduce:
Create a new project (Single View Application).
Click ViewController, Editor->Embed in->Tap Bar Controller
Click Library icon, drag Container view to ViewController
Set [hidden] property for this Container view to true in Interface Builder
Create Outlet from Container View to ViewController
Click Library icon, drag Tap Gesture Recognizer to ViewController (not into container view!)
Create Outlet from Container View to ViewController
Copy my code or write from scratch

Customize a button and its highlighted state in Swift3 using a class

I'm new to Swift and I assume this is a fundamental question to programming for iOS.
I have three buttons in my storyboard and I want to customize how those buttons look if pressed once, twice and three times.
I also have three themes (pink, blue and orange). What I thought of doing is to create three new classes called pink,blue and orange.swift
I don't want to create them programmatically, only style them programmatically.
What I lack to understand is how do I call the function (Example: "ButtonIsPressed") from my pink.swift class into my #IBAction and #IBOutlet in the main view controller that is also object oriented (ie. I don't want to create a function for every button)?
I can't really find a decent and up-to-date Swift 3 Tutorial for this, any help or advice on this topic will be greatly appreciated.
Why can it not be as simple as?:
#IBAction func buttonPressed(_ sender: UIButton!) {
self.backgroundColor = myPinkCGolor
}
I think shallowThought's answer will work for changing backgroundColor based on button state of a specifically named IBOutlet.
I have three buttons in my storyboard and I want to customize how those buttons look if pressed once, twice and three times.
If you want to maintain "state", as in have a "counter" for how many times a button's been clicked or tapped, you can use the "tag" property of the button. Set it to zero, and in your IBAction functions increment it. (Like shallowThought said, use .touchUpInside and .touchDown for the events.)
Also, you have one minor - but important! - thing wrong in your code Brewski:
#IBAction func buttonPressed(_ sender: UIButton!) {
self.backgroundColor = myPinkCGolor
}
Should be:
#IBAction func buttonPressed(_ sender: UIButton!) {
sender.backgroundColor = myPinkCGolor
}
So combining everything - up vote to shallowThought (also, changing his AnyObject to UIButton and making it Swift 3.x syntax on the UIColors - and would end up with this. Note that there is no need for an IBOutlet, and you can wire everything up in IB without subclassing:
// .touchUpInside event
// can be adapted to show different color if you want, but is coded to always show white color
#IBAction func buttonClicked(sender: UIButton) {
sender.backgroundColor = UIColor.whiteColor()
}
// .touchDown event
// will show a different color based on tap counter
#IBAction func buttonReleased(sender: UIButton) {
switch sender.tag {
case 1:
sender.backgroundColor = UIColor.blue
case 2:
sender.backgroundColor = UIColor.red
case 3:
sender.backgroundColor = UIColor.green
default:
sender.backgroundColor = UIColor.yellow
}
sender.tag += 1
}
There is no methode to set the backgroundColor for a certain state, like there is for other UIButton properties, so you have to listen to the buttons actions:
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
#IBAction func buttonClicked(sender: AnyObject) { //Touch Up Inside action
button.backgroundColor = UIColor.whiteColor()
}
#IBAction func buttonReleased(sender: AnyObject) { //Touch Down action
button.backgroundColor = UIColor.blueColor()
}
...
}
or set a unicolor image withimage:UIImage, forState:.selected.

How to set selected segment index in UISegmentedControl?

I'm trying to avoid an app crash here... I have a button that will remove a segment from a UISegmentedControl. If that button is pressed and the user has the segment to be removed selected, the segment will remove and no selection will be highlighted. However, when another button is pushed that does an action that retrieves the selectedSegmentIndex, the app crashes.
In short: Is there any way to force the selection of a segment in a UISegmentedControl?
edit it seems as though the UISegmentedControl is returning a selectedSegmentIndex of -1 when no segment is selected... let's see what I can do from here.
Use yourSegmentname.selectedSegmentIndex = 1; or whichever segment you want.
This code is for swift 2.0
#IBOutlet weak var segmentcontroll: UISegmentedControl!
#IBAction func segmentneeded(sender: AnyObject)
{
if(segmentcontroll.selectedSegmentIndex==0)
{
self.view.backgroundColor=UIColor.purpleColor()
segmentcontroll.selectedSegmentIndex=UISegmentedControlNoSegment
}
else if(segmentcontroll.selectedSegmentIndex==1)
{
self.view.backgroundColor=UIColor.yellowColor()
segmentcontroll.selectedSegmentIndex=UISegmentedControlNoSegment
}
else
{
self.view.backgroundColor=UIColor.grayColor()
segmentcontroll.selectedSegmentIndex=UISegmentedControlNoSegment
}
}

Resources