Unable to populate xib-created swift tableView using macOS - swift

I am unable to populate a swift cell-based tableview in macOS 10.14.6 using an Xcode 11.2 xib. The app is Document based and the tableView is created with a separate WindowController xib. A similar project created programmatically in Xcode works ok, including drag and drop; I am relatively new to using xibs and likely have not set things correctly. A column identifier has been set in the xib and NSTableViewDataSource and NSTableViewDelegate have been added to the Window Controller. Pertinent source code follows and the complete Xcode project may be downloaded here: https://www.dropbox.com/s/6tsb98b7iihhfxl/tableView.zip?dl=0
Any help in getting the tableView populated with a String array would be appreciated. I would also like to get drag and drop working but can get by for now just getting the array items to show up in the table view. It correctly creates four rows, corresponding to the number of elements in the array, but there is no visible text. The tableView is cell-based, but I could use view-based if that would work better. Thank you in advance.
class WindowController: NSWindowController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet var tableView: NSTableView!
var sports : [String] = ["Basketball","Baseball","Football","Tennis"]
override func windowDidLoad() {
super.windowDidLoad()
tableView.registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL])
tableView.dataSource = self
tableView.delegate = self
}
func numberOfRows(in tableView: NSTableView) -> Int {
return (sports.count)
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
var value : Any? = 0
let columnIdentifier : String = (tableColumn?.identifier.rawValue)!
if (columnIdentifier == "Col1"){
value = sports[row]
}
return value
}

In Document.swift windowController is released at the end of showTableView() and the table view looses its data source. Add windowController to the window controllers of the document or hold a strong reference to windowController.
#IBAction func showTableView(_ sender: Any) {
let windowController = WindowController.init(windowNibName:NSNib.Name("WindowController"))
addWindowController(windowController)
windowController.showWindow(nil)
}

Related

Swift: Update Layout and Content of ViewController when dismissing presented ViewController

I have a UIView which displays some information such as a user's Name and more, including a list of objects that all get pulled from my database. This works fine.
However, I now have a ViewController that gets presented on top of the current ViewController. In this presented ViewController, I am adding Data to my Database. When dismissing that view, I want the original ViewController to update all of its content to be up to date.
Right now, all my views are getting layedout in ViewDidLoad, meaning that they only really get loaded once and don't reload later on. I have managed to update Layout by calling self.view.layoutIfNeeded(), but if I understand correctly, this only updates constraint. Of course, I could call a new init of my original view controller. This would make it reload, but I would like to avoid that.
Another Idea I had was to set up all my content in the ViewWillAppear, which should maybe then update anytime my view controller is about to be visible. However, I don't know how to go about doing this. Can I just move all my setup code to viewWillAppear? Does this have any disadvantages?
TLDR: Is there a way to update a stackview with new elements without having to reload the full ViewController over ViewWillAppear?
The UITableView element works very smoothly with database data. If you fetch the data from your database inside viewDidLoad in your first view controller, and store it in an array, the UITableView (if you set up its dataSource correctly) will automatically populate the table with the new values from the second view controller. With this method, there is no need to use ViewWillAppear at all.
It sounds like as of now, you're using Views (inside a VStack)? to display individual objects from the database. If you want to keep whatever custom style/layout you're using with your views, this can be done by defining a custom subclass of UITableViewCell and selecting the "Also create XIB file" option. The XIB file lets you customize how the cells in your UITableView look.
Here is a simple example to show the database values in the first view controller automatically updating. I didn't include the custom XIB file (these are all default UITableViewCells), to keep it streamlined.
FIRST VIEW CONTROLLER
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var dataTable: UITableView!
var tableRows: [DataItem] = []
func loadData() {
let request: NSFetchRequest<DataItem> = DataItem.fetchRequest()
do {
tableRows = try Global_Context.fetch(request)
} catch {
print("Error loading data: \(error)")
}
}
override func viewDidLoad() {
super.viewDidLoad()
dataTable.dataSource = self
loadData()
}
#IBAction func goForward(_ sender: UIButton) {
self.performSegue(withIdentifier: "toSecond", sender: self)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableRows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dataTableCell", for: indexPath)
cell.textLabel?.text = tableRows[indexPath.row].name
return cell
}
}
let Global_Context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveContext () {
if Global_Context.hasChanges {
do {
try Global_Context.save()
} catch {
let nserror = error as NSError
print("Error saving database context: \(nserror), \(nserror.userInfo)")
}
}
}
SECOND VIEW CONTROLLER:
import UIKit
import CoreData
class AddViewController: UIViewController {
#IBOutlet weak var itemEntry: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
itemEntry.delegate = self
}
#IBAction func addNewItem(_ sender: UIButton) {
let newDataItem = DataItem(context: Global_Context)
newDataItem.name = itemEntry.text
saveContext()
}
#IBAction func goBack(_ sender: UIButton) {
self.performSegue(withIdentifier: "toFirst", sender: self)
}
}
extension AddViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return true
}
}
Main.storyboard:
Once you set up your view controller as a UITableViewDataSource (as in the example code), the table view should make things simpler by eliminating any need to manually manage individual Views.
Is this the functionality you were looking for? (Note about the example: it was set up in Xcode with "Use Core Data" enabled.)
Here is a link to the official documentation:
https://developer.apple.com/documentation/uikit/uitableview

