Search code examples
iosswift2swift3ios10nsexception

Swift 2 to 3 Migration Error (libc++abi.dylib: terminating with uncaught exception of type NSException)


One of my controllers is oddly causing a runtime error crash when it didn't in Swift 2.

The error I get is.

libc++abi.dylib: terminating with uncaught exception of type NSException

I attached a screenshot showing that I have all my interface builder elements hooked up.

I did some experiments to see at which point the error occurs and I think its related to setting the textView possibly, but not sure.

1.) Notice the console messages stop after print("Draw Undo HTML"). I originally thought the error was related to the line after since I never saw the console message print("Set attributed text accordingly").

2.) Turns out though if I comment out the line incrementListTextView.attributedText = html.html2AttributedString

The error will still occur just the rest of the code runs before it happens. Very strange, but seeing as the error first seems to appear around that line I'm thinking its related to the TextView but not sure what it could be.

I attached images of this scenario as well.

enter image description here enter image description here

InventoryItemController.Swift (Full File Reference)

//
//  InventoryItemController.swift
//  Inventory Counter
//
//  Created by Joseph Astrahan on 4/3/16.
//  Copyright © 2016 Joseph Astrahan. All rights reserved.
//

import UIKit
import CoreData

class InventoryItemController: UIViewController, UITextFieldDelegate {

    var inventoryItem : Inventory?
    var m_incrementAmount = 0 //amount it will increment everytime you press (+)
    var m_itemHistory = [Int]() //create array to store undo/redo history
    var m_undoIndex = 0


    @IBOutlet weak var inventoryBarCodeLabel: UILabel!

    @IBOutlet weak var inventoryTotalLabel: UILabel!

    //List of increments
    @IBOutlet weak var incrementListTextView: UITextView!

    //Amount to increment by
    @IBOutlet weak var incrementAmountTextField: UITextField!

    @IBOutlet weak var inventoryNameNavItem: UINavigationItem!

    @IBAction func resetBarButtonAction(_: UIBarButtonItem) {
        //Present 'Are you sure?' Dialog & Reset Vars.

        // create the alert in local scope (no need for weak or unowned reference to self in closures)
        let alert = UIAlertController(title: "Are You Sure?", message: "This will delete all the counts you have done.", preferredStyle: UIAlertControllerStyle.alert)

        // add an action (button)
        alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.default, handler: { action in
            self.resetTotals()
        }))

        alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil))

        // show the alert
        self.present(alert, animated: true, completion: nil)

    }

    func resetTotals(){
        print("resetting...")
        m_itemHistory = [Int]()
        m_itemHistory.append(0)//first count is always 0 after reset.

        m_undoIndex = 0 //set back to 0

        updateTotal()//update total and save to disk.

        print("reset!...")
    }

    @IBAction func addInventoryButtonAction(_: UIButton) {

        //When you add you have to first trim the array of everything that was after it.  The redo history is now gone.
        let slice = m_itemHistory[0...m_undoIndex]
        m_itemHistory = Array(slice)//create new int array from the slice

        m_incrementAmount = Int(incrementAmountTextField.text!)!
        m_itemHistory.append(m_incrementAmount)

        m_undoIndex = m_undoIndex + 1 //add to the index, because undo always happens from last added~

        //Update addCount on actual inventoryItem (save to database)
        updateTotal()
    }

    @IBAction func undoButtonAction(_: UIButton) {
        print("undo")

        m_undoIndex = m_undoIndex - 1

        updateTotal()
    }

    @IBAction func redoButtonAction(_: UIButton) {
        print("redo")

        m_undoIndex = m_undoIndex + 1

        updateTotal()
    }


    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    func textFieldDidEndEditing(_ textField: UITextField){
       textField.resignFirstResponder()
    }

    func dismissKeyboard() {
        //Causes the view (or one of its embedded text fields) to resign the first responder status.
        view.endEditing(true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(LoginViewController.dismissKeyboard))
        self.view.addGestureRecognizer(tap)

        print("Showing Inventory Item In Question")
        print(inventoryItem)
        print("Inventory Name="+(inventoryItem?.name!)!)

        //inventoryNameLabel.text = inventoryItem.name!
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        //add observer for contentSize so it centers vertically
        incrementListTextView.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil)

        //Draw inventory name & barcode
        inventoryNameNavItem.title = inventoryItem?.name
        inventoryBarCodeLabel.text = inventoryItem?.barcode

        //Load first time only when view appears to set default
        m_itemHistory.append(Int((inventoryItem?.addCount!)!))//add initial value of last 'count'

        m_undoIndex = 0 //reset to start.

        updateTotal() //updates total to screen

        print("Finished UpdateTotal, Should be Showing Screen Now")

    }

    func updateTotal(){

        //Get the max Index we can count to -1 (because arrays are 0 indexed)
        let historyTotalMaxIndex = m_itemHistory.count - 1
        print("historyTotalCheck: HistoryTotalMaxIndex=\(historyTotalMaxIndex)")

        //Current state of undoIndex
        print("Init: m_undoIndex =\(m_undoIndex)")

        //Do checks to prevent out of index bounds.
        if(m_undoIndex<0){
            m_undoIndex = 0
        }

        if(m_undoIndex>historyTotalMaxIndex){
            m_undoIndex = historyTotalMaxIndex
        }

        //After modifying...
        print("Current: m_undoIndex =\(m_undoIndex)")


        //Draw HTML
        var html = "<html><font size=\"5\"><font color=\"#008800\"><center>"
        for index in 0...m_undoIndex {
            let increment = m_itemHistory[index]
            html = html + "+\(increment), "
        }
        html = html + "</center></font></font></html>"

        print(html)

        print("Draw Undo HTML")

        incrementListTextView.attributedText = html.html2AttributedString

        print("Set attributed text accordingly")

        //get sum of itemHistory

        let slice = m_itemHistory[0...m_undoIndex] //returns slice of the array we want.
        let sumArray = Array(slice)//create new int array from the slice

        print("SumArray Created")

        let sum = sumArray.reduce(0, +) //now we can sum up that new sliced array
        inventoryItem?.addCount = sum as NSNumber? //set the add count

        print("Reduced the Array")

        //reset html to different now
        html = "<font size=\"10\"><center>Current: \(inventoryItem?.currentCount!) , <font color=\"#008800\"> Counted: \(inventoryItem?.addCount!)</font></center></font>"

        inventoryTotalLabel.attributedText = html.html2AttributedString

        print("save the context")
        //Save the changes
        (UIApplication.shared.delegate as! AppDelegate).saveContext()

    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //have to remove or will crash
        incrementListTextView.removeObserver(self, forKeyPath: "contentSize")
    }

    /// Force the text in a UITextView to always center itself.
    func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutableRawPointer) {
        let textView = object as! UITextView
        var topCorrect = (textView.bounds.size.height - textView.contentSize.height * textView.zoomScale) / 2
        topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect;
        textView.contentInset.top = topCorrect
    }



    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }



}

extension String {
    var html2AttributedString: NSAttributedString? {
        guard let data = data(using: .utf8) else { return nil }
        do {
            return try NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue], documentAttributes: nil)
        } catch let error as NSError {
            print(error.localizedDescription)
            return  nil
        }
    }
    var html2String: String {
        return html2AttributedString?.string ?? ""
    }
}

Solution

  • Hello I found your issue and fixed here.

    The problem was you never followed Swift 3 style's method call for KVO Since Swift 3, method prototype has been changed.

    Your code is here:

    func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutableRawPointer) {
            let textView = object as! UITextView
            var topCorrect = (textView.bounds.size.height - textView.contentSize.height * textView.zoomScale) / 2
            topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect;
            textView.contentInset.top = topCorrect
    }
    

    But new prototype is following by:

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            let textView = object as! UITextView
            var topCorrect = (textView.bounds.size.height - textView.contentSize.height * textView.zoomScale) / 2
            topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect;
            textView.contentInset.top = topCorrect
        }
    

    Do you see differences here?

    I have also found another issue: You did adding observer in viewWillAppear function, please move it to viewDidLoad function, and move removeObserver code to deinit or override func didReceiveMemoryWarning().

    Here is the code:

    deinit {
            incrementListTextView.removeObserver(self, forKeyPath: "contentSize")
        }
    

    I hope this will help you greatly!