UISegmentedControl.noSegment stopped working with Xcode 11, iOS 13 [duplicate] - ios

This question already has answers here:
Specifying UISegmentedControlNoSegment to UISegmentedControl's selectedSegmentIndex has no Effect on iOS 13
(3 answers)
Closed 3 years ago.
I've had two segmented controls stacked on top of each other, each with two options, so there's a 2x2 grid of filtering options for a search field. This has worked fine, but I just updated to Xcode 11 and UISegmentedControl.noSegment has stopped working when I try to update it in response to user selection. However, it works when I set the initial value to .noSegment in the property observer. isMomentary is set to false. The outlets are all set up correctly. Is there some update to UISegmentedControl behavior I'm missing, or is this a bug?
New, incorrect behavior shown here.
Current code that was working before, and stopped working after update:
#IBOutlet private weak var segmentedControlOne: UISegmentedControl!
#IBOutlet private weak var segmentedControlTwo: UISegmentedControl! {
didSet {
// Start with no segment selected on this control. This works!
segmentedControlTwo.selectedSegmentIndex = -1
}
}
#IBAction private func oneIndexChanged(_ sender: UISegmentedControl) {
//Turn off selection on second control while first is selected
segmentedControlTwo.selectedSegmentIndex = UISegmentedControl.noSegment
let i = sender.selectedSegmentIndex
if i == 0 {
searchType = .users
} else {
searchType = .contributors
}
}
#IBAction private func twoIndexChanged(_ sender: UISegmentedControl) {
//Turn off selection on first control while second is selected
segmentedControlOne.selectedSegmentIndex = UISegmentedControl.noSegment
let i = sender.selectedSegmentIndex
if i == 0 {
searchType = .articles
} else {
searchType = .categories
}
}

Thanks for asking this question. I ran into the same issue, so was great to get some confirmation it wasn't just something I was missing.
While Apple hopefully fixes this bug soon, I implemented the following workaround by recreating the segments. This code sample is based on a UISegmentedControl with images as segments, you could of course implement the same approach for title strings:
public func resetSegmentedControl(_ control: UISegmentedControl) {
if #available(iOS 13, *) {
// workaround: recreate the segments
let numSegments = control.numberOfSegments
let segmentImages = (0..<numSegments).compactMap { control.imageForSegment(at: $0) }
control.removeAllSegments()
for (index, image) in segmentImages.enumerated() {
control.insertSegment(with: image, at: index, animated: false)
}
} else {
// for earlier versions of iOS, just reset the selectedSegmentIndex
control.selectedSegmentIndex = UISegmentedControl.noSegment
}
}
There's a slight flicker when removing and re-inserting the segments, but for me that's preferable to the broken state.
EDIT
As pointed out by #matt in the comment below, all that's needed is a call to setNeedsLayout,i.e.:
control.selectSegmentIndex = .noSegment
control.setNeedsLayout()

Related

Why didn't the UI update timely when using #IBAction func?

Here is the code of #IBAction func
#IBAction func touchCard(_ sender: UIButton) {
flipCount += 1
if let index = buttonCollection.firstIndex(of: sender) {
game.chooseCard(at: index)
updateViewFromModel() // I set breakpoint at here
}
}
Concentration.swift file, part of model of MVC
class Concentration {
var cards = [Card]()
var numberOfPairsOfCards: Int
var identifierOfOneAndOnlyOneCard: Int? {
didSet {
print("identifierOfOneAndOnlyOneCard: \(identifierOfOneAndOnlyOneCard)")
}
}
init(numberOfPairsOfCards: Int) {
self.numberOfPairsOfCards = numberOfPairsOfCards
for _ in 0..<numberOfPairsOfCards {
let card = Card()
cards += [card, card]
}
}
func chooseCard(at Index: Int) {
print("Index: \(Index)")
if !cards[Index].isMatched {
if let matchIndex = identifierOfOneAndOnlyOneCard, matchIndex != Index {
// check if cards match
if cards[matchIndex].identifier == cards[Index].identifier {
cards[matchIndex].isMatched = true
cards[Index].isMatched = true
}
cards[Index].isFaceUp = true
identifierOfOneAndOnlyOneCard = nil
} else {
// either no cards or 2 cards are face up
for flipDownIndex in cards.indices {
cards[flipDownIndex].isFaceUp = false
}
cards[Index].isFaceUp = true
identifierOfOneAndOnlyOneCard = Index
}
}
}
}
the code of func updateViewFromModel()
Card.swift file, part of model of MVC
struct Card {
var isFaceUp: Bool = false
var isMatched: Bool = false
var identifier = getUniqueIdentifier()
static var uniqueIdentifier: Int = 0
static func getUniqueIdentifier() -> Int{
uniqueIdentifier += 1
return uniqueIdentifier
}
}
These code are part of project concentration game from CS193p.
When I traced the code step by step, I found something confusing.
As mentioned before, I set a breakpoint at the line of
updateViewFromModel() in #IBAction func touchCard(_ sender:
UIButton)
Then I clicked 'Run' button of Xcode
The iPhone simulator came out.
the default UI image without any clicking
I clicked the first 'card'(actually, a button) from left to right in the first row
Xcode reacted and the UI remained the same as default
I started debug the code with LLDB, and I stepped into func updateViewFromModel()
When I stepped over to Line 64, it showed that the isFaceUp of the first card is true because I just clicked this card.
Let's go on, I stepped over to Line 68. Line 65 and 66 must be executed! What I think is that when executing Line 65 and 66, the
UI is supposed to change.But, why didn't the UI update timely?
I finished executing the left code in func updateViewFromModel because I didn't click any other card.
Finally it came to the end of #IBAction func touchCard, the UI still remained the same as default.
I clicked 'continue program execution' button, the UI responded correctly.I felt so weird.
What I want to figure out is that at Step 9 why didn't the UI update timely.
I will appreciate your help very much!
When you make changes on a view, it waits for a redraw cycle, thus the view won't be updated immediately. There is nothing wrong with IBAction, you put changes elsewhere and set a break point, it won't make any difference.
It may be due to slow simulators as i am also facing slow simulators issue i don't know if someone else also faced this or not, I doubt your facing this issue but i am not sure.
1- There is Nothing wrong with your code update your Simulators when working with Xcode 9.2 and simulators of IOS11.
2- Give it a try on your device

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)
}
}