Safari app extension popover not calling table view notification methods

I want to include a view-based NSTableView in the popover of a Safari App Extension.
Starting with the default project in Xcode, I made the SFSafariExtensionViewController the delegate and datasource for the table view as it is the only content on the popover, and mostly this works.
I can populate the table and implement methods like tableView(_:shouldSelectRow:), yet methods which return a notification object such as tableViewSelectionDidChange(_:) do not get called.
Whilst those methods show a cludgy way of knowing when a row is selected, I am left with no way of knowing when a cell is edited.
As I had to connect the delegate outlet of the NSTableView to the File Owner to allow the delegated methods to work, I also tried connecting the dataSource outlet too, but this rightly did not help.
Here is the essence of my code (which for now includes returning dummy table data to test editing):
class SafariExtensionViewController: SFSafariExtensionViewController {
#IBOutlet weak var tableView: NSTableView!
static let shared: SafariExtensionViewController = {
let shared = SafariExtensionViewController()
shared.preferredContentSize = NSSize(width:445, height:421)
return shared
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
func textDidEndEditing(_ notification: Notification) {
NSLog("I will NEVER appear in the console")
}
}
extension SafariExtensionViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return 5
}
}
extension SafariExtensionViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView
cellView?.textField?.stringValue = "Blah"
return cellView
}
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
NSLog("I will appear in the console")
return true
}
func tableViewSelectionDidChange(_ notification: Notification) {
NSLog("I will NEVER appear in the console")
}
func controlTextDidEndEditing(_ obj: Notification) {
NSLog("I will NEVER appear in the console")
}
}
(Obviously I do not need both textDidEndEditing(_:) and controlTextDidEndEditing(_:) but I am just trying everything.)
I am guessing the problem is something to do with the table view not being registered for notifications within a SFSafariExtensionViewController? That object inherits from NSViewController, though, so I would have thought these methods should work automatically.
This is my first time using swift, and it is a long time since I wrote a Mac app. But the actual functionality of the extension works, now I just want to have the ability to customize the settings through the UI.
However there seems to be very little written about Safari app extension programming, Apple's documentation is sparse, and I have not even been able to find any code examples featuring a table view in a popover to learn from.
I am probably missing something very obvious, but I have run out of searches to try on here and the web in general, so any help will be appreciated.
UPDATE:
I think I have an answer, by explicitly linking the NSTextFields in the table to the File's Owner as a delegate, the tableViewSelectionDidChange(_:) and controlTextDidEndEditing(_:) methods are now working. There must have been something else wrong causing the former to not work that I accidentally broke and fix, but it makes some sense for the latter.
That is all I need for the functionality to work, however I am still confused why the textDidEndEditing(_:) is still not working when I am led believe it should.
And in Apple's documentation, textDidEndEditing(_ :) is a method of an NSTextField, which links to a page saying controlTextDidEndEditing(_ :) is deprecated
And I misunderstanding anything?
I think you are not setting up the outlet properly please confirm this. Also check you setting up reusable identifier? identifier. for me all delegate calling without no issue after that.

