swiftmacosnsnotificationcenternstextviewnscollectionview

Trying to create new NSTextView every time the attributed string exceeds a certain height?


I'm trying to add a new NSTextView to the last index in my collection view every time the attributed string exceeds a certain bounds. The code works perfectly until the 5th item then it starts its starts creating an item every time the enter button is pressed. I'm thinking its a bug but im not sure. if any one can show me a better way to do it or improve the current code I have I would appreciate it. Below is my code:

Here is the CollectionViewItem

class DocumentItem: NSCollectionViewItem {

   var itemView: DocumentTextView?
   
   
   
   override func viewDidLoad() {
       super.viewDidLoad()
       self.itemView?.wantsLayer = true
       
       
   
       // Do view setup here.
   }

   override func loadView() {
       self.itemView = DocumentTextView(frame: NSZeroRect)
       self.view = self.itemView!
       
   }

   func getView() -> DocumentTextView {
       return self.itemView!
   }
   
   
   
}


Here is the collectionView datasource

func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
            return DocList.count
        }
            
        
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
       
            let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue:  "DocumentItem"), for: indexPath)
            
            return item
}

Here is the NSTextView subclass

class DocumentTextView: NSTextView {
    
    
    var theContainer = NSTextContainer()
    var theStorage = NSTextStorage()
    var theManager = NSLayoutManager()
    
    
    var table = NSTextTable()
    var pdfPage: PDFPage?
    
    


   

    override init(frame frameRect: NSRect) {
        super.init(frame: NSRect(origin: frameRect.origin, size: NSSize(width: 800, height: 1131 )), textContainer: theContainer)
        theStorage.addLayoutManager(theManager)
        theManager.addTextContainer(theContainer)
        self.textContainerInset = CGSize(width: 50, height: 50)
        self.textContainer?.widthTracksTextView = true
        self.textContainer?.heightTracksTextView = true
        self.textContainer?.lineBreakMode = .byWordWrapping
        self.maxSize = NSSize(width: 800, height: 1131)
        self.backgroundColor = NSColor.fromHexString("ffffff")!
        self.isRichText = true
       

    
        
    }
  


   
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
        
    }
}

Here is the function bringing the bug


  func textDidChange(_ notification: Notification) {
        var textView = notification.object as? DocumentTextView
        let numberOfItems = theDocumentOutlineView.numberOfItems(inSection: 0)
        let theLastTextView = theDocumentOutlineView.item(at: numberOfItems - 1) as! DocumentItem
        if textView == theLastTextView.itemView {
            print(textView?.attributedString().size())
            if (textView?.attributedString().size().height)! >= 1106.0 {
                self.DocList.append(2)
                var set = Set<IndexPath>()
                set.insert(NSIndexPath(forItem: self.DocList.count - 1 , inSection: 0) as IndexPath)
                theDocumentOutlineView.insertItems(at: set)
                theDocumentOutlineView.scrollToItems(at: set, scrollPosition: NSCollectionView.ScrollPosition.top)
                var newFirstResponder = theDocumentOutlineView.item(at: self.DocList.count - 1) as! DocumentItem
                newFirstResponder.itemView?.delegate = self
                self.view.window?.makeFirstResponder(newFirstResponder.itemView)
                
            
            }
        }
    }


Solution

  • Here's my test project, maybe it helps. The delegate of the text view is its view controller, the NSCollectionViewItem. The view controller of the collection view also receives NSText.didChangeNotification notifications to check the length of the text. heightTracksTextView of the text container is false.

    ViewController:

    class ViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate {
    
        @IBOutlet weak var collectionView: NSCollectionView!
        var docList: [DocumentObject] = [DocumentObject(index: 0, string: NSAttributedString(string: "New 0"))]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            collectionView.register(DocumentItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("DocumentItem"))
            NotificationCenter.default.addObserver(self, selector: #selector(textDidChange),
                name: NSText.didChangeNotification, object: nil)
        }
    
        func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
            return docList.count
        }
                
        func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
            if let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier("DocumentItem"),
                for: indexPath) as? DocumentItem {
                item.representedObject = docList[indexPath.item]
                return item
            }
            return NSCollectionViewItem()
        }
    
        @objc func textDidChange(_ notification: Notification) {
            if let textView = notification.object as? NSTextView {
                if let theLastItem = self.collectionView.item(at: self.docList.count - 1) as? DocumentItem,
                    textView === theLastItem.itemView {
                    //if textView.attributedString().size().height >= 1106.0 {
                    print("\(textView.attributedString().size().height) \(textView.layoutManager!.usedRect(for: textView.textContainer!).size.height)")
                    if let textContainer = textView.textContainer,
                        let heigth = textView.layoutManager?.usedRect(for: textContainer).size.height,
                        heigth >= 1106.0 {
                        DispatchQueue.main.async {
                            if let window = self.view.window,
                                window.makeFirstResponder(nil) { // end editing of previous item
                                self.docList.append(DocumentObject(index: self.docList.count - 1, string: NSAttributedString(string: "New \(self.docList.count)")))
                                let set: Set = [IndexPath(item: self.docList.count - 1, section: 0)]
                                self.collectionView.insertItems(at: set)
                                self.collectionView.scrollToItems(at: set, scrollPosition: NSCollectionView.ScrollPosition.top)
                                if let newItem = self.collectionView.item(at: self.docList.count - 1) as? DocumentItem {
                                    window.makeFirstResponder(newItem.itemView)
                                }
                            }
                        }
                    }
                }
            }
        }
    
    }
    

    DocumentItem:

    class DocumentItem: NSCollectionViewItem, NSTextViewDelegate {
    
        var itemView: DocumentTextView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do view setup here.
        }
    
        override func loadView() {
            self.itemView = DocumentTextView(frame: NSZeroRect)
            self.view = self.itemView!
            self.itemView?.delegate = self
        }
        
        override var representedObject: Any? {
            didSet {
                if let item = representedObject as? DocumentObject {
                    itemView?.textStorage?.setAttributedString(item.string)
                }
            }
        }
        
        func textDidEndEditing(_ notification: Notification) {
            if let item = representedObject as? DocumentObject {
                item.string = itemView?.textStorage?.copy() as! NSAttributedString
            }
        }
    
    }
    

    DocumentObject:

    class DocumentObject {
    
        var index: Int
        var string: NSAttributedString
        
        init(index: Int, string: NSAttributedString) {
            self.index = index
            self.string = string
        }
    
    }