Using protocols with services in swift - ios

I have implemented an Audio Capturing service in my swift project that is supposed to handle audio recording and translates it into text.
AudioCaptureService
class AudioCaptureService:
// EXLCUDED A BUNCH OF SETUP CODE
func record(textView: UITextView, microphoneButton: UIButton) {
if audioEngine.isRunning {
audioEngine.stop()
recognitionRequest?.endAudio()
microphoneButton.setImage(#imageLiteral(resourceName: "microphone-full-white").withRenderingMode(.alwaysOriginal), for: .normal)
print("stopped recording...")
} else if !audioEngine.isRunning, isRecordingEnabled{
startRecording(textView: textView)
print("start recording...")
microphoneButton.setImage(#imageLiteral(resourceName: "microphone-red").withRenderingMode(.alwaysOriginal), for: .normal)
}
}
fileprivate func startRecording(textView: UITextView) {
if recognitionTask != nil {
recognitionTask?.cancel()
recognitionTask = nil
}
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryRecord)
try audioSession.setMode(AVAudioSessionModeMeasurement)
try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
} catch {
print("audioSession properties weren't set because of an error.")
}
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
let inputNode = audioEngine.inputNode
guard let recognitionRequest = recognitionRequest else {
fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")
}
recognitionRequest.shouldReportPartialResults = true
recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
var isFinal = false
if result != nil {
textView.text = result?.bestTranscription.formattedString
isFinal = (result?.isFinal)!
}
if error != nil || isFinal {
self.audioEngine.stop()
inputNode.removeTap(onBus: 0)
self.recognitionRequest = nil
self.recognitionTask = nil
//self.microphoneButton.isEnabled = true
}
})
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
self.recognitionRequest?.append(buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
print("audioEngine couldn't start because of an error.")
}
textView.text = ""
}
}
ViewController
In a separate view controller, I have a microphone button that is supposed to trigger this recording functionality. When the user clicks the microphone button, the button should turn red to indicate its recording, and then the audio that the user inputs will display in the textview of that view controller:
class PreviewController: UIViewController {
var notesOpen = false
let audioCaptureService = AudioCaptureService()
let microphoneButton: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(handleRecord), for: .touchUpInside)
button.setImage(#imageLiteral(resourceName: "muted-white").withRenderingMode(.alwaysOriginal), for: .normal)
button.isEnabled = false
return button
}()
lazy var notesView: UITextView = {
let frame = CGRect(x: 0, y: 0, width: 0, height: 0)
let tv = UITextView()
tv.layer.cornerRadius = 8
tv.font = UIFont.boldSystemFont(ofSize: 12)
tv.backgroundColor = .white
tv.keyboardDismissMode = .onDrag
return tv
}()
//EXCLUDED SOME BASIC SETUP CODE
#objc func handleRecord() {
print("record button pressed")
toggleNotesView()
audioCaptureService.record(textView: notesView, microphoneButton: microphoneButton)
}
Issue
Right now my implementation works, but I'm suspicious that it could be improved. I don't think I should be worried about passing in the textView and microphone button into my AudioCaptureService? Ideally, I'd like to have these things separate without AudioCaptureService depending on having a textView and button passed to it to work.
I was reading about protocols and think this may be a solution, but I can't seem to wrap my head around how I would implement this.
I was thinking I could do something like :
protocol AudioCaptureServiceDelegate {
func record(textView: UITextView)
}
But then who would the PreviewController class be the delegate? Im just a bit confused on how to better implement my code and any suggestions would help.

KISS would say conform in UITextView, if its just an output delegate, see below. If you want to handle more in the delegate it should definitely go to into the PreviewViewController cause UITextView is only to display text.
Some would recommend it should go into some kind of controller since audio != View, that is up for debate. Controller would mean the thing that controls the PreviewViewController which then sets text on PreviewViewController to display every time the text changes. The recordButton then leads through PreviewViewControllers' delegate to the controller which then handles recording, maybe storing the recordings somewhere, etc.
All in all a ViewController which lets its parent controller handle recording and displays text and changes the recordButton depending on the state the parent controller sets on the ViewController.
// Variant 1
protocol AudioCaptureServiceOutputDelegate: class {
func audioCaptureServiceOutputDelegate(outputChanged: String)
}
extension UITextView: AudioCaptureServiceOutputDelegate {
func audioCaptureServiceOutputDelegate(outputChanged: String) {
self.text = outputChanged
}
}
// Variant 2
protocol AudioCaptureServiceOutputDelegate2: class {
var text: String! { get set }
}
extension UITextView: AudioCaptureServiceOutputDelegate2 {}
// Variant 3
protocol AudioCaptureServiceOutputDelegate3: class {
var audioCaptureServiceOutputText: String { get set }
}
extension UITextView: AudioCaptureServiceOutputDelegate3 {
var audioCaptureServiceOutputText: String {
get { return text }
set { text = newValue }
}
}
All three work just as good. Variant 2 is just a little bit worse since a Controller/ViewController implementing a variable with the random name text may be a little too generic.
A controller would probably prefer 1, since what should it return on get? It has maybe several recordings stored already.
Calling example
func record(outputDelegate: AudioCaptureServiceOutputDelegate, microphoneButton: UIButton) {
// ....
startRecording(outputDelegate: outputDelegate)
// ....
}
fileprivate func startRecording(outputDelegate: AudioCaptureServiceOutputDelegate) {
// ....
outputDelegate.audioCaptureServiceOutputDelegate(outputChanged: result?.bestTranscription.formattedString ?? "Error: result is nil.")
// ....
outputDelegate.audioCaptureServiceOutputDelegate(outputChanged: "")
}

Related

How to add two numbers in Swift only when both of the text fields are filled in

I'm trying to add two numbers in Swift 5, and I want to add some error checks. I don't want to make it possible for a user to click on the plus button if both of the text fields are not filled in. I tried with the if state below but it did not work.
the whole function:
#IBAction func sum(_ sender: Any) {
let one = input1.text
let oneInt = Int(one!)
let two = input2.text
let twoInt = Int(two!)
let total = oneInt! + twoInt!
label.text = "\(total)"
if(input2.text == nil){
addBtn.isEnabled = false
}
if(input1.text == nil){
addBtn.isEnabled = false
}
}
Try to use guard like this. If your input field does not contain any value that field return blank string and when you try to get integer value from that string it will return nil and your add button will be disable.
#IBAction func sum(_ sender: Any) {
guard let text1 = input1.text, let intValue1 = Int(text1) else {
addBtn.isEnabled = false
return
}
guard let text2 = input2.text, let intValue2 = Int(text2) else {
addBtn.isEnabled = false
return
}
label.text = "\(intValue1 + intValue2)"
}
A nice and simple way is to addTarget to your textFiels. This will enable you to handle the events on the text field. In this scenario we'll use .editingChanged and use a single selector to achieve our goal:
What we'll do : We will listen for when someone types something in the textfield. Whenever a text changed was made, we'll check to see if all the textfields was populated and then if it was we enable the sum button.
A small controller sample :: Make sure to read the comments to understand the code faster
import UIKit
class ViewController: UIViewController {
#IBOutlet var textfield1: UITextField!
#IBOutlet var textfield2: UITextField!
#IBOutlet var sumButton: UIButton!
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
sumButton.isEnabled = false /// Disable the button first thing
[textfield1, textfield2].forEach {
$0.addTarget(self, action: #selector(editingChanged(_:)), for: .editingChanged) /// add targets to handle the events (in your case it listens for the 'editingChanged' event )
}
}
#objc func editingChanged(_ textField: UITextField) {
/// Here we just loop through all our textfields
for each in [textfield1, textfield2] {
if let text = each?.text { /// Just make sure the textfields text is not nil
if text.count < 1 {
// If the textfiels text has no value in, then we keep the button disabled and return
sumButton.isEnabled = false
return
}
} else {
/// Else if the text field's text is nill, then return and keep the button disabled
sumButton.isEnabled = false
return
}
}
sumButton.isEnabled = true /// If the code reaches this point, it means the textfields passed all out checks and the button can be enabled
}
#IBAction func sum(_ sender: Any) {
let one = textfield1.text!
let two = textfield2.text!
guard let oneInt = Int(one), let twoInt = Int(two) else {
print("Whatever was in that text fields, couldn't be converted to an Int")
label.text = "Be sure to add numbers."
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
}
textfields are not nil but empty strings. so make your comparison like :
if input1.text == "" {
// do your check here
}
Seems like you want to start with addBtn.isEnabled = false then update it whenever the user enters two valid integers into the text fields, i.e. Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil. You can do this by adding a target to your textfields (input1 and input2) for .editingChanged events. For example, if you're doing this in a UIViewController, you can do this in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
addBtn.isEnabled = false
input1.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
input2.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
}
Where textFieldDidEdit(_:) action looks like:
#objc func textFieldDidEdit(_ sender: UITextField) {
addBtn.isEnabled = Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil
}
Finally your sum function becomes:
#IBAction func sum(_ sender: UIButton) {
guard let oneInt = Int(input1.text ?? ""), let twoInt = Int(input2.text ?? "") else {
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
As all of the number validation has moved to the textFieldDidEdit(_:) function.

Allowing background audio with Swift not working

I want to allow background audio while the app is not in focus. I currently have this code, which should allow that:
do {
try AKSettings.setSession(category: .playback, with: .mixWithOthers)
} catch {
print("error")
}
AKSettings.playbackWhileMuted = true
I also have the setting 'Audio, Airplay and Picture in Picture' enabled in capabilities settings. However, when I press the home button on my device the audio doesn't keep playing. What am I doing wrong? I am using AudioKit to produce sounds if that matters.
I am using a singleton to house all of the AudioKit components which I named AudioPlayer.swift. Here is what I have in my AudioPlayer.swift singleton file:
class AudioPlayer: NSObject {
var currentFrequency = String()
var soundIsPlaying = false
var leftOscillator = AKOscillator()
var rightOscillator = AKOscillator()
var rain = try! AKAudioFile()
var rainPlayer: AKAudioPlayer!
var envelope = AKAmplitudeEnvelope()
override init() {
super.init()
do {
try AKSettings.setSession(category: .playback, with: .mixWithOthers)
} catch {
print("error")
}
AKSettings.playbackWhileMuted = true
AudioKit.output = envelope
AudioKit.start()
}
func setupFrequency(left: AKOscillator, right: AKOscillator, frequency: String) {
currentFrequency = frequency
leftOscillator = left
rightOscillator = right
let leftPanner = AKPanner(leftOscillator)
leftPanner.pan = -1
let rightPanner = AKPanner(rightOscillator)
rightPanner.pan = 1
//Set up rain and rainPlayer
do {
rain = try AKAudioFile(readFileName: "rain.wav")
rainPlayer = try AKAudioPlayer(file: rain, looping: true, deferBuffering: false, completionHandler: nil)
} catch { print(error) }
let mixer = AKMixer(leftPanner, rightPanner, rainPlayer)
//Put mixer in sound envelope
envelope = AKAmplitudeEnvelope(mixer)
envelope.attackDuration = 2.0
envelope.decayDuration = 0
envelope.sustainLevel = 1
envelope.releaseDuration = 0.2
//Start AudioKit stuff
AudioKit.output = envelope
AudioKit.start()
leftOscillator.start()
rightOscillator.start()
rainPlayer.start()
envelope.start()
soundIsPlaying = true
}
}
And here is an example of one of my sound effect view controllers, which reference the AudioKit singleton to send it a certain frequency (I have about a dozen of these view controllers, each with its own frequency settings):
class CalmView: UIViewController {
let leftOscillator = AKOscillator()
let rightOscillator = AKOscillator()
override func viewDidLoad() {
super.viewDidLoad()
leftOscillator.amplitude = 0.3
leftOscillator.frequency = 220
rightOscillator.amplitude = 0.3
rightOscillator.frequency = 230
}
#IBAction func playSound(_ sender: Any) {
if shared.soundIsPlaying == false {
AudioKit.stop()
shared.setupFrequency(left: leftOscillator, right: rightOscillator, frequency: "Calm")
} else if shared.soundIsPlaying == true && shared.currentFrequency != "Calm" {
AudioKit.stop()
shared.leftOscillator.stop()
shared.rightOscillator.stop()
shared.rainPlayer.stop()
shared.envelope.stop()
shared.setupFrequency(left: leftOscillator, right: rightOscillator, frequency: "Calm")
} else {
shared.soundIsPlaying = false
shared.envelope.stop()
}
}
}
I instantiated the AudioPlayer singleton in my ViewController.swift file.
It depends on when you are doing your configuration in relation to when AudioKit is started. If you're using AudioKit you should be using its AKSettings to manage your session category. Basically not only the playback category but also mixWithOthers. By default, does this:
/// Set the audio session type
#objc open static func setSession(category: SessionCategory,
with options: AVAudioSessionCategoryOptions = [.mixWithOthers]) throws {
So you'd do something like this in your ViewController:
do {
if #available(iOS 10.0, *) {
try AKSettings.setSession(category: .playAndRecord, with: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
} else {
// Fallback on earlier versions
}
} catch {
print("Errored setting category.")
}
So I think its a matter of getting that straight. It might also help to have inter-app audio set up. If you still have trouble and provide more information, I can help more, but this is as good an answer as I can muster based on the info you've given so far.

Empty array causing my app to crash in Swift

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.

How do I update textfield2 based on editing textfield1?

I like to update textfield2 as soon as totalAmount has been edited, I am trying the following:
totalAmount.addTarget(self, action: "changeTextField2:", forControlEvents: UIControlEvents.EditingDidEndOnExit)
func changeTextField2(textField: UITextField) {
print("Total amount: \(totalAmount.text)")
//amount vat - calculate vat based on textField
amountVAT.text = "88"
}
And i am saving the text field values with
func newItem(){
let context = self.context
let ent = NSEntityDescription.entityForName("List", inManagedObjectContext: context)
let nItem = List(entity: ent!, insertIntoManagedObjectContext: context)
let numberFormatter = NSNumberFormatter()
nItem.amountVAT = numberFormatter.numberFromString(amountVAT.text!)
.....more values
nItem.invoiceImage = UIImagePNGRepresentation(imageHolder.image!)
do {
try context.save()
} catch {
print(error)
}
}
func saveValues(sender: AnyObject) {
if nItem != nil {
editItem()
} else {
newItem()
}
dismissVC()
}
This can be done using UITextFieldDelegate Method use this function
1.)
textField1.delegate = self
2.)
func textFieldDidEndEditing(textField: UITextField)
{
if textField == textField1
{
textField2.text = textField1.text
}
}
now its upto you how you change the totalAmount.
Use the delegate function of UITextField
- textFieldDidEndEditing: for your textField1. Once in this function, you can get value of textField1 and you can edit textField2.
Apple Documentation for UITextFieldDelegate
Change the code to this totalAmount.addTarget(self, action: "changeTextField2:", forControlEvents: UIControlEvents.EditingChanged)
i.e change the UIControlEvents to "EditingChanged"

App crash on sendEvent method

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.

Resources