Updating tableview binded to array error

I am extremely new to Swift so I am guessing this is a rookie mistake but here's my situation:
I am trying to create an application that allows you to add things to a list. I have a NSTextField where users input there data and a NSButton to add that data to the myTeam array. The tableView is binded (I think that's what you call it) to the myTeam array.
When I run the application a table containing the preset values of myTeam populate the table view correctly, but when I add to this array nothing happens. After research, I found this question.
Unfortunately, like most swift tutorials and Q&A, it was for iOS. But I thought I would give it a try. I added
self.tableView.reloadData()
self.refresher.endRefreshing()
to my code, and received two errors: Value of type '(NSTableView, NSTableColumn?, Int) -> Any?' has no member 'reloadData' and Value of type 'ViewController' has no member 'refresher', respectively.
Heres my code below:
import Cocoa
import Darwin
class ViewController: NSViewController, NSTableViewDataSource {
var myTeam = ["Test", "Test2"]
#IBOutlet weak var myText: NSTextField!
#IBOutlet weak var AddMember: NSButton!
#IBAction func addmem(_ sender: Any) {
myTeam.append(myText.stringValue);
self.tableView.reloadData()
self.refresher.endRefreshing()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: DataSource
func numberOfRows(in tableView: NSTableView) -> Int {
return myTeam.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return myTeam[row]
}
}
P.S. I used this tutorial to get as far as I did.
Simplest solution, we are going to use (full) Cocoa Bindings:
Replace the entire code with
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var myText: NSTextField!
#objc dynamic var myTeam = ["Test", "Test2"]
#IBAction func addmem(_ sender: NSButton) {
myTeam.append(myText.stringValue)
}
}
In Interface Builder select the table view (not the enclosing scroll view) and press ⌥⌘6 (Connections Inspector)
Disconnect dataSource
Press ⌥⌘7 (Bindings Inspector)
Bind Content to ViewController > Model Key Path myTeam
Since you are still using objectValueFor I assume that the value of the table cell view is already bound to objectValue.
macOS doesn't know a refresher.

How to make some specific items of a NSTableView in bold?

I would like to set some items of a non-editable, View Based NSTableView in bold. The items correspond to a specific index number of the array I use to populate the TableView.
I would like to set the change before the NSTableView is displayed to the users.
I tried to handle this change in this method but I can't find a way to do it:
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any?
If you don't want to use Cocoa Bindings
It's very similar on how you do it on iOS. Configure the cell's view in tableView(_:viewFor:row:)
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
var daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
var boldDays = ["Monday", "Wednesday"]
override func viewDidLoad() {
self.tableView.dataSource = self
self.tableView.delegate = self
}
func numberOfRows(in tableView: NSTableView) -> Int {
return daysOfWeek.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
// Assuming that you have set the cell view's Identifier in Interface Builder
let cell = tableView.make(withIdentifier: "myCell", owner: self) as! NSTableCellView
let day = daysOfWeek[row]
cell.textField?.stringValue = day
if boldDays.contains(day) {
let fontSize = NSFont.systemFontSize()
cell.textField?.font = NSFont.boldSystemFont(ofSize: fontSize)
// if you require more extensive styling, it may be better to use NSMutableAttributedString
}
return cell
}
}
If you want to use Cocoa Bindings
Cocoa Bindings can make this very simple, but if you set the slightest things wrong, it's pretty hard to figure out where things went south. Heed the warning from Apple:
Populating a table view using Cocoa bindings is considered an advanced topic. Although using bindings requires significantly less code—in some cases no code at all—the bindings are hard to see and debug if you aren’t familiar with the interface. It’s strongly suggested that you become comfortable with the techniques for managing table views programmatically before you decide to use Cocoa bindings.
Anyhow, here's how to do it. First, the code:
// The data model must inherit from NSObject for KVO compliance
class WeekDay : NSObject {
var name: String
var isBold: Bool
init(name: String, isBold: Bool = false) {
self.name = name
self.isBold = isBold
}
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
let daysOfWeek = [
WeekDay(name: "Sunday"),
WeekDay(name: "Monday", isBold: true),
WeekDay(name: "Tuesday"),
WeekDay(name: "Wednesday", isBold: true),
WeekDay(name: "Thursday"),
WeekDay(name: "Friday"),
WeekDay(name: "Saturday")
]
override func viewDidLoad() {
self.tableView.dataSource = self
self.tableView.delegate = self
}
}
Then the Interface Builder config:
Make sure the Table Column and Table Cell View have the same identifier. Best is to leave both of them blank for automatic
Select the Table View, bind Table Content to View Controller, Model Key Path = self.daysOfWeek
Select the Table View Cell, bind Value to Table Cell View (no kidding), Model Key Path = objectValue.name
Scroll down, bind Font Bold to Table Cell View, Model Key Path = objectValue.isBold
Either way, you should end up with something like this:
Polish as needed.

Using the contentView property on a custom tableView cell (being passed as a header) how to prevent it from nullifying the custom attributes?

For example here is my custom cell:
protocol SectionHeaderTableViewCellDelegate {
func didSelectUserHeaderTableViewCell(sectionHeader: SectionHeaderTableViewCell, selected: Bool, type: Type)
}
class SectionHeaderTableViewCell: UITableViewCell {
#IBOutlet weak var labelContainerView: LabelContainerView!
#IBOutlet weak var sectionTitleLabel: UILabel!
#IBOutlet weak var plusButton: UIButton!
var type: Type?
var delegate: SectionHeaderTableViewCellDelegate?
var dog: Dog?
let sections = [Type.Meals, Type.Exercise, Type.Health, Type.Training, Type.Misc]
}
extension SectionHeaderTableViewCell {
#IBAction func plusButtonPressed(sender: AnyObject) {
if let type = type {
delegate?.didSelectUserHeaderTableViewCell(self, selected: plusButton.selected, type: type )
}
}
In my controller if I add a return of header.contenView I get the desired results of the header staying in place but unfortunately it nullifies the button included in the custom header preventing it from being called. Otherwise if I simply just return header the button on the custom header cell works as expected but the header moves with the row being deleted which is obviously unsightly and not what I want.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = tableView.dequeueReusableCellWithIdentifier("sectionHeader") as? SectionHeaderTableViewCell else { return UITableViewCell() }
header.delegate = self
header.updateDogWithGender(dog)
header.type = header.sections[section]
header.sectionTitleLabel.text = header.sections[section].rawValue
return header.contentView
}
moving headers
In case anyone runs into a similar situation the solution was to create a Nib file and customize it as you see fit. Create a nib file by going to File -> New File -> iOS -> User Interface -> and selecting View. Create Nib file. I added my views and buttons to get the look I wanted. customize Nib. From there I changed the custom cell class to be UITableViewHeaderFooterView instead and reconnected my outlets and actions to the new Nib file.
class SectionHeaderView: UITableViewHeaderFooterView {... previous code from above }
Back in the controller update the viewForHeaderInSection function to load a nib instead :
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = NSBundle.mainBundle().loadNibNamed("SectionHeader", owner: self, options: nil).first as? SectionHeaderView
header?.delegate = self
header?.updateDogWithGender(dog)
header?.type = header?.sections[section]
header?.sectionTitleLabel.text = header?.sections[section].rawValue
return header
}
By the way we declared the property first at the end of the loadNibNamed property because it returns an array of AnyObjects and since my Nib file only contains one UIView that houses a label and a button I only needed the first and only item in the array. Thanks to my mentor James for figuring this out!