Why are animated calls of UIView.isHidden = false compounded?

As you can see I'm having trouble formulating the question. Let me try to explain:
I'm using a search bar in my swift ios app. In order to get a desired animation effect I put it in a vertical stack view and then animate its isHidden property. This way search bar pushes down other children of the stack view as it animates in or pulls them up as it animates out. So far so good.
I've noticed a behavior that I think is strange. Could be a bug or could be me not understanding how things work. Basically if I call search bar hiding method x times in a row I need to call search bar showing method x times before it would show. I'd expect to have to make just one call to show search bar regardless of how many times I called hiding method. The issue doesn't exist other way around: if I call search bar showing code x times I only need to call hiding method once for it to go away. This doesn't happen if I set isHidden without animating it...
Here's a sample code and a video of the issue. I'd appreciate it if someone would help me understand this behavior.
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar! {
didSet {
searchBar.isHidden = true
}
}
#IBAction func showAction(_ sender: UIButton) {
expandSearch()
}
#IBAction func hideAction(_ sender: UIButton) {
collapseSearch()
}
private func expandSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = false
}
}
private func collapseSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = true
}
searchBar.resignFirstResponder()
}
}
You should not call an asynchronous animation of searchbar x times, instead of I suggest you to keep its state in variable, something like isSearchBarHidden,
and check it before show/hide search bar. You could use just one method with such signature showSearchBar(show: Bool) and setting this variable there.
#IBAction func showAction(_ sender: UIButton) {
showSearchBar(true)
}
#IBAction func hideAction(_ sender: UIButton) {
showSearchBar(false)
}
private
func showSearchBar(_ show: Bool) {
guard isSearchBarHidden != show else {
return
}
UIView.animate(withDuration: 0.3, animations: {
self.searchBar.isHidden = show
}) {
self.isSearchBarHidden = show
if !show && searchBar.isFerstResponder {
searchBar.resignFirstResponder
}
}
}
private
var isSearchBarHidden: Bool = true
Also it is a good practice to check if your textView/textField/searchBar isFirstResponder before call resignFirstResponder.
Hope it will help. Good luck!

IOS Segmented Control: Can't Change Selected Segment

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.

Button in Xcode doesn't change a variable - Swift

I am trying to change the value of a variable in Xcode 7 with Swift. For some strange reason, it just doesn't work.
Here are the functions I used.
#IBAction func ten(sender: AnyObject) {
var percentage = 0.10
print("test")
}
#IBAction func twentyBtn(sender: AnyObject) {
var percentage = 0.20
}
#IBAction func fifteenBtn(sender: AnyObject) {
var percentage = 0.15
}
#IBAction func twentyfiveBtn(sender: AnyObject) {
var percentage = 0.25
}
#IBAction func thirtyBtn(sender: AnyObject) {
var percentage = 0.30
}
Then later in the code I use the percentage variable in this function.
#IBAction func calcTip(sender: AnyObject) {
var billAmount: Int? {
return Int(billField.text!)
}
var tipTotal = percentage * Double((billAmount)!)
toalLabel.text = String(tipTotal)
testLabel.text = String(percentage)
}
I don't understand why this shouldn't work because I also have this variable underneath my outlets where at the beggining of the code.
What's even more confusing is that if I try to see if it the button would print into the output, as you see I've tried for the one of the buttons, IT WORKS. Even trying to change the text within a label works, but for some strange reason it won't change a variable.
I'm not sure if this could be a bug with Xcode either.
Declare and initialize your percentage variable outside of your IBAction functions.
Like Mukesh already explained in the comments, if you are writing var percentage = 0.10 within one of these functions, you are not assigning 0.10 to the correct variable. Instead, you are creating a new variable within the scope of that function.
Any code written outside of that function cannot access it, and unless you are returning this value or passing it to a different function, the variable will be deleted by the garbage collector after the program finishes executing that function (so pretty much immediately).
Long story short, your code should look something like this instead:
var percentage = 0;
#IBAction func ten(sender: AnyObject) {
percentage = 0.10
print("test")
}
#IBAction func calcTip(sender: AnyObject) {
var billAmount: Int? {
return Int(billField.text!)
}
var tipTotal = percentage * Double((billAmount)!)
toalLabel.text = String(tipTotal)
testLabel.text = String(percentage)
}

Resources