Search code examples
swiftmacoscocoansoutlineview

swift outline view with two sub children


i working with swift 4 for macOS and i have a NSOutlineView:

enter image description here

i get the data from core data.

structure:

  • entity Person (relationship to entity Book)
  • entity Book

My Code for this result:

@IBOutlet weak var myOutlineView: NSOutlineView!

    let context = (NSApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    var people = [Person]()

    override func viewWillAppear() {
       requestPeople()
    }

    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: self) as? CustomCell
        if let person = item as? Person {
           // Show Person
        } else if let book = item as? Book {
            // Show Books
        }
        return view
    }


    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if let person = item as? Person {
            return person.books.count
        }
        return people.count
    }


    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if let person = item as? Person {
            return person.books[index]
        }
        return people[index]
    }


    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        if let person = item as? Person {
            return person.books.count > 0
        }
        return false
    }



    func requestPeople() {
        let request = NSFetchRequest<Person>(entityName: "Person")
        do {
            people = try context.fetch(request)
            myOutlineView.reloadData()
        } catch { print(error) }
    }

now my problem: i would like create another outline view.

My Book entity looks like this (attributes):

  • name
  • creationDate

My new outlineview should get this structure:

+ Year
++ Month
+++ Bookname

but i dont know how can I realize this structure. It is different as my first outline view.

can somebody help me?

=======

i guess that i have create arrays for year and month without duplicates. for this i try a this function to get the data:

 var year = [String]()
 var month = [String]()
 var books = [Book]()

    func requestBooks() {
        let request = NSFetchRequest<Book>(entityName: "Book")
        do {
            books = try context.fetch(request)

            for x in 0 ...< books.count {

                if !year.contains("\(Calendar.current.component(.year, from: books[x].creationDate))") {
                    year.append("\(Calendar.current.component(.year, from: books[x].creationDate))")
                }

                if !month.contains("\(Calendar.current.component(.month, from: books[x].creationDate))") {
                    month.append("\(Calendar.current.component(.month, from: books[x].creationDate))")
                }

            }

            myOutlineView.reloadData()
        } catch { print(error) }
    }

Solution

  • A multi-level outline is easier to manage when your underlying data structure is hierarchical (i.e. a tree structure).

    Here's an example of how you can create a "Tree" node class for your Books:

    class BookNode
    {
       // levels and relationships, from parent to children
       enum Level { case Top, Year, Month, Book }       
       let subLevels:[Level:Level] = [ .Top:.Year, .Year:.Month, .Month:.Book ]       
    
       var label    = ""               // description and unique "key"
       var level    = Level.Top        
       var children : [BookNode] = []       
       var book     : Book! = nil      // .Book level will store the actual Book
    
       // add book to hierarchy, auto-create intermediate levels        
       func add(_ book:Book) 
       {
          var subLabel = ""
          switch level
          {
             case .Top   : subLabel   = String(Calendar.current.component(.year, from:book.creationDate))
             case .Year  : subLabel   = String(Calendar.current.component(.month, from:book.creationDate))
             case .Month : subLabel   = book.name
             case .Book  : self.book  = book  // last level stores the book 
                           return             // and has no children
          }                     
          // Add branch (.Year, .Month) or leaf (.Book) node as needed
          var subNode:BookNode! = children.first{$0.label == subLabel}
          if subNode == nil 
          { 
            subNode       = BookNode() 
            subNode.level = subLevels[level]!
            subNode.label = subLabel
            children.append(subNode)
          }
          // keep adding recursively down to .Book level          
          subNode.add(book) 
       }
    }
    

    Your data will be stored in a hierarchy of BookNodes which you can load from your fetch request (you can pre-sort it, as I did, or leave that up to the BookNode class)

    var topNode = BookNode()
    
    func requestBooks() 
    {
      let request = NSFetchRequest<Book>(entityName: "Book")
      do {
           let books = try context.fetch(request)
    
           topNode = BookNode()
           for book in books.sorted(by:{$0.creationDate < $1.creationDate}) 
           {
              topNode.add(book)
           }
         }
    }
    

    With this, it will be easy to respond to your outline protocols using the BookNodes as the outline items:

    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? 
    {
      let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: self) as? CustomCell
    
      let node = (item as? BookNode) ?? topNode
      switch node.level
      { 
         case .Year    : // show year      : node.label
         case .Month   : // show month     : node.label
         case .Book    : // show book name : node.label and/or node.book
         default       : break
      }
    
      return view
    }
    
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int 
    {        
      let node = (item as? BookNode) ?? topNode
      return node.children.count        
    }
    
    
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any 
    {
      let node = (item as? BookNode) ?? topNode
      return node.children[index]        
    }
    
    
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool 
    {
      let node = (item as? BookNode) ?? topNode
      return node.children.count > 0        
    }
    

    If you program needs to allow adding/changing/removing individual books, the BookNode class can be used to reflect the individual changed (e.g. remove a book child or add a new one). You will then only need to call reloadData() on the outline without having to get everything back from the database.