I've just started working on my first project for macOS and am having trouble setting up a NSTableView. When I run it the window will appear but there is nothing in it. I've made sure all the objects have the correct class in the identity inspector and can't seem to find what I'm doing wrong.
The goal of the app is to make a notes app. I want a tableView which displays the titles of all the notes in the database, in a single column, so when you click on the cell the note will then be displayed in the rest of the window.
Here's the code:
import Foundation
import AppKit
import SQLite
class NoteCloudVC: NSViewController {
// Declare an array of Note objects for populating the table view
var notesArray: [Note] = []
// IBOutlets
#IBOutlet weak var tableView: NSTableView!
// ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// set the tableViews delegate and dataSource to self
tableView.delegate = self
tableView.dataSource = self
//Establsih R/W connection to the db
do {
let path = NSSearchPathForDirectoriesInDomains(
.applicationSupportDirectory, .userDomainMask, true
).first! + "/" + Bundle.main.bundleIdentifier!
// create parent directory iff it doesn’t exist
try FileManager.default.createDirectory(
atPath: path,
withIntermediateDirectories: true,
attributes: nil
)
let db = try Connection("\(path)/db.sqlite3")
//Define the Notes Table and its Columns
let notes = Table("Notes")
let id = Expression<Int64>("ID")
let title = Expression<String>("Title")
let body = Expression<String>("Body")
/*
Query the data from NotesAppDB.sqlite3 into an array of Note objs
Then use that array to populate the NSTableView
*/
for note in try db.prepare(notes) {
let noteToAdd = Note(Int(note[id]), note[title], note[body])
notesArray.append(noteToAdd)
}
} catch {
print(error)
}
}
// viewWillAppear
override func viewWillAppear() {
super.viewWillAppear()
tableView.reloadData()
}
}
// NSTableViewDataSource Extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDataSource {
// Number of rows ~ returns notesArray.count
func numberOfRows(in tableView: NSTableView) -> Int {
return notesArray.count
}
}
// NSTableViewDelegate extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDelegate {
// Configures each cell to display the title of its corresponding note
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
//configure the cell
if tableColumn?.identifier == NSUserInterfaceItemIdentifier(rawValue: "NotesColumn") {
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "NotesCell")
guard let noteCell = tableView.makeView(withIdentifier: cellIdentifier, owner: self) as? NotesCell else { return nil }
let note = notesArray[row]
noteCell.noteTitle.stringValue = note.title
return noteCell
}
return nil
}
}
// NotesCell class
class NotesCell: NSTableCellView {
// IBOutlet for the title
#IBOutlet weak var noteTitle: NSTextField!
}
I'm pretty familiar with UIKit so I thought the learning curve of AppKit would be a little better than SwiftUI, so if anyone could provide some guidance about where I've gone wrong that would be very much appreciated. Also if it will be a better use of my time to turn towards SwiftUI please lmk.
Here's the values while debugging:
It's reading the values from the table correctly, so I've at least I know the problem lies somewhere in the tableView functions.
The most confusing part is the fact that the header doesn't even show up. This is all I see when I run it:
Here are some images of my storyboard as well:
This is for an assignment for my software modeling and design class where my professor literally doesn't teach anything. So I'm very thankful for everyone who helps with this issue because y'all are basically my "professors" for this class. When I move the tableView to the center of the view controller in the story board I can see a little dash for the far right edge of the column but that's it, and I can't progress any further without this tableView because the whole app is dependant upon it.
So, it turns out that the code itself wasn't actually the problem. I had always used basic swift files when writing stuff for iOS so it never occured to me that I'd need to import Cocoa to use AppKit but that's where the problem lied all along. Using this code inside the auto-generated ViewController class that had Cocoa imported did the trick. Also I got rid of the extensions and just did all the Delegate/ DataSource func's inside the viewController class.
Related
Background: I've been teaching myself Swift, iOS and macOS for a few weeks and I've been trying to put together a 'Price Calculator' for macOS that grabs from a Database of paper and uses the selected properties to select a piece of paper. The user would be able to select from a list of properties such as: Size, Weight, Finish, Color, and Brand. From there it would calculate a price of the paper using general arithmetic.
At first I thought about using a SQLite or a similar Database style program to populate my Calculator – but the User needs to be able to populate the Paper Database themselves. Thus, I've started working with Core Data.
Problem: I have a tableView that's taking the Core Data and displaying the different paper's available – or at least it should be doing that. For testing, I've created a 'Add Paper' button that generates a random Paper and adds it to the Database. I know this is creating random random Paper because I'm able to display it in the Output. But I don't actually know if it's saving.
The problem I have is that when I try to refresh the table and display the Data to the tableView... it either Crashes due to Nil values or it does not add to the tableView.
I've come to the conclusion that I may be displaying the Data wrong to the table -OR- I'm not properly saving the Data -OR- I'm not fetching the Data correctly.
The resources online for iOS only helps so much as this is for Mac OS Any help in this would be appreciated. My Code can be found below and Thank you in advance: NOTE: I'm using the boilerplate AppDelegate file from the CoreData.
Defined in ViewController:
private var papers = [FlatPaper]()
private var appDelegate = NSApplication.shared.delegate as! AppDelegate
private let context = (NSApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
The Add Random Paper button (for testing):
#IBAction func addPaper(_ sender: NSButton) {
let data = PaperData()
let paper = FlatPaper(entity: FlatPaper.entity(), insertInto: context)
paper.paperSize = data.paperSize
paper.paperWeight = data.paperWeight
paper.paperBrand = data.paperBrand
paper.paperColor = data.paperColor
paper.paperFinish = data.paperFinish
paper.paperPrice = data.paperPrice
appDelegate.saveAction(paper)
papers.append(paper)
print("Add Paper Button Pressed. \(paper)")
refresh()
tableView.reloadData()
}
Extensions NSTableDelegate and NSTableDataSource:
extension ViewController: NSTableViewDataSource, NSTableViewDelegate {
// Mark: - Specifying how many rows
func numberOfRows(in tableView: NSTableView) -> Int {
return papers.count
}
// Mark: - Populating the Columns and Rows with Data
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
let paperSpecs = papers[row]
if tableColumn!.title == "Paper Size" {
return paperSpecs.paperSize
} else if tableColumn!.title == "Paper Weight" {
return paperSpecs.paperWeight
} else if tableColumn!.title == "Paper Brand" {
return paperSpecs.paperBrand
} else if tableColumn!.title == "Paper Color" {
return paperSpecs.paperColor
} else if tableColumn!.title == "Paper Finish" {
return paperSpecs.paperFinish
} else {
return paperSpecs.paperPrice
}
}
// Mark: - Refresh Method for reloading all of the data
private func refresh() {
do {
papers = try context.fetch(FlatPaper.fetchRequest())
} catch let error as NSError {
print("Could not fetch. \(error), USER INFO: \(error.userInfo)")
}
}
}
In tableview:objectValueFor you should get your paper specs object from your array using the row argument instead of creating a new (empty) instance.
let paperSpecs = papers[row]
You might also want to look into NSFetchedResultsController, it’s really helpful when working with Core data and table views.
I have a NSTextView field which filters a NSTable table as user types in the input. I have successfully implemented table filtering.
Now, my goal is to auto-select the first result (the first row in the table) and allow user to use arrow keys to move between the results while typing the search query. When moving between the results in the table, the input field should stay focused. (This is similar to how Spotlight works).
This is how the app looks now:
This is my ViewController:
import Cocoa
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {
#IBOutlet weak var field: NSTextField!
#IBOutlet weak var table: NSTableView!
var projects: [Project] = []
override func viewDidLoad() {
super.viewDidLoad()
projects = Project.all()
field.delegate = self
table.dataSource = self
table.delegate = self
}
override func controlTextDidChange(_ obj: Notification) {
let query = (obj.object as! NSTextField).stringValue
projects = Project.all().filter { $0.title.contains(query) }
table.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return projects.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "FirstCell"), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = projects[row].title
return cell
}
return nil
}
}
and this is Project class
struct Project {
var title: String = ""
static func all() -> [Project] {
return [
Project(title: "first project"),
Project(title: "second project"),
Project(title: "third project"),
Project(title: "fourth project"),
];
}
}
Thank you
This kinda, sorta has an answer already in the duplicate posted by #Willeke, but 1) that answer is in Objective-C, not Swift, 2) I can provide a somewhat more detailed answer (with pictures!), and 3) I'm brazenly going after the bounty (Rule of Acquisition #110). So, with that in mind, here's how I'd implement what you're trying to do:
Don't use an NSTextView; use an NSTextField, or even better, an NSSearchField. NSSearchField is great because we can set it up in Interface Builder to create the filter predicate with almost no code. All we have to do to do that is to create an NSPredicate property in our view controller, and then set up the search field's Bindings Inspector to point to it:
Then you can create an Array Controller, with its Filter Predicate bound to that same property, and its Content Array binding bound to a property on the view controller:
And, of course, bind the table view to the Array Controller:
Last but not least, bind the text field in your table's cell view to the title property:
With all that set up in Interface Builder, we hardly need any code. All we need is the definition of the Project class (all properties need to be marked #objc so that the Cocoa Bindings system can see them):
class Project: NSObject {
#objc let title: String
init(title: String) {
self.title = title
super.init()
}
}
We also need properties on our view controller for the projects, array controller, and filter predicate. The filter predicate needs to be dynamic so that Cocoa Bindings can be notified when it changes and update the UI. If projects can change, make that dynamic too so that any changes to it will be reflected in the UI (otherwise, you can get rid of dynamic and just make it #objc let).
class ViewController: NSViewController {
#IBOutlet var arrayController: NSArrayController!
#objc dynamic var projects = [
Project(title: "Foo"),
Project(title: "Bar"),
Project(title: "Baz"),
Project(title: "Qux")
]
#objc dynamic var filterPredicate: NSPredicate? = nil
}
And, last but not least, an extension on our view controller conforming it to NSSearchFieldDelegate (or NSTextFieldDelegate if you're using an NSTextField instead of an NSSearchField), on which we'll implement the control(:textView:doCommandBy:) method. Basically we intercept text-editing commands being performed by the search field's field editor, and if we get moveUp: or moveDown:, return true to tell the field editor that we will be handling those commands instead. For everything other than those two selectors, return false to tell the field editor to do what it'd normally do.
Note that this is the reason that you should use an NSTextField or NSSearchField rather than an NSTextView; this delegate method will only be called for NSControl subclasses, which NSTextView is not.
extension ViewController: NSSearchFieldDelegate {
func control(_: NSControl, textView _: NSTextView, doCommandBy selector: Selector) -> Bool {
switch selector {
case #selector(NSResponder.moveUp(_:)):
self.arrayController.selectPrevious(self)
return true
case #selector(NSResponder.moveDown(_:)):
self.arrayController.selectNext(self)
return true
default:
return false
}
}
}
Voilà!
(Of course, if you prefer to populate the table view manually instead of using bindings, you can ignore most of this and just implement control(:textView:doCommandBy:), updating your table's selection manually instead of asking your array controller to do it. Using bindings, of course, results in nice, clean code, which is why I prefer it.)
As #Willeke points out, this is likely a duplicate. The solution from that other question works here. I've converted it to swift and added some explanation.
I tested this with an NSSearchField instead of an NSTextField, but I expect it should work the same.
First, you need to add the NSControlTextEditingDelegate protocol to your ViewController, and add the following function:
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(moveUp(_:)) {
table.keyDown(with: NSApp.currentEvent!)
return true
} else if commandSelector == #selector(moveDown(_:)) {
table.keyDown(with: NSApp.currentEvent!)
return true
}
return false
}
You've already set the text field's delegate to the ViewController, so you're all set there.
This will cause your NSTextField to first check the delegate before executing the moveUp(_:) selector (triggered by pressing the up arrow). Here, the function responds saying "don't do what you normally do, the delegate will handle it" (by returning true) and sends the event to the NSTableView object instead. Focus is not lost on the text field.
I know that our IBOutlets should be private, but for example if I have IBOutlets in TableViewCell, how should I access them from another ViewController? Here is the example why I'm asking this kind of question:
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitle: UILabel!
}
if I assign to the IBOutlet that it should be private, I got an error in another ViewController while I'm accessing the cell property: 'bookTitle' is inaccessible due to 'private' protection level
If I understand your question correctly, you are supposing the #IBOutlet properties should be marked as private all the time... Well it's not true. But also accessing the properties directly is not safe at all. You see the ViewControllers, TableViewCells and these objects use Implicit unwrapping on optional IBOutlets for reason... You don't need to init ViewController when using storyboards or just when using them somewhere in code... The other way - just imagine you are creating VC programmatically and you are passing all the labels to the initializer... It would blow your head... Instead of this, you come with this in storyboard:
#IBOutlet var myLabel: UILabel!
this is cool, you don't need to have that on init, it will just be there waiting to be set somewhere before accessing it's value... Interface builder will handle for you the initialization just before ViewDidLoad, so the label won't be nil after that time... again before AwakeFromNib method goes in the UITableViewCell subclass, when you would try to access your bookTitle label property, it would crash since it would be nil... This is the tricky part about why this should be private... Otherwise when you know that the VC is 100% on the scene allocated there's no need to be shy and make everything private...
When you for example work in prepare(for segue:) method, you SHOULD NEVER ACCESS THE #IBOutlets. Since they are not allocated and even if they were, they would get overwritten by some internal calls in push/present/ whatever functions...
Okay that's cool.. so what to do now?
When using UITableViewCell subclass, you can safely access the IBOutlets (ONLY IF YOU USE STORYBOARD AND THE CELL IS WITHIN YOUR TABLEVIEW❗️)
and change their values... you see
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// We shouldn't return just some constructor with UITableViewCell, but who cares for this purposes...
guard let cell = tableView.dequeueReusableCell(withIdentifier: "bookTableViewCell", for: indexPath) else { return UITableViewCell() }
cell.bookTitle.text = "any given text" // This should work ok because of interface builder...
}
The above case should work in MVC pattern, not MVVM or other patterns where you don't use storyboards with tableViewControllers and embed cells too much... (because of registering cells, but that's other article...)
I will give you few pointers, how you can setup the values in the cell/ViewController without touching the actual values and make this safe... Also good practice (safety) is to make the IBOutlets optional to be 100% Safe, but it's not necessary and honestly it would be strange approach to this problem:
ViewControllers:
class SomeVC: UIViewController {
// This solution should be effective when those labels could be marked weak too...
// Always access weak variables NOT DIRECTLY but with safe unwrap...
#IBOutlet var titleLabel: UILabel?
#IBOutlet var subtitleLabel: UILabel?
var myCustomTitle: String?
var myCustomSubtitle: String?
func setup(with dataSource: SomeVCDataSource ) {
guard let titleLabel = titleLabel, let subtitleLabel = subtitleLabel else { return }
// Now the values are safely unwrapped and nothing can crash...
titleLabel.text = dataSource.title
subtitleLabel.text = dataSource.subtitle
}
// WHen using prepare for segue, use this:
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = myCustomTitle
subtitleLabel.text = myCustomSubtitle
}
}
struct SomeVCDataSource {
var title: String
var subtitle: String
}
The next problem could be this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? SomeVC else { return }
let datasource = SomeVCDataSource(title: "Foo", subtitle: "Bar")
// This sets up cool labels... but the labels are Nil before the segue occurs and even after that, so the guard in setup(with dataSource:) will fail and return...
destinationVC.setup(with: datasource)
// So instead of this you should set the properties myCustomTitle and myCustomSubtitle to values you want and then in viewDidLoad set the values
destinationVC.myCustomTitle = "Foo"
destinationVC.myCustomSubtitle = "Bar"
}
You see, you don' need to set your IBOutlets to private since you never know how you will use them If you need any more examples or something is not clear to you, ask as you want... Wish you happy coding and deep learning!
You should expose only what you need.
For example you can set and get only the text property in the cell.
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitleLabel: UILabel!
var bookTitle: String? {
set {
bookTitleLabel.text = newValue
}
get {
return bookTitleLabel.text
}
}
}
And then, wherever you need:
cell.bookTitle = "It"
Now outer objects do not have access to bookTitleLabel but are able to change it's text content.
What i usually do is configure method which receives data object and privately sets all it's outlets features.
I haven't come across making IBOutlets private to be common, for cells at least. If you want to do so, provide a configure method within your cell that is not private, which you can pass values to, that you want to assign to your outlets. The function within your cell could look like this:
func configure(with bookTitle: String) {
bookTitle.text = bookTitle
}
EDIT: Such a function can be useful for the future, when you change your cell and add new outlets. You can then add parameters to your configure function to handle those. You will get compiler errors everywhere, where you use that function, which allows you to setup your cell correctly wherever you use it. That is helpful in a big project that reuses cells in different places.
I am trying to build an NSCollectionView filled with multiple editable TextViews. (OS X app in Swift.) My subclass of NSCollectionViewItem is called NoteViewItem. I am trying to have the program detect when one of the TextView has changed. I tried using both controlTextDidChange and textDidChange in the NoteViewItem's delegate with test print statement to see which would work. ControlTextDidChange did nothing; textDidChange recognized a change happened, so I went with that.
The problem is that textDidChange appears to point to a different NoteViewItem than the one that was shown on screen in the first place. It wasn't able to recognize the variable (called theNote) set in the original NoteViewItem; when I ask NoteViewItem to print String(self), I get two different results, one while setting the initial text and one in textDidChange. I'm wondering if I've set up my delegates and outlets wrongly. Any thoughts on why my references are off here?
Here's my code for NoteViewItem:
import Cocoa
class NoteViewItem: NSCollectionViewItem, NSTextViewDelegate
{
// MARK: Variables
#IBOutlet weak var theLabel: NSTextField!
#IBOutlet var theTextView: NSTextView!
var theNote: Note?
{
didSet
{
// Pre: The NoteViewItem's theNote property is set.
// Post: This observer has set the content of the *item's text view*, and label if it has one.
guard viewLoaded else { return }
if let theNote = theNote
{
// textField?.stringValue = theNote.noteText
theLabel.stringValue = theNote.filename
theTextView.string = theNote.noteText
theTextView.display()
print("theTextView.string set to "+theTextView.string!+" in NoteViewItem "+String(self))
}
else
{
theLabel.stringValue = "Empty note?"
}
}
}
// MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
// Hopefully this will set the note's background to white.
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.whiteColor().CGColor
}
// MARK: - NSTextViewDelegate
/*
override func controlTextDidChange(notification: NSNotification)
{
print("Control text changed.")
}
*/
func textDidChange(notification: NSNotification)
{
if let noteyMcNoteface = theNote
{
print("On edit, we have a note: "+String(noteyMcNoteface))
}
else
{
print("On edit, we have no note. I am NoteViewItem "+String(self))
}
}
}
I figured it out. My delegate, in the TextView, was connected to the wrong object in the Interface Builder for NoteViewItem.xib. I had connected it to the object labelled Note View Item, under objects in the outline. It should have been connected to File's Owner instead, since File's Owner stands for the NoteViewItem.swift class associated with the xib.
You'd think that if you want to connect the delegate to the NoteViewItem class and there is exactly one Note View Item listed in the outline, then that Note View Item is the thing you want to connect it to. Nope, you connect it to something entirely different that isn't called the Note View Item but is the Note View Item. I'm glad Interface Builder makes things so simple.
I have the following code in one of my classes.
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
parkCode = tableView.cellForRowAtIndexPath(indexPath).text
RTATab.codeText.text = parkCode.substringToIndex(3)
RTATab.codeLetter.text = parkCode.substringFromIndex(3)
self.dismissModalViewControllerAnimated(true)
}
The RTATab referenced above is another class I have made (type UIViewController) and in that class I have declared it as a global class as show below as I need to access some of the textfields (codeText and codeLetter) in its view.
import UIKit
import messageUI
import CoreData
import QuartzCore
var RTATab : ViewController = ViewController()
class ViewController: UIViewController, MFMessageComposeViewControllerDelegate {
//some code
}
When I run this, I get a can't unwrap optional.none error on the line RTATab.codeText.text = parkCode.substringToIndex(3).
Can someone please help. Do I need to have an initialiser in viewController class?
Thanks
You are getting this error the text of the cell is nil. Before calling methods on parkCode, you must first check if it is nil:
let possibleParkCode = tableView.cellForRowAtIndexPath(indexPath).text
if let parkCode = possibleParkCode {
RTATab.codeText.text = parkCode.substringToIndex(3)
RTATab.codeLetter.text = parkCode.substringFromIndex(3)
}
You're getting the error because RTATab.codeText and RTATab.codeLetter are nil -- the way you're initializing RTATab doesn't actually link up its properties with the text fields in your storyboard. If you truly just need a global version of the view controller, you'd need to give it a storyboard ID and load it using something like:
var RTATab: ViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("RTA") as ViewController
However, my guess is that it's a view controller you've already displayed elsewhere that you want to update, in which case you're better off just setting up a data structure that can hold the "parkCode" values and pulling them back into the correct view controller when it's time to display them.
Either parkCode or RTATab.codeText don't exist (are nil). You need to check for their existence prior to dereferencing either of them.
override func tableView (tableView: ...) {
if let theParkCode = tableView.cellForRowAtIndexPath(indexPath).text {
parkCode = theParkCode;
if let theCodeText = RTATab.codeText {
theCodeText.text = parkCode?.substringToIndex(3)
}
if let theCodeLetter ... {
// ...
}
}
}
Note: the above code depends on how your ViewController (the class of RTATab) declares its instance variables for codeText and codeLetter - I've assumed as optionals.