I am trying to make a file browser in my app that opens in a side panel (with a split view controller).
The source is a URL brought by a prepareForSegue method in the previous viewController.
Each time the vc loads i have the fatal error :
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
The compiler locates the error to where i declare :
outlineView.delegate = self
outlineView.dataSource = self
I tried :
1. Undoing and redoing all my outlets connections, by code, by
storyboard
2. Reconnecting delegates and datasource by code, by storyboard
3. I thought maybe something was wrong in my datasource method and i rewrote it 5 times
4. I tried to put my setDelegatesAndDatasource method in the viewDidAppear too, thinking it was a problem of view life cycle
I can't understand what's going on.
Thanks for your help.
'''
extension ViewControllerSource : NSOutlineViewDataSource, NSOutlineViewDelegate {
func setDelegatesAndDatasources(){
outlineView.delegate = self
outlineView.dataSource = self
}
// MARK: - NSOutlineView Datasource
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children[index]
}
return rootfileSystemItem
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.hasChildren()
}
return false
}
// MARK: - NSOutlineView Delegate
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let colIdentifier = tableColumn?.identifier else { return nil }
if colIdentifier == NSUserInterfaceItemIdentifier(rawValue: "col1") {
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "cell1")
guard let cell = outlineView.makeView(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView else { return nil }
if let collection = item as? FileSystemItem {
cell.textField?.stringValue = collection.name ?? "Title not available"
cell.textField?.isEditable = false
cell.textField?.wantsLayer = true
cell.imageView?.image = collection.icon
// cell.textField?.delegate = self
} else {
cell.textField?.stringValue = "unknown item"
cell.textField?.isEditable = false
cell.textField?.wantsLayer = true
}
return cell
} else {
return nil
}
}
}
'''
And here is the main viewController file :
'''
class ViewControllerSource: NSViewController {
#IBOutlet var outlineView: NSOutlineView!
var echo:Echo? {
didSet {
echo!.checkFolderIntegrity()
rootfileSystemItem = FileSystemItem(url: echo!.url)
let window = self.view.window?.windowController as! WindowControllerEcho
window.directoryPath.url = echo!.url
}
}
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var rootfileSystemItem: FileSystemItem! {
didSet {
displayItems()
outlineView.reloadData()
}
}
// MARK: - Initialization
override func viewDidLoad() {
super.viewDidLoad()
setDelegatesAndDatasources()
}
func displayItems(){
for fileSystemItem in rootfileSystemItem.children as [FileSystemItem] {
print("item : \(fileSystemItem)")
for subItem in fileSystemItem.children as [FileSystemItem] {
print("\(fileSystemItem.name) - \(subItem.name)")
}
}
}
}
extension ViewControllerSource : EchoDelegate {
func didLoad(echo: Echo) {
self.echo = echo
}
}
'''
The prepareForSegue code reveals the mistake:
You are setting echo in prepareForSegue. This causes to call the property observer didSet. However at this moment the view is not loaded yet and force unwrapping the type crashes.
The solution is to move the code in didSet into viewDidLoad and viewWillAppear and delete the property observer. Nevertheless I recommend to optional bind window
var echo : Echo!
override func viewDidLoad() {
super.viewDidLoad()
setDelegatesAndDatasources()
echo.checkFolderIntegrity()
rootfileSystemItem = FileSystemItem(url: echo.url)
}
override func viewWillAppear(_ animated : Bool) {
super.viewWillAppear(animated)
if let window = self.view.window?.windowController as? WindowControllerEcho {
window.directoryPath.url = echo.url
}
}
Setting delegate and dataSource once is sufficient. If you are using storyboard or Xib the most convenient way is to connect both in Interface Builder.
Related
I'm building a note-taking app for macOS and have a question about saving text whenever the user is editing the NSTextView. I use textDidChange function to detect any changes in the NSTextView and then save the changes to Core Data. However, my code will only save the first edit that the user makes, e.g. if I type hello in the NSTextView, it will only save h instead of hello.
I'm wondering how to fix it? Thank you for your help.
This is the code:
class ViewController: NSViewController, NSTextViewDelegate, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet var textArea: NSTextView!
#IBOutlet weak var noteList: NSTableView!
let context = (NSApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var notes = [Testnote]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
loadData()
textArea.textStorage?.setAttributedString(notes[0].noteItem!)
textArea.delegate = self
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: - Tableview stuff
func numberOfRows(in tableView: NSTableView) -> Int {
return notes.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "rowNo"), owner: self) as? NSTableCellView {
cell.textField?.stringValue = "sth here"
return cell
}
return nil
}
func tableViewSelectionDidChange(_ notification: Notification) {
if noteList.selectedRow >= 0 {
let selectedNote = notes[noteList.selectedRow]
textArea.textStorage?.setAttributedString(selectedNote.noteItem!)
}
}
func textDidChange(_ notification: Notification) {
if let textview = notification.object as? NSTextView {
notes[0].noteItem = textview.attributedString()
saveData()
}
}
func loadData() {
let request: NSFetchRequest<Testnote> = Testnote.fetchRequest()
do {
notes = try context.fetch(request)
} catch {
print("sth wrong")
}
}
func saveData() {
do {
try context.save()
} catch {
print("Error saving \(error)")
}
}
#IBAction func addButton(_ sender: Any) {
// Create new NSObject and assign values
let newnote = Testnote(context: context)
newnote.noteItem = textArea.attributedString()
(NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil)
}
#IBAction func delete(_ sender: NSButton) {
context.delete(notes[noteList.selectedRow])
do {
try context.save()
} catch {
print("Error")
}
}
}
Trying to implement the IGListKit library, I'm running into the issue that my cells are updated unnecessarily. I'm using a singleton adapter.dataSource with one section per row in the table.
Minimum example:
import IGListKit
class ContentItem: ListDiffable {
weak var item: Content?
weak var section: ContentSectionController?
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
init(item: Content?) {
self.item = item
}
}
class ContentSectionController: ListSectionController {
weak var object: ContentItem?
override func didUpdate(to object: Any) {
self.object = object as? ContentItem
self.object?.section = self
// should only be called on updates
}
override func sizeForItem(at index: Int) -> CGSize {
guard let content = object?.item else {
return CGSize(width: 0, height: 0)
}
// calculate height
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cell = collectionContext!.dequeueReusableCellFromStoryboard(withIdentifier: "ContentCell", for: self, at: index)
(cell as? ContentCell)?.item = object // didSet will update cell
return cell
}
override init() {
super.init()
self.workingRangeDelegate = self
}
}
extension ContentSectionController: ListWorkingRangeDelegate {
func listAdapter(_ listAdapter: ListAdapter, sectionControllerWillEnterWorkingRange sectionController: ListSectionController) {
// prepare
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerDidExitWorkingRange sectionController: ListSectionController) {
return
}
}
class ContentDataSource: NSObject {
static let sharedInstance = ContentDataSource()
var items: [ContentItem] {
return Content.displayItems.map { ContentItem(item: $0) }
}
}
extension ContentDataSource: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return items
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return ContentSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
/// VC ///
class ContentViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let updater = ListAdapterUpdater()
adapter = ListAdapter(updater: updater, viewController: self, workingRangeSize: 2)
adapter.collectionView = collectionView
adapter.dataSource = ContentDataSource.sharedInstance
}
var adapter: ListAdapter!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
adapter.performUpdates(animated: true)
}
// ...
}
On every view appear I call adapter.performUpdates(animated: true), which should never update the cells since isEqual is overridden with true. Nonetheless, all cells' didUpdate is triggered, calling cellForItem again too.
IGListKit requires both diffIdentifier and isEqual to be implemented with the IGListDiffable protocol in order to compare the identity/equality of two objects. (You're missing the diff identifier in your model).
My understanding is that under the hood, ListKit checks to see if the two diff identifiers of the objects are equal, if they are THEN it moves on to comparing them with isEqual.
Resources:
IGListKit Best Practices
IGListDiffable Protocol Reference
My NSTableView seems to be mirroring all content which draws a String.
I have never seen something like this before and hope somebody has a tip on how to solve this Problem. I already looked it up, but couldn't find anything. I also filed a bug report, but apple didn't respond.
First idea, that I have: It must have something to do with the NSTextField and NSPopUpButton being disabled at start. They are only enabled as soon as you click one cell. And when they are enabled the text gets displayed the right way. But I don't want to enable them at start to prevent changing values by accidentally clicking one cell.
My Code seems to be fine and compiled without problems.
The Program is a simple Database Program which takes an own created file type and reads its content. From the content it creates Database, Table, Column and cell objects at runtime to display the database content.
Here is my NSTableView Code:
import Cocoa
class TableContentViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var tableContent: NSTableView!
var columnDragFrom:Int = -1
override func viewDidLoad() {
super.viewDidLoad()
tableContent.dataSource = self
tableContent.delegate = self
// Do view setup here.
tableContent.backgroundColor = NSColor(named: "darkColor")!
NotificationCenter.default.addObserver(self, selector: #selector(reloadData(_:)), name: .tableUpdated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(cellSelection(_:)), name: .cellSelection, object: nil)
tableContent.selectionHighlightStyle = .none
}
#objc func cellSelection(_ notification: Notification) {
if let cell = notification.object as? NSTableCellView {
nxSelectionHandler.currentRow = tableContent.row(for: cell)
nxSelectionHandler.currentColumn = tableContent.column(for: cell)
nxSelectionHandler.highlightCell(sender: tableContent)
}
}
#objc func reloadData(_ notification: Notification) {
setupTable()
tableContent.reloadData()
}
func setupTable() {
tableContent.rowHeight = 30
while(tableContent.tableColumns.count > 0) {
tableContent.removeTableColumn(tableContent.tableColumns.last!)
}
if nxSelectionHandler.currentTable != nil {
for column in (nxSelectionHandler.currentTable?.nxColumns)! {
let newColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: column.title))
newColumn.title = column.title
tableContent.addTableColumn(newColumn)
}
}
}
func numberOfRows(in tableView: NSTableView) -> Int {
if nxSelectionHandler.currentTable != nil {
var rowCounts:[Int] = []
for column in (nxSelectionHandler.currentTable?.nxColumns)! {
rowCounts.append(column.nxCells.count)
}
return rowCounts.max()!
}
return 0
}
func tableView(_ tableView: NSTableView, mouseDownInHeaderOf tableColumn: NSTableColumn) {
self.columnDragFrom = tableView.tableColumns.firstIndex(of: tableColumn)!
}
func tableView(_ tableView: NSTableView, didDrag tableColumn: NSTableColumn) {
nxSelectionHandler.currentTable?.nxColumns.swapAt(columnDragFrom, tableView.tableColumns.firstIndex(of: tableColumn)!)
tableView.reloadData()
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let column = tableView.tableColumns.firstIndex(of: tableColumn!)
if nxSelectionHandler.currentTable != nil {
let nxCell = nxSelectionHandler.currentTable?.nxColumns[column!].nxCells[row]
switch nxCell! {
case .nxString(let value):
var StringCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxString"), owner: self) as? StringCell
if StringCellView == nil {
tableView.register(NSNib(nibNamed: "StringCellNib", bundle: nil), forIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxString"))
StringCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxString"), owner: self) as? StringCell
}
StringCellView?.textField?.stringValue = value
return StringCellView
case .nxCheckbox(let state):
var CheckboxCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxCheckbox"), owner: self) as? CheckboxCell
if CheckboxCellView == nil {
tableView.register(NSNib(nibNamed: "CheckboxCellNib", bundle: nil), forIdentifier: NSUserInterfaceItemIdentifier("nxCheckbox"))
CheckboxCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxCheckbox"), owner: self) as? CheckboxCell
}
CheckboxCellView?.column = column!
CheckboxCellView?.row = row
CheckboxCellView?.checkbox.state = state
return CheckboxCellView
case .nxSelection(let selection, let options):
var SelectionCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxSelection"), owner: self) as? SelectionCell
if SelectionCellView == nil {
tableView.register(NSNib(nibNamed: "SelectionCellNib", bundle: nil), forIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxSelection"))
SelectionCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nxSelection"), owner: self) as? SelectionCell
}
SelectionCellView?.column = column!
SelectionCellView?.row = row
for option in options {
SelectionCellView?.selection.addItem(withTitle: option)
}
SelectionCellView?.selection.selectItem(at: selection)
return SelectionCellView
}
}
return nil
}
}
The objects used in the Code are all class types and cells are loaded from Nibs, where the cells all have constraints and are displayed the right way. A Screenshot of the NSTableView displaying the content wrong can be seen below.
Code of one of the custom cells:
import Cocoa
class StringCell: NSTableCellView, NSTextFieldDelegate {
var isSelected: Bool = false {
didSet {
self.needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
self.textField?.focusRingType = .none
self.textField?.textColor = NSColor.white
self.textField?.delegate = self
self.wantsLayer = true
self.layer?.borderWidth = 2
self.layer?.cornerRadius = 2
if isSelected {
self.layer?.borderColor = NSColor.systemBlue.cgColor
} else {
self.layer?.borderColor = NSColor.clear.cgColor
self.textField?.isEnabled = false
self.textField?.isEditable = false
}
}
override func mouseDown(with event: NSEvent) {
if self.isSelected {
self.textField?.isEditable = true
self.textField?.isEnabled = true
self.textField?.selectText(self)
} else {
self.isSelected = true
NotificationCenter.default.post(name: .cellSelection, object: self)
}
}
func controlTextDidEndEditing(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
nxSelectionHandler.currentCell = nxCell.nxString(textField.stringValue)
}
}
}
Yeah, that's weird. Did you check if you have any active content filters in the View Effects inspector?
View Effects inspector
I want to get the selected cell from the NSOutlineView control. I found a solution here: How can I get the selected cell from a NSOutlineView?. It says
Use the delegate. willDisplayCell: is called when a cell changes its selection state.
However when I test it, I found that my willDisplayCell: is not be called.
Here's my code, it can be run normally, but the willDisplayCell: method has never been called. Where did I make a mistake? Thanks.
class TreeNode: NSObject{
var name: String = ""
private(set) var isLeaf: Bool = false
var children: [TreeNode]?
init(name: String, isLeaf: Bool){
self.name = name
self.isLeaf = isLeaf
if !isLeaf{
children = [TreeNode]()
}
}
}
class ViewController: NSViewController {
#IBOutlet weak var sourceList: NSOutlineView!
private var data = TreeNode(name: "Root", isLeaf: false)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for i in 0..<10{
let node = TreeNode(name: "name \(i)", isLeaf: i % 2 == 0)
data.children?.append(node)
}
}
}
extension ViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let item = item as? TreeNode, !item.isLeaf {
return item.children!.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
return !((item as? TreeNode)?.isLeaf ?? false)
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let item = item as? TreeNode {
if item.isLeaf{
return item
}else{
return item.children![index]
}
}
return data
}
}
extension ViewController: NSOutlineViewDelegate {
func outlineView(_ outlineView: NSOutlineView, willDisplayCell cell: Any, for tableColumn: NSTableColumn?, item: Any) {
print("called")
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let cell: NSTableCellView?
if let item = item as? TreeNode{
cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as? NSTableCellView
cell?.textField?.stringValue = item.name
}else{
cell = nil
}
return cell
}
}
I've solved this problem myself. The following code is how to get the selected cell:
func getSelectedCell() -> NSTableCellView? {
if let view = self.sourceList.rowView(atRow: self.sourceList.selectedRow, makeIfNecessary: false) {
return view.view(atColumn: self.sourceList.selectedColumn) as? NSTableCellView
}
return nil
}
Now I can access the NSTextField control by the code getSelectedCell()?.textField.
I'm creating a NSOutlineView. When implementing the Data Source, although I'm able to create the top hierarchy I can not implement the childHierarchy. The reason is that I can't read the item: AnyObject? which prevents me from returning the right array from the dictionary.
//MARK: NSOutlineView
var outlineTopHierarchy = ["COLLECT", "REVIEW", "PROJECTS", "AREAS"]
var outlineContents = ["COLLECT":["a","b"], "REVIEW":["c","d"],"PROJECTS":["e","f"],"AREAS":["g","h"]]
//Get the children for item
func childrenForItem (itemPassed : AnyObject?) -> Array<String>{
var childrenResult = Array<String>()
if(itemPassed == nil){ //If no item passed we return the highest level of hirarchy
childrenResult = outlineTopHierarchy
}else{
//ISSUE HERE:
//NEED TO FIND ITS TITLE to call the correct child
childrenResult = outlineContents["COLLECT"]! //FAKED, should be showing the top hierarchy item so I could return the right data
}
return childrenResult
}
//Data source
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject{
return childrenForItem(item)[index]
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool{
if(outlineView.parentForItem(item) == nil){
return true
}else{
return false
}
}
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int{
return childrenForItem(item).count
}
func outlineView(outlineView: NSOutlineView, viewForTableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
// For the groups, we just return a regular text view.
if (outlineTopHierarchy.contains(item as! String)) {
let resultTextField = outlineView.makeViewWithIdentifier("HeaderCell", owner: self) as! NSTableCellView
resultTextField.textField!.stringValue = item as! String
return resultTextField
}else{
// The cell is setup in IB. The textField and imageView outlets are properly setup.
let resultTextField = outlineView.makeViewWithIdentifier("DataCell", owner: self) as! NSTableCellView
resultTextField.textField!.stringValue = item as! String
return resultTextField
}
}
}
I used this as a reference, although it's Objective-C implemented
You need to cast the item to the correct type for your outline. Generally you'd want to use a real data model, but for your toy problem with exactly two levels in the hierarchy, this suffices:
func childrenForItem (itemPassed : AnyObject?) -> Array<String>{
if let item = itemPassed {
let item = item as! String
return outlineContents[item]!
} else {
return outlineTopHierarchy
}
}