I'm new to iOS development and have been working through some online courses. In one of these we develop a quiz app and I'd like to improve my skills by improving the app beyond what is covered in the course.
The app uses a .json file as the 'database' of questions and answers. This .json file looks as follows...
{
"id" : "1",
"question": "Earth is a:",
"answers": [
"Planet",
"Meteor",
"Star",
"Asteroid"
],
"difficulty": "1"
}
...and just keeps going for over 500 questions.
At present, the app presents the user a question with the four possible answers. In the .json file, the first answer is always the correct answer, but the app is coded to shuffle the answers so that the correct answer is not always listed first.
The app is also coded so that the four buttons (I use four different coloured images for the buttons) displaying the answers are disabled and also dimmed in appearance after the user makes a selection, except that the button they selected is not dimmed so that their choice is highlighted.
What I would like to do is change this so that the button with the correct answer is highlighted instead, as a way of notifying the user what the correct answer was.
I'm using the following code to load the questions and answers and to shuffle the answers:
func loadAllQuestionsAndAnswers()
{
let path = NSBundle.mainBundle().pathForResource("content", ofType: "json")
let jsonData : NSData = NSData(contentsOfFile: path!)!
allEntries = (try! NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)) as! NSArray
//println(allEntries)
}
func loadQuestion(index : Int)
{
let entry : NSDictionary = allEntries.objectAtIndex(index) as! NSDictionary
let question : NSString = entry.objectForKey("question") as! NSString
let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray
//println(question)
//println(arr)
labelQuestion.text = question as String
let indices : [Int] = [0,1,2,3]
//let newSequence = shuffle(indices)
let newSequence = indices.shuffle()
var i : Int = 0
for(i = 0; i < newSequence.count; i++)
{
let index = newSequence[i]
if(index == 0)
{
// we need to store the correct answer index
currentCorrectAnswerIndex = i
}
let answer = arr.objectAtIndex(index) as! NSString
switch(i)
{
case 0:
buttonA.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 1:
buttonB.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 2:
buttonC.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 3:
buttonD.setTitle(answer as String, forState: UIControlState.Normal)
break;
default:
break;
}
At present I am using this code to check the answer:
var currentCorrectAnswerIndex : Int = 0
func checkAnswer( answerNumber : Int)
{
if(answerNumber == currentCorrectAnswerIndex)
{
// we have the correct answer
labelFeedback.text = "Correct! +1"
labelFeedback.textColor = UIColor.greenColor()
score = score + 1
labelScore.text = "score: \(score)"
SaveScore()
// later we want to play a "correct" sound effect
PlaySoundCorrect()
}
else
{
// we have the wrong answer
labelFeedback.text = "Wrong answer"
labelFeedback.textColor = UIColor.redColor()
// we want to play a "incorrect" sound effect
PlaySoundWrong()
}
In terms of highlighting the buttons I was going to use coding such as:
func resetAnswerButtons()
{
buttonA.alpha = 1.0
buttonB.alpha = 1.0
buttonC.alpha = 1.0
buttonD.alpha = 1.0
buttonA.enabled = true
buttonB.enabled = true
buttonC.enabled = true
buttonD.enabled = true
}
#IBAction func PressedButtonA(sender: UIButton) {
print("button A pressed")
buttonB.alpha = 0.3
buttonC.alpha = 0.3
buttonD.alpha = 0.3
buttonA.enabled = false
buttonB.enabled = false
buttonC.enabled = false
buttonD.enabled = false
CheckAnswer(0)
}
#IBAction func PressedButtonB(sender: UIButton) {
print("button B pressed")
buttonA.alpha = 0.3
buttonC.alpha = 0.3
buttonD.alpha = 0.3
buttonA.enabled = false
buttonB.enabled = false
buttonC.enabled = false
buttonD.enabled = false
CheckAnswer(1)
}
#IBAction func PressedButtonC(sender: UIButton) {
print("button C pressed")
buttonA.alpha = 0.3
buttonB.alpha = 0.3
buttonD.alpha = 0.3
buttonA.enabled = false
buttonB.enabled = false
buttonC.enabled = false
buttonD.enabled = false
CheckAnswer(2)
}
#IBAction func PressedButtonD(sender: UIButton) {
print("button D pressed")
buttonA.alpha = 0.3
buttonB.alpha = 0.3
buttonC.alpha = 0.3
buttonA.enabled = false
buttonB.enabled = false
buttonC.enabled = false
buttonD.enabled = false
CheckAnswer(3)
}
What I can't get my head around is how to code the app so that it highlights the correct answer? At present, the above code effectively highlights the button that was pressed by dimming the alpha of the 'other' buttons. How do I get the app to identify which 'shuffled' button contains the correct answer and highlight that one instead?
When rolling your dice, remember the index of the button containing the right answer.
Also, you may put all buttons in an array and manipulate all of them with forall loops. That makes the code much, much cleaner and gets rid of the weird variable names and the switch case.
Related
My following code displays a question and its matching answer by making an array of keys and values from a dictionary and making sure both arrays have the same index that is pulled. However after a question and its answers are randomly selected I want to remove them permanently from their respective arrays until all the other items in the array have been displayed. Simply put, how can I make sure a question and set of answers are not shown again until all the items are shown or until the user gets an answer wrong? As of now my code only makes sure the same random index isn't chosen twice in a row when I want to make sure it isn't shown twice at all.
Here is my code (swift for iOS):
func randomQuestion() {
//random question
var questionList = Array(QADictionary.keys)
var rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answers
var answerList = Array(QADictionary.values)
var choices = answerList[rand]
rightAnswerBox = arc4random_uniform(4)+1
//create button
var button:UIButton = UIButton()
var x = 1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
func removePair() {
questionList.remove(at: rand)
answerList.remove(at: rand)
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
Move your questionList to a property of the viewController so that it stays around from one call of randomQuestion to the next. Reload it only when it is empty.
Use the dictionary to find the matching answers instead of using a second array.
var questionList = [String]()
func randomQuestion() {
//if no more questions, reload the questionList
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
}
var rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answers
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
rightAnswerBox = arc4random_uniform(4)+1
//create button
var button:UIButton = UIButton()
var x = 1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
func removePair() {
questionList.remove(at: rand)
}
randomImage()
}
}
I keep coming across this error "fatal error: Index out of range", after researching this I am still unsure of exactly how to fix this issue. To give context I have started off with an empty array var playersArray = [UITextField]() so that users can enter their names in order to play the game. I then make sure that the user has or has not entered a value into the text field for each name slot
if let player1Name = name1.text, !player1Name.isEmpty
{ playersArray.append(name1)
} else {
print("Player 1 Empty")
If the player has entered a value into that textfield then ill append that value to the array.
The issue I have is that if I run the game and no user has entered a name into any of the 10 textfields then the game will crash. Im assuming this is because the array is empty?
The error appears on this line where I randomize the names used for the game with the number of elements in the array:
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
I assume if the array is empty then .count will not work?
How can I make sure the game wont crash if the array is empty?
CODE:
var playersArray = [UITextField]()
override func viewDidLoad() {
super.viewDidLoad()
textColor()
question1View.isHidden = true
questionLabel.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// Alert message on startup
func alertMessageOnStartUp(){
let alert = UIAlertController(title: "Warning!", message: "Please drink responsibly. By continuing, you agree that you are responsible for any consequences that may result from BottomsUp.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Agree", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
// Dismiss keyboard when tapped outside the keyboard
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
// Dimiss keybaord when return button is tapped
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
name1.resignFirstResponder()
name2.resignFirstResponder()
name3.resignFirstResponder()
name4.resignFirstResponder()
name5.resignFirstResponder()
name6.resignFirstResponder()
name7.resignFirstResponder()
name8.resignFirstResponder()
name9.resignFirstResponder()
name10.resignFirstResponder()
return(true)
}
//randomise background colour of each question page
func getRandomBackgroundColor() -> UIColor{
let randomRed:CGFloat = CGFloat(drand48())
let randomGreen:CGFloat = CGFloat(drand48())
let randomBlue:CGFloat = CGFloat(drand48())
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
func textColor(){
name1.textColor = UIColor.white
name2.textColor = UIColor.white
name3.textColor = UIColor.white
name4.textColor = UIColor.white
name5.textColor = UIColor.white
name6.textColor = UIColor.white
name7.textColor = UIColor.white
name8.textColor = UIColor.white
name9.textColor = UIColor.white
name10.textColor = UIColor.white
}
#IBAction func playButton(_ sender: Any) {
alertMessageOnStartUp()
if let player1Name = name1.text, !player1Name.isEmpty
{ playersArray.append(name1)
} else {
print("Player 1 Empty")
}
if let player2Name = name2.text, !player2Name.isEmpty
{ playersArray.append(name2)
} else {
print("Player 2 Empty")
}
if let player3Name = name3.text, !player3Name.isEmpty
{ playersArray.append(name3)
} else {
print("Player 3 Empty")
}
if let player4Name = name4.text, !player4Name.isEmpty
{ playersArray.append(name4)
} else {
print("Player 4 Empty")
}
if let player5Name = name5.text, !player5Name.isEmpty
{ playersArray.append(name5)
} else {
print("Player 5 Empty")
}
if let player6Name = name6.text, !player6Name.isEmpty
{ playersArray.append(name6)
} else {
print("Player 6 Empty")
}
if let player7Name = name7.text, !player7Name.isEmpty
{ playersArray.append(name7)
} else {
print("Player 7 Empty")
}
if let player8Name = name8.text, !player8Name.isEmpty
{ playersArray.append(name8)
} else {
print("Player 8 Empty")
}
if let player9Name = name9.text, !player9Name.isEmpty
{ playersArray.append(name9)
} else {
print("Player 9 Empty")
}
if let player10Name = name10.text, !player10Name.isEmpty
{ playersArray.append(name10)
} else {
print("Player 10 Empty")
}
question1View.isHidden = false
question1View.backgroundColor = getRandomBackgroundColor()
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
#IBAction func nextQuestionButton(_ sender: Any) {
question1View.backgroundColor = getRandomBackgroundColor()
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
}
Breaking this down:
Int(arc4random_uniform(UInt32(playersArray.count)))
This line gets a random number with a minimum value of 0 and a maximum value of the length of the playersArray minus 1.
I'm actually not sure what it does when the argument you pass in is 0, but it doesn't really matter, as we'll see next.
Then you use that random value here:
playersArray[thatRandomNumber]
Because there are no elements in playersArray, no matter what the value is of thatRandomNumber, it's going to be out of bounds.
You probably want something more like this:
let RandomPlayer = <some default value>
if !playersArray.isEmpty {
RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
}
EDIT
Your latest code still doesn't seem to do anything to prevent indexing into the empty array.
You have:
#IBAction func playButton(_ sender: Any) {
...
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
You need:
#IBAction func playButton(_ sender: Any) {
...
if playersArray.isEmpty {
// do something about that
} else {
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
}
playersArray.count for an empty array is 0, so you are trying to access playersArray[0] - but the array is empty, so nothing exists at the 0 index.
You should do something like this:
let randomPlayer: Player
if !playersArray.isEmpty {
randomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
} else {
randomPlayer = Player() //create a fallback player
}
Alternatively you could make randomPlayer an optional, rather than providing a fallback value. Depends on your needs for that variable.
My problem: the text on some of my UILabels (the answers for the questions) in a particular cell are blank when I scroll down (only the 3rd through 7th labels, as you'll see in my code the first and second are always set and seem to magically work no matter what). It only happens for some cells, for most cells when they render the first time it's correct (not always, though).
I've been struggling with this for a few hours now and can't seem to fix it. I'm pretty sure it has to do with table cell reuse, but I set the data in a didSet block in my custom table cell and even tried the prepareForReuse() override as suggested here: Stop the reuse of custom cells Swift. Nothing seems to work, I still have some cells (only some, not all!) that lose some of their text when I scroll through my tableView. What am I missing?
Here is the didSet block for my custom cell class QuestionCustomCell (and if it helps, the NSLog printout does show the correct text that should be in the cell when I scroll down, but it doesn't render):
var cellQuestion: Question! {
didSet {
questionLabel.text = cellQuestion.topQuestion
questionLabel.sizeToFit()
//definitely at least 2 answers
switchLabel1.text = cellQuestion.theResponses[0]
switchLabel2.text = cellQuestion.theResponses[1]
if(cellQuestion.theResponses[2] == nil) {
switch3.isHidden = true
switchLabel3.isHidden = true
} else {
switch3.isHidden = false
switchLabel3.text = cellQuestion.theResponses[2]
let questionName : String = cellQuestion.topQuestion
NSLog("question: "+questionName+" response: "+cellQuestion.theResponses[2]!)
}
if(cellQuestion.theResponses[3] == nil) {
switch4.isHidden = true
switchLabel4.isHidden = true
} else {
switch4.isHidden = false
switchLabel4.text = cellQuestion.theResponses[3]
let questionName : String = cellQuestion.topQuestion
NSLog("question: "+questionName+" response: "+cellQuestion.theResponses[3]!)
}
if(cellQuestion.theResponses[4] == nil) {
switch5.isHidden = true
switchLabel5.isHidden = true
} else {
switch5.isHidden = false
switchLabel5.text = cellQuestion.theResponses[4]
let questionName : String = cellQuestion.topQuestion
NSLog("question: "+questionName+" response: "+cellQuestion.theResponses[4]!)
}
if(cellQuestion.theResponses[5] == nil) {
switch6.isHidden = true
switchLabel6.isHidden = true
} else {
switch6.isHidden = false
switchLabel6.text = cellQuestion.theResponses[5]
let questionName : String = cellQuestion.topQuestion
NSLog("question: "+questionName+" response: "+cellQuestion.theResponses[5]!)
}
if(cellQuestion.theResponses[6] == nil) {
switch7.isHidden = true
switchLabel7.isHidden = true
} else {
switch7.isHidden = false
switchLabel7.text = cellQuestion.theResponses[6]
let questionName : String = cellQuestion.topQuestion
NSLog("question: "+questionName+" response: "+cellQuestion.theResponses[6]!)
}
restorePriorAnswer()
}
}
And here is restorePriorAnswer() (I use it so the cells remember whether the switch was off or on when reused - that does work):
func restorePriorAnswer ( )
{
if(cellQuestion.theAnswer != nil)
{
self.cellDelegate.restorePrior(forCell: self, forShortName: cellQuestion.shortName, subAction: "\(cellQuestion.theAnswer!.answerSelect)")
print("Prior answer was \(cellQuestion.theAnswer!.answerSelect)")
switch(cellQuestion.theAnswer!.answerSelect)
{
case 0:
switch1.isOn = true
break
case 1:
switch2.isOn = true
break
case 2:
switch3.isOn = true
break
case 3:
switch4.isOn = true
break
case 4:
switch5.isOn = true
break
case 5:
switch6.isOn = true
break
case 6:
switch7.isOn = true
break
default:
self.cellDelegate.restorePrior(forCell: self, forShortName: "Control", subAction: "restorePriorAnswer (Bad Select Value)")
break
}
}
else
{
switch1.setOn(false, animated: false)
switch2.setOn(false, animated: false)
switch3.setOn(false, animated: false)
switch4.setOn(false, animated: false)
switch5.setOn(false, animated: false)
switch6.setOn(false, animated: false)
switch7.setOn(false, animated: false)
}
}
And here is my cellForRowAtIndexPath:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:QuestionCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuse, for: indexPath) as! QuestionCustomCell
if((currentQuestion?.canSubsume())!) {
if((currentQuestion?.shouldDoSubsume())!) {
//since this apparently refreshes random rows, we need to hard-code which row goes where
if(indexPath.row == 0) {
cell.cellQuestion = currentQuestion
} else {
let rowQuestion: Question = (currentQuestion?.getSubsumeList().getQuestion(shortID: currSubsumeArray![indexPath.row - 1]))!
cell.cellQuestion = rowQuestion
}
} else {
//there should only be one cell at this point - the first one
cell.cellQuestion = currentQuestion
//print out the first question, just to make sure...
let logStringOne : String = "the current question: " + (currentQuestion?.topQuestion)!
NSLog(logStringOne)
let logString : String = "the first subsumed question: " + (currentQuestion?.subsumeList!.firstQuestion)!
NSLog(logString)
}
} else {
cell.cellQuestion = currentQuestion
}
cell.selectionStyle = UITableViewCellSelectionStyle.none
cell.cellDelegate = self
return cell
}
Please let me know if I'm missing any code you think is helpful to solve the problem. Everything else works besides the labels - the switches remember if they were on or off when scrolling back and forth, the appropriate switches are hidden if their appropriate slot in the answers array is nil, and all other text is as it should be - except some of the labels for the answers, which is the problem. As I mentioned, I tried the prepareForReuse() method but it didn't do anything, so I took it out.
I apologize, it was staring me in the face for several hours and I didn't realize it. In the didSet block I had hidden the labels if they weren't supposed to be used, like this:
if(cellQuestion.theResponses[2] == nil) {
switch3.isHidden = true
switchLabel3.isHidden = true
} else {
switch3.isHidden = false
switchLabel3.text = cellQuestion.theResponses[2]
}
but in the else block, I didn't unhide them...when a cell was loaded fresh and not reused, it had no issue because the label had never been hidden previously. But in a reused cell, it was, and so the label didn't exist for those cells that were reused.
So, each of these if/else blocks should have a switchLabelx.isHidden = falsein them, with x being the appropriate number. Not that I wish staring at code for hours on anyone, but hopefully someone will find this useful.
How do I set up the buttons that are linked to didPressNumber to add to each other when pressed so lets say its a calculator and I want set it up where each button is pressed has a letter and number value when it is pressed it adds to the previous one press and I want to set up 2 labels one displaying the number value and one displaying the letter value and how would I set up the value of each number?
enum modes {
case not_set
case addition
case subtraction
case equals
}
#IBAction func didPressNumber(_ sender: UIButton) {
let stringValue:String? = sender.titleLabel?.text
if (lastButtonWasMode) {
lastButtonWasMode = false
labelString = "0"
}
labelString = labelString.appending(stringValue!)
updateText()
}
func updateText() {
guard let labelInt:Int = Int(labelString) else {
return
}
if (currentMode == .not_set) {
savedNum = labelInt
}
let formatter: NumberFormatter = NumberFormatter()
formatter.numberStyle = .decimal
let num:NSNumber = NSNumber(value: labelInt)
label.text = formatter.string(from: num)
}
func changeMode(newMode:modes) {
if (savedNum == 0) {
return
}
currentMode = newMode
lastButtonWasMode = true
}
When I rotate the app twice after selecting a few items, it crashes. I have overridden the sendEvent method and that's where the debugger stops. When I try to print the event type, it shows me something weird (I think it's a memory location that doesn't exist):
(lldb) print event.type
(UIEventType) $R10 = <invalid> (0xff)
Somehow I think this is related to how I handle the rotation. I have a master-detail style application, that uses a different type of navigation for pad-landscape, pad-portrait and phone. I have created a class named NavigationFlowController which handles all navigational events and sets up the views accordingly. On rotation, it breaks up the view trees and recomposes them with the correct navigation
func changeViewHierarchyForDevideAndOrientation(newOrientation:UIInterfaceOrientation? = nil){
print("MA - Calling master layout method")
UIApplication.myDelegate().window?.frame = UIScreen.mainScreen().bounds
let idiom = UIDevice.currentDevice().userInterfaceIdiom
var orientation:UIInterfaceOrientation!
if let no = newOrientation{
orientation = no
}else{
orientation = UIApplication.sharedApplication().statusBarOrientation
}
print("MA - Breaking up view tree...")
breakupFormerViewTree([sidebarViewController, listViewController, detailViewController, loginViewController])
print("MA - Start init navbackbone")
initNavBackboneControllers()
guard let _ = UIApplication.myDelegate().currentUser else {
if idiom == UIUserInterfaceIdiom.Phone{
currentState = AppState.PHONE
}else if idiom == UIUserInterfaceIdiom.Pad && UIInterfaceOrientationIsLandscape(orientation){
currentState = AppState.PAD_LANDSCAPE
}else if idiom == UIUserInterfaceIdiom.Pad && UIInterfaceOrientationIsPortrait(orientation){
currentState = AppState.PAD_PORTRAIT
}
print("MA - Current user is nil - resetting")
mainViewController.addChildViewController(loginViewController)
return
}
if idiom == UIUserInterfaceIdiom.Phone{
currentState = AppState.PHONE
leftNavigationController?.viewControllers = [listViewController]
slideViewController?.rearViewController = sidebarViewController
slideViewController?.frontViewController = leftNavigationController
slideViewController?.rearViewRevealWidth = 267;
mainViewController.addChildViewController(slideViewController!)
}else if idiom == UIUserInterfaceIdiom.Pad && UIInterfaceOrientationIsLandscape(orientation){
currentState = AppState.PAD_LANDSCAPE
leftNavigationController!.viewControllers = [sidebarViewController, listViewController]
rightNavigationController!.viewControllers = [detailViewController]
detailViewController.navigationItem.leftBarButtonItems = []
detailViewController.initLayout()
print("MA - Init split view controller with VCs")
splitViewController!.viewControllers = [leftNavigationController!, rightNavigationController!]
mainViewController.addChildViewController(splitViewController!)
}else if idiom == UIUserInterfaceIdiom.Pad && UIInterfaceOrientationIsPortrait(orientation){
currentState = AppState.PAD_PORTRAIT
leftNavigationController!.pushViewController(sidebarViewController, animated: false)
leftNavigationController!.pushViewController(listViewController, animated: false)
rightNavigationController!.pushViewController(detailViewController, animated: false)
rightNavigationController?.setNavigationBarHidden(false, animated: false)
slideViewController!.rearViewController = leftNavigationController
slideViewController!.frontViewController = rightNavigationController
detailViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "< Documenten", style: UIBarButtonItemStyle.Bordered, target: slideViewController, action: "revealToggle:")
detailViewController.initLayout()
slideViewController!.rearViewRevealWidth = 350;
mainViewController.addChildViewController(slideViewController!)
}
}
func breakupFormerViewTree(vcs:[UIViewController?]){
for vc in vcs{
if let vcUnwrapped = vc, _ = vcUnwrapped.parentViewController {
vcUnwrapped.removeFromParentViewController()
vcUnwrapped.view.removeFromSuperview()
}
}
}
func initNavBackboneControllers(){
leftNavigationController = UINavigationController()
leftNavigationController?.navigationBar.barTintColor = UIColor(red: 0.25, green: 0.25, blue: 0.25, alpha: 1.0)
leftNavigationController?.navigationBar.tintColor = UIColor.whiteColor()
leftNavigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()]
leftNavigationController?.navigationBar.translucent = false
rightNavigationController = UINavigationController()
rightNavigationController?.navigationBar.barTintColor = UIColor(red: 0.25, green: 0.25, blue: 0.25, alpha: 1.0)
rightNavigationController?.navigationBar.tintColor = UIColor.whiteColor()
rightNavigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()]
rightNavigationController?.navigationBar.translucent = false
slideViewController = SWRevealViewController()
slideViewController?.rearViewRevealOverdraw = 0;
slideViewController?.bounceBackOnOverdraw = false;
slideViewController?.stableDragOnOverdraw = true;
slideViewController?.delegate = self
if UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad{
splitViewController = UISplitViewController()
}
}
EDIT (in response to Justin's questions):
1) I've experienced the crash on all iOS8 iPad simulators.
2) From a fresh start, if I select like 6-7 items and then I rotate twice, it crashes. But I can also select an item, rotate a few times, select some more and keep rotating and at some point it will crash.
3) When an item is selected, the following code is executed:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let document = getInfoForSection(indexPath.section).documents[indexPath.item]
if document.canOpen{
openDocument(document)
DataManager.sharedInstance.getDocument(document.uri, after: {
(document:Document?) -> () in
if let documentUnwrapped = document{
let detailVC = NavigationFlowController.sharedInstance.detailViewController;
if detailVC.document?.uri == documentUnwrapped.uri{
NavigationFlowController.sharedInstance.detailViewController.documentUpdated(documentUnwrapped)
}
}
})
}
}
And then in the detail view controller:
func initLayout(){
if viewForCard == nil{
// views not yet initialized, happens when initLayout if called from the document setter before this view has been loaded
// just return, the layouting will be done on viewDidLoad with the correct document instead
return
}
self.navigationItem.rightBarButtonItems = []
if document == nil{
// Removed code that handles no document selected
...
return
}
heightForCard.constant = NavigationFlowController.sharedInstance.currentState == AppState.PHONE ? CARD_HEIGHT_PHONE : CARD_HEIGHT_TABLET
viewForCard.hidden = false
removeAllSubviews(viewForCard)
removeAllSubviews(viewForDetails)
viewForDetails.translatesAutoresizingMaskIntoConstraints = false
self.metaVC?.document = document
//self.documentVC?.document = document
self.navigationItem.rightBarButtonItems = []
downloadDocumentIfNeeded()
if NavigationFlowController.sharedInstance.currentState == AppState.PAD_LANDSCAPE || NavigationFlowController.sharedInstance.currentState == AppState.PAD_PORTRAIT{
self.viewForDetails.backgroundColor = document?.senderStyling?.color
addChildViewController(self.metaVC!)
addChildViewController(self.documentVC!)
let metaView = self.metaVC!.view
let documentView:UIView = self.documentVC!.view
viewForDetails.addSubview(metaView)
viewForDetails.addSubview(documentView)
// whole lot of layouting code removed
...
let doubleTap = UITapGestureRecognizer(target: self, action: "toggleZoom")
documentVC!.view.addGestureRecognizer(doubleTap)
}else{
// Phone version code removed
...
}
}
EDIT2:
func downloadDocumentIfNeeded(){
var tmpPath:NSURL?
if let url = document?.contentUrl{
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
if let docName = self.document?.name,
safeName = disallowedCharacters?.stringByReplacingMatchesInString(docName, options: [], range: NSMakeRange(0, docName.characters.count), withTemplate: "-"){
tmpPath = directoryURL.URLByAppendingPathComponent("\(safeName)_\(DetailViewController.dateFormatter.stringFromDate(self.document!.creationDate!)).pdf")
}
if let urlString = tmpPath?.path{
if NSFileManager.defaultManager().fileExistsAtPath(urlString) {
// File is there, load it
loadDocumentInWebview(tmpPath!)
}else{
// Download file
let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
(temporaryURL, response) in
if let path = tmpPath{
return path
}
return temporaryURL
}
download(.GET, URLString: url, destination: destination).response {
(request, response, data, error) in
if error != nil && error?.code != 516{
ToastView.showToastInParentView(self.view, withText: "An error has occurred while loading the document", withDuaration: 10)
}else if let pathUnwrapped = tmpPath {
self.loadDocumentInWebview(pathUnwrapped)
}
}
}
}
}
}
func loadDocumentInWebview(path:NSURL){
if self.navigationItem.rightBarButtonItems == nil{
self.navigationItem.rightBarButtonItems = []
}
self.documentVC?.finalPath = path
let shareItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Action, target: self, action: "share")
shareItem.tag = SHARE_ITEM_TAG
addNavItem(shareItem)
}
func addNavItem(navItem:UIBarButtonItem){
var addIt = true
for item in self.navigationItem.rightBarButtonItems!{
if item.tag == navItem.tag{
addIt = false
}
}
if addIt{
self.navigationItem.rightBarButtonItems?.append(navItem)
self.navigationItem.rightBarButtonItems!.sortInPlace({ $0.tag > $1.tag })
}
}
EDIT3: I've overridden the sendEvent method to track whether or not a user is touching the app or not, but even if I take out this code, it still crashes, and the debugger then breaks on UIApplicationMain.
override func sendEvent(event: UIEvent)
{
super.sendEvent(event)
if event.type == UIEventType.Touches{
if let touches = event.allTouches(){
for item in touches{
if let touch = item as? UITouch{
if touch.phase == UITouchPhase.Began{
touchCounter++
}else if touch.phase == UITouchPhase.Ended || touch.phase == UITouchPhase.Cancelled{
touchCounter--
}
if touchCounter == 0{
receiver?.notTouching()
}
}
}
}
}
}
Tough one, a bit more insight in the events upto this bug might be helpful.
Does it happen on every device (if not, which devices gives you troubles)
It happens after "vigorously selecting" items. Did your device change orientation before that. Does it also happen before you once rotate?
What do you do in code when you "select an item".
Other then that, I'd start to get the flow of removing your child ViewControllers in breakupFormerViewTree() right. Based on the Apple Docs you want to tell the child it's being removed, before removing the view and then finally removing the child from the Parent ViewController
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html
Here it actually says you want to call willMoveToParentViewController(nil) before doing the removing. It doesn't say what happens if you don't, but I can imagine the OS doing some lifecycle management there, preventing it from sending corrupt events at a later point.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/willMoveToParentViewController:
EDIT (After extra was code posted)
I don't see anything else in your code that might cause it to crash. It does look like a memory-error as you stated, but no idea where it's coming from. Try turning on Zombie objects and Guard Malloc (Scheme > Run > Diagnostics) and maybe you can get a bit more info on what's causing it.
Other then that, I'd just comment out loads of your implementation, swap Subclasses with empty ViewControllers until it doesn't happen again. You should be able to pinpoint what part of your implementation is involved in creating this event. Once you do that, well, pinpoint more and evaluate every single line of code in that implementation.