5

I have a custom UITextView that takes advantage of Apple's TextKit by defining a custom NSTextStorage class, however, when I use my subclass for the custom text view's, text storage (as implemented below) and try opening any file greater than 20.0KB, the app crashes due to memory leakage: "Message from debugger: Terminated due to memory issue".

Oddly enough, if I replace my custom BMTextStorage with just a standard one, NSTextStorage, the text loads instantly without any memory leakage and uses < 30MB of RAM. What is causing this?

TextView.swift

class TextView : UITextView {

    required init(frame: CGRect) {

        // If I replace the following line with simply 
        // "let textStorage = NSTextStorage()"
        // I can open any file of any size and not have a memory leak
        // issue, using only about 20-30MB of RAM. If I ran this code
        // as is, it can open most files less than 20KB but will 
        // crash otherwise.
        let textStorage = BMTextStorage() 

        let layoutManager = NSLayoutManager()

        layoutManager.allowsNonContiguousLayout = true

        let textContainer = NSTextContainer(size: CGSizeMake(.max, .max))

        textContainer.widthTracksTextView = true
        textContainer.heightTracksTextView = true
        textContainer.exclusionPaths = [UIBezierPath(rect: CGRectMake(0.0, 0.0, 40.0, .max))]

        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        super.init(frame: frame, textContainer: textContainer)

        textStorage.delegate = self
        layoutManager.delegate = self

    }

}

BMTextStorage.swift

typealias PropertyList = [String : AnyObject]

class BMTextStorage : NSTextStorage {

    override var string: String {
        return storage.string
    }

    private var storage = NSMutableAttributedString()

    override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> PropertyList {
        return storage.attributesAtIndex(location, effectiveRange: range)
    }

    override func replaceCharactersInRange(range: NSRange, withString str: String) {
        storage.replaceCharactersInRange(range, withString: str)
        edited([.EditedAttributes, .EditedCharacters], range: range, changeInLength: str.length - range.length)
    }

    override func setAttributes(attrs: PropertyList?, range: NSRange) {
        storage.setAttributes(attrs, range: range)
        edited([.EditedAttributes], range: range, changeInLength: 0)
    }

    override func processEditing() {
        super.processEditing()
    }

 }
rolling_codes
  • 15,174
  • 22
  • 76
  • 112
  • Can you narrow it down to one of the four methods that you override? (And `processEditing()` can be removed altogether, as it doesn't do anything.) – NRitH Jun 21 '16 at 19:25
  • @NRitH I can't narrow it down, easily as those methods are required by the abstract class to be overridden. And yes the problem doesn't change if I omit `processEditing()` I tried breakpoints I suppose, but it just loops forever before it crashes especially for large files – rolling_codes Jun 21 '16 at 19:26

1 Answers1

10

Wow.... weird, it got fixed when I changed the type of storage to NSTextStorage....

typealias PropertyList = [String : AnyObject]

class BMTextStorage : NSTextStorage {

    overrride var string: String {
        return storage.string
    }

    private var storage = NSTextStorage()

    override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> PropertyList {
        return storage.attributesAtIndex(location, effectiveRange: range)
    }

    override func replaceCharactersInRange(range: NSRange, withString str: String) {
        storage.replaceCharactersInRange(range, withString: str)
        edited([.EditedAttributes, .EditedCharacters], range: range, changeInLength: str.length - range.length)
    }

    override func setAttributes(attrs: PropertyList?, range: NSRange) {
        storage.setAttributes(attrs, range: range)
        edited([.EditedAttributes], range: range, changeInLength: 0)
    }

    override func processEditing() {
        super.processEditing()
    }

 }
rolling_codes
  • 15,174
  • 22
  • 76
  • 112
  • 1
    Genius move! I wonder what freak cases this will break under, though. And do you now have to call `beginEditing`/`endEditing` for both `self` and `storage`? But the memory problems are gone so far. – ctietze Jul 13 '17 at 07:46
  • 1
    @ctietze oddly enough, I only make `beginEditing` and `endEditing` calls for `self` and it works out fine. I believe those methods are used primarily to help application manage the document state – rolling_codes Jul 16 '17 at 22:35
  • 2
    I can't help but feel like this is a massive code smell from Apple's API... Also, why the /heck/ NSTextStorage is an "abstract" class when that makes no sense in Objective-C is another great indication that there is some black magic behind this powerful class. Thanks for the tip, though! +1 – Bruno Philipe Aug 27 '17 at 01:03
  • Awesome finding! like 4 years later – Ayoub Khayati Nov 24 '20 at 17:49
  • Thanks for the finding. I can't get them really smooth. It's still a bit flaky. I used `NSTextStorage()` and removed all beginEditing/endEditing from my overwritten implementation. Some ideas how to get awesome scroll experience? – Peter Shaw May 03 '21 at 10:50
  • 1
    Got it. Called `setApperance()` in a draw method. :facepalm: Thanks for the finding, even in 2021! – Peter Shaw May 03 '21 at 11:05