I made my very first iOS app. But there are two annoying bugs which I cannot get rid of. I hope somebody can help me!
The app is supposed to train to read musical notation. The user specifies his instrument and level (on the previous viewcontroller) and based on that, it places random notes in musical notation on the screen. The user should match those notes in textfields and the app keeps track of the score and advances a level after ten right answers.
However, somehow I'm having problems with the function which generates the random notes. The function for some reason gets called twice, the first time it generates the notes, saves them in a global variable and creates the labels with the notes. The second time, it changes the global variable but not the labels. It returns the following error message this time: 2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
Because of this, the user answers the question on the screen, but the app thinks it's the wrong answer, because it has the second answer stored.
The second time the user answers a question, the function is only called once, but the read-out from the text fields doesn't update to the new values, but keeps the same as with the first question.
Here is the code which gives the problems:
import UIKit
class ThirdViewController: UIViewController
{
// snip
func setupLabels() {
// snip
// here the random notes are created, this is function is called multiple times for some reason
let antwoord = Noten()
let antwoordReturn = antwoord.generateNoten(instrument: instrument, ijkpunt: ijkpunt, aantalNoten: aantalNoten-1)
let sleutel = antwoordReturn.0
let heleOpgave = antwoordReturn.1
print(heleOpgave)
print(PassOpgave.shared.category)
let heleOpgaveNummers = antwoordReturn.2
// snip
var a = 0
while a < aantalNoten {
// the labels are created, no problems there
let myTekstveld = UITextField(frame: CGRect(x: labelX, y: labelY + 150, width: labelWidth, height: labelHeight / 2))
myTekstveld.backgroundColor = UIColor.white
myTekstveld.textAlignment = .center
myTekstveld.placeholder = "?"
myTekstveld.keyboardType = UIKeyboardType.default
myTekstveld.borderStyle = UITextField.BorderStyle.line
myTekstveld.autocorrectionType = .no
myTekstveld.returnKeyType = UIReturnKeyType.done
myTekstveld.textColor = UIColor.init(displayP3Red: CGFloat(96.0/255.0), green: CGFloat(35.0/255.0), blue: CGFloat(123.0/255.0), alpha: 1)
myTekstveld.delegate = self as? UITextFieldDelegate
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
a += 1
labelX += labelWidth
}
// the button is created
}
override func viewDidLoad()
{
super.viewDidLoad()
// snip
setupLabels()
}
#objc func buttonAction(sender: UIButton!) {
// snip
// here the text from the text fields is read, but this only works the first time the buttonAction is called, the next times, it simply returns the first user input.
while a <= aantalNoten {
if let theLabel = view.viewWithTag(a) as? UITextField {
let tekstInput = theLabel.text!
userInput.append(tekstInput)
}
a += 1
}
// snip
setupLabels()
return
}
// snip
You have two instances of ThirdViewController when you don't mean to.
This error is very telling:
2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
This is telling you that SecondViewController is trying to create ThirdViewController when SecondViewController is not even on the screen. This suggests that the mistake is in SecondViewController (perhaps observing notifications or other behaviors when not on screen). It's possible of course that you also have two instances of SecondViewController.
I suspect you're trying to build all of this by hand rather than letting Storyboards do the work for you. That's fine, but these kinds of mistakes are a bit more common in that case. The best way to debug this further is to set some breakpoints and carefully check the address of the objects (0x7fc709125890 for example). You'll need to hunt down where you're creating an extra one.
Your genreteNoten method is being called multiple times because it is called from setupLabels which is In turn called from viewDidLoad.
viewDidLoad may be called multiple times and your code should account for that. As it says in this answer to a similar question:
If you have code that only needs to run once for your controller use -awakeFromNib.
I managed to partially solve my second problem myself (that the read-out from the text fields was not updating to the second answer) by not creating them again.
I added some code to setupLabels function to only create the text fields if there was no input already:
let myTekstveld = UITextField()
if (view.viewWithTag(a+1) as? UITextField) != nil {
}
else {
myTekstveld.frame = CGRect(x: labelX, y: labelY + 100, width: labelWidth, height: labelHeight / 2)
// snip
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
}
The app works as expected now, the only problem is that the text fields are not cleared after each question.
Related
Using this stackoverflow solution as a guide I have a setup where I have a UITabBarController and two tabs. When changes are made in the first tab (a UIViewController), the second tab (another UIViewController with a UITableView) needs to perform some calculations, which take a while. So I have a UIActivityIndicatorView (bundled with a UILabel) that shows up when the second tab is selected, displayed, and the UITableView data is being calculated and loaded. It all works as desired in the Simulator, but when I switch to my real device (iPhone X), the calculations occur before the second tab view controller is displayed so there's just a large pause on the first tab view controller until the calculations are done.
The scary part for me is when I started debugging this with a breakpoint before the DispatchQueue.main.async call it functioned as desired. So in desperation after hours of research and debugging, I introduced a tenth of a second usleep before the DispatchQueue.main.async call. With the usleep the problem no longer occurred. But I know that a sleep is not the correct solution, so hopefully I can explain everything fully here for some help.
Here's the flow of the logic:
The user is in the first tab controller and makes a change which will force the second tab controller to recalculate (via a "dirty" flag variable held in the tab controller).
The user hits the second tab, which activates this in the UITabController:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let controllerIndex = tabBarController.selectedIndex
if controllerIndex == 1 {
if let controller = tabBarController.viewControllers?[1] as? SecondViewController {
if dirty {
controller.refreshAll()
}
}
}
}
Since dirty is true, refreshAll() is called for the secondController and its implementation is this:
func refreshAll() {
showActivityIndicator()
// WHAT?!?! This usleep call makes the display of the spinner work on real devices (not needed on simulator)
usleep(100000) // One tenth of a second
DispatchQueue.main.async {
// Load new data
self.details = self.calculateDetails()
// Display new data
self.detailTableView.reloadData()
// Clean up the activityView
DispatchQueue.main.async {
self.activityView.removeFromSuperview()
}
}
}
showActivityIndicator() is implemented in the second view controller as such (activityView is a class property):
func showActivityIndicator() {
let avHeight = 50
let avWidth = 160
let activityLabel = UILabel(frame: CGRect(x: avHeight, y: 0, width: avWidth, height: avHeight))
activityLabel.text = "Calculating"
activityLabel.textColor = UIColor.white
let activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.frame = CGRect(x: 0, y: 0, width: avHeight, height: avHeight)
activityIndicator.color = UIColor.white
activityIndicator.startAnimating()
activityView.frame = CGRect(x: view.frame.midX - CGFloat(avWidth/2), y: view.frame.midY - CGFloat(avHeight/2), width: CGFloat(avWidth), height: CGFloat(avHeight))
activityView.layer.cornerRadius = 10
activityView.layer.masksToBounds = true
activityView.backgroundColor = UIColor.systemIndigo
activityView.addSubview(activityIndicator)
activityView.addSubview(activityLabel)
view.addSubview(activityView)
}
So in summary, the above code works as desired with the usleep call. Without the usleep call, calculations are done before the second tab view controller is displayed about 19 times out of 20 (1 in 20 times it does function as desired).
I'm using XCode 12.4, Swift 5, and both the Simulator and my real device are on iOS 14.4.
Your structure is wrong. Time consuming activity must be performed off the main thread. Your calculateDetails must be ready to work on a background thread, and should have a completion handler parameter that it calls when the work is done. For example:
func refreshAll() {
showActivityIndicator()
myBackgroundQueue.async {
self.calculateDetails(completion: {
DispatchQueue.main.async {
self.detailTableView.reloadData()
self.activityView.removeFromSuperview()
}
})
}
}
So the answer is two parts:
Part 1, as guided by matt, is that I was using the wrong thread, which I believe explains the timing issue being fixed by usleep. I have since moved to a background thread with a qos of userInitiated. It seems like the original stackoverflow solution I used as a guide is using the wrong thread as well.
Part 2, as guided by Teju Amirthi, simplified code by moving the refreshAll() call to the second controller's viewDidAppear function. This simplified my code by removing the need for the logic implemented in step 2 above in the UITabController.
I have a button with accessibility label #"a". When the button is pressed, I have a callback that sets the accessibility label button.accessibilityLabel = #"b". I know this line of code runs. However, if I tap the button again, VoiceOver still reads a. Unfortunately, the code I'm working with is proprietary, so I can't share it directly.
However, in general, I would like to know what issues might cause VoiceOver to not recognize an update to a label.
THE BEST way to handle dynamic accessibility labels is to override the property functions on the views that are being focused (EX: on a UIButton). This allows TWO things. A: it's a lot easier to maintain than setting the property everywhere it can change. B: you can log information and see when the system is requesting that information, so you can better understand WHY things are happening. So even if it doesn't directly fix your issue, seeing WHEN the system requests your value, and logging that data, is inherently valuable.
Doing this in Objective C
#implementation YourUIButton
-(NSString*)accessibilityLabel {
if(someCondition) {
return #"a";
} else {
return #"b";
}
}
#end
In Swift
public class YourUIButton : UIButton
override public var accessibilityLabel: String? {
get {
if (someCondition) {
return "a"
} else {
return "b"
}
}
set {
NSException.raise(NSException("AccessibilityLabelException", "You should not set this accessibility label.", blah))
}
}
}
You could also use this logic JUST to debug, and allow setting and such.
There are a lot of potential issues here. Race conditions, which view is actually getting focus, is there some parent child relationship going on, etc. Overriding the property and adding logging statements to the above code will help you understand what view is actually getting the accessibility label requested and when. Super valuable information!
Use this while changing button text
UIAccessibility.post(notification: .layoutChanged, argument: yourButton)
Try to add UIAccessibilityTraitUpdatesFrequently to your buttons property accessibilityTraits
- (void)viewDidLoad {
myButton.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently
}
Also, when changing accessibilityLabel be sure that you're on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
myButton.accessibilityLabel = #"b";
});
You don't really need a way to refresh the voice over labels. Its done automatically. I have tried this and it works as expected.
class ViewController: UIViewController {
var tapCount = 0
var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
button = UIButton(type: .system)
button.setTitle("Hello", for: .normal)
button.frame = CGRect(x: 10, y: 10, width: 100, height: 50)
view.addSubview(button)
button.accessibilityLabel = "Hello Button"
button.accessibilityHint = "Tap here to start the action"
button.accessibilityIdentifier = "hello_button"
button.addTarget(self, action: #selector(buttonTap(sender:)), for: .touchUpInside)
}
#IBAction func buttonTap(sender:UIButton) {
tapCount = tapCount + 1
sender.accessibilityLabel = "Hello button, tapped \(tapCount) times"
}
}
What Voice oversays:
Hello Button-pause-Button-pause-Tap here to start the action
On button tap
Hello button, tapped one times
Another tap
Hello button, tapped two times
Ok I have this case where I insert 5 Views programmatically using this method:
let starView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width))
// Set Image & Alpha
starView.image = #imageLiteral(resourceName: "star")
starView.alpha = 1
starView.tag = starIndex
// Add to Super View
self.mainView.addSubview(starView)
Please note that starIndex for the 5 views are 1,2,3,4,5 consequently
It's Straightforward.
After a while when an event happens, I use another method to remove these views using this method:
func removeOldStars() {
for index in 1...5 {
if let foundView = view.viewWithTag(index) {
foundView.removeFromSuperview()
}
}
}
What happens here as a result is that the last element only "number 5" is removed. I have tried several trial and error and found this weird behavior. When I remove the view twice using the tag number it works. So for example, if I want to remove view with tag number 3 if I write
view.viewWithTag(3).removeFromSuperView()
view.viewWithTag(3).removeFromSuperView()
It works!!! if just one time it doesn't do anything. I thought maybe the view is added twice and so it need to be removed twice to notice it, but i Debugged it and no the view is added single time.
I removed the view in the main thread to be sure that its not threading issue not no its not the problem.
I would appreciate your help because this is so weird i really need to understand whats happening here.
Tags, in general, are a brittle way to reference views. As #Paulw11 mentioned, this is very likely an issue with other subviews having identical tag values.
In this case, I would hold on to instances of the UIImageViews, and then in the removeOldStars method, iterate through and call removeFromSuperview on the instance directly.
//instantiate empty array of UIImageView
var starViews = [UIImageView]()
//assuming your add method name..
func addStar() {
//your code above up to...
self.mainView.addSubview(starView)
starViews.append(starView)
}
func removeOldStars() {
for view in starViews {
view.removeFromSuperview()
//maybe explicitly de allocate the view depending
}
}
I have a Text Field in a Tip Calculator. I want the Tip Calculator to update all the fields of my calculator.
With the current code that I have now, for whatever reason. It doesn't call calcTip() (or atleast it doesn't seem to) until I press return once, enter another (or the same) value and press return again.
I'm really close to having this app work exactly how I want it to, and I feel like I'm just 1 or 2 lines away.
My ViewController conforms to UITextFieldDelegate, and I declared amountBeforeTaxTextField.delegate = self in viewDidLoad.
TextShouldReturn:
func textFieldShouldReturn(textField: UITextField) -> Bool {
amountBeforeTaxTextField.clearsOnBeginEditing = true
amountBeforeTaxTextField.becomeFirstResponder()
calcTip()
amountBeforeTaxTextField.resignFirstResponder()
return true
}
calcTip():
func calcTip() {
let tip = tipCalc.calculateTip()
let total = tipCalc.calculateTotal()
let tax = tipCalc.calculateTax()
let perPerson = total / Float(Int(partySizeSlider.value))
tipCalc.tipPercentage = (tipPercentageSlider.value)
tipCalc.amountBeforeTax = (amountBeforeTaxTextField.text! as
NSString).floatValue
tipCalc.taxPercentage = (taxPercentageSlider.value)
resultLabel.text = String(format: "Tip $%.2f, Tax: $%.2f Total: $%.2f", tip, tax, total)
perPersonLabel.text = String(format: "Per-Person Total: $%.2f", perPerson)
}
Note: I have the same outcome with & without .becomeFirstResponder()
Thanks for reading.
So your textFieldShouldReturn function is called when you press the bottom right button on the keyboard, which can have various titles; such as "Go", "Return", "Done", or "Send".
Your first line, only accomplishes a UI effect; meaning when the user taps on the textfield to input text, there will be a little box that appears on the far right handside of the textfield that allows the user to clear the inputted text, if there is any. Sort of like a reset.
When you use becomeFirstResponder, you are pretty much calling another keyboard to pop up, so you aren't accomplishing anything by doing that.
When you use resignFirstResponder, you are hiding the keyboard.
What you want to do is remove "becomeFirstResponder" as well as the first line, because those don't do anything to help your problem. That should be the solution to the problem you are facing.
I got following code:
func addObject(object: UIImageView)
{
view.addSubview(object)
}
and in another class I got this code:
func loadPlayground(width: CGFloat)
{
let playerX = screenWidth / fieldWidth * 2
let playerY = screenHight / fieldWidth * 2
let player1 = UIImageView(frame: CGRectMake(playerX, playerY, width, width))
player1.image = player
ViewController().addObject(player1)
print(playerX)
and the variable player is:
let player = UIImage(named: "Player.png")
Now my problem. My error code is:
Thread 1: EXC_BAD_ACCESS(code=2, address=0x114fbc).
It appears in the line where I'm trying to add the object to the Subview inside the class addObject. I also got the console output of the function various times. That let me conclude that the function got started often and stops after a time. Hopefully you can help me out.
In your code you are creating a new instance of ViewController everytime for adding the image view.
ViewController().addObject(player1)
So in the addObject method the view will be nil always, because it is not loaded yet.
For fixing this issue, you want to re-use the actual ViewController that is loaded currently or add the image view to the view after it is loaded.