How to make a table View with message showing if tableview empty? - swift

I am building a tableView which displays a message when empty.
I'm using the really helpful answers on this question (Handling an empty UITableView. Print a friendly message) This has led me to a function:
func emptyMessage(message:String, viewController:UITableViewController) {
let VCSize = viewController.view.bounds.size
let messageLabel = UILabel(frame: CGRect(x:0, y:0, width:VCSize.width, height:VCSize.height))
messageLabel.text = message
messageLabel.textColor = UIColor.black
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center;
messageLabel.font = UIFont(name: "Futura", size: 15)
messageLabel.sizeToFit()
viewController.tableView.backgroundView = messageLabel;
viewController.tableView.separatorStyle = .none;
}
I could call this in every table views data source like this :
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if projects.count > 0 {
return 1
} else {
TableViewHelper.EmptyMessage("You don't have any projects yet.\nYou can create up to 10.", viewController: self)
return 0
}
}
which would work. However I would rather not have to implement that repeatedly and instead have one custom tableview with a method in the data source asking what message you would like to add.
I've tried extending the TableView class or making a subclass of tableView but I think this isn't the solution. Instead I think the solution is to overwrite the UITableViewDataSource protocol but my knowledge of protocols and delegation isn't sufficient.
I hope i'm on the right track with this. And to clarify I could do it in the way mentioned above but i'm trying to override the functionality to make a smart solution where i'm not repeating myself.

There is a very good library :
https://github.com/dzenbot/DZNEmptyDataSet
This can be used for all types of containers like UITableView, UICollectionView etc.
After conforming to some DZNEmptyDataSetSource, DZNEmptyDataSetDelegate , you can simply implement these functions:
func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
let str = "Empty Data."
let attrs = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)]
return NSAttributedString(string: str, attributes: attrs)
}
func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
let str = "Any Details about empty data."
let attrs = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)]
return NSAttributedString(string: str, attributes: attrs)
}
Apart from that, you can add a button to to perform some actions. Please refer the library for more features.

Related

NSTableView not appearing at all

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.

SourceView and SourceRect is depreciated?

I'm trying to fix all my warnings for my project, but I can't seem to figure out how to do this.
The warning I am given is:
'sourceView' was deprecated in iOS 13.0: renamed to 'UIContextMenuInteraction'
I have read the documentation here but i still cant figure out how to fix this warning?
Here is the code it is talking about:
extension CollectionsViewController: UIViewControllerPreviewingDelegate {
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
let tableView = previewingContext.sourceView as! UITableView
if let indexPath = tableView.indexPathForRow(at: location) {
let cell = tableView.cellForRow(at: indexPath) as! CollectionCell
let touch = cell.convert(location, from: tableView)
if let productResult = cell.productFor(touch) {
previewingContext.sourceRect = tableView.convert(productResult.sourceRect, from: cell)
return self.productDetailsViewControllerWith(productResult.model)
} else if let collectionResult = cell.collectionFor(touch) {
previewingContext.sourceRect = tableView.convert(collectionResult.sourceRect, from: cell)
return self.productsViewControllerWith(collectionResult.model)
}
}
return nil
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
self.navigationController!.show(viewControllerToCommit, sender: self)
}
}
Any help would be greatly appreciated!
UIViewControllerPreviewingDelegate protocol changed as documented:
Deprecated
UseUIContextMenuInteractionDelegate instead.
So you need to remove previewingContext delegate method rather than change its parameter sourceview etc. Instead of this method you need to pick from the new protocol UIContextMenuInteractionDelegate of its
delegate methods as Apple indicated.
In other words your extension is totally outdated and you need to rewrite according to new protocol.

After setting layout constant for label it stops working

I have a table view with automaticDimension for height and 2 cells.
One cell has a read more button, which updates the layout constraint constant for a label (as IBOutlet). It works fine and the cell height updates depending on the label height, but when I try to update the layout constraint first time on cellForRowAt method delegate it works, but after that it stops working from the read more button (and the layout constraint outlet doesn't become nil).
if cellType == .movie {
let movieCell = cell as! MovieDescriptionCell
movieCell.updateCellWith(movie: selectedMovie!)
movieCell.infoLabelHeight.constant = 200. ***(here is the problem, if I don't have this the read more button works)***
movieCell.readMorePressedBlock = { [weak self] (tag) in
guard let strongSelf = self else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(70), execute: {
strongSelf.tableView.reloadData()
})
}
}
#IBOutlet var infoLabelHeight: NSLayoutConstraint!
#IBAction func onReadMoreButton(button: UIButton) {
self.readMoreState = !self.readMoreState
setReadMoreImageFor(state: readMoreState)
let infoText = self.infoLabel.text
let expectedHeight = infoText?.height(withConstrainedWidth: infoLabelWidth.constant, font: self.infoTextFont) ?? self.infoDefaultHeight
self.infoLabelHeight.constant = self.readMoreState ? expectedHeight : self.infoDefaultHeight
readMorePressedBlock?(button.tag)
}
How can I make this work? If I delete the line "movieCell.infoLabelHeight.constant = 200" the read more button works, but I need to set the constant at the beginning too.
Thanks.
I found a solution, if I set the layout constraint in awakeFromNib method the read more button works (the possibility to change the constraint again)
override func awakeFromNib() {
super.awakeFromNib()
self.infoLabelHeight.constant = 200
}

How do I change/modify the displayed title of an NSPopUpButton

I would like an NSPopUpButton to display a different title than the title of the menu item that is selected.
Suppose I have an NSPopUpButton that lets the user pick a list of currencies, how can I have the collapsed/closed button show only the currencies abbreviation instead of the menu title of the selected currency (which is the full name of the currency)?
I imagine I can override draw in a subclass (of NSPopUpButtonCell) and draw the entire button myself, but I would prefer a more lightweight approach for now that reuses the system's appearance.
The menu items have the necessary information about the abbreviations, so that's not part of the question.
Subclass NSPopUpButtonCell, override drawTitle(_:withFrame:in:) and call super with the title you want.
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
var attributedTitle = title
if let popUpButton = self.controlView as? NSPopUpButton {
if let object = popUpButton.selectedItem?.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
attributedTitle = NSAttributedString(string:shortTitle, attributes:title.attributes(at:0, effectiveRange:nil))
}
}
}
return super.drawTitle(attributedTitle, withFrame:frame, in:controlView)
}
In the same way you can override intrinsicContentSize in a subclass of NSPopUpButton. Replace the menu, call super and put the original menu back.
override var intrinsicContentSize: NSSize {
if let popUpButtonCell = self.cell {
if let orgMenu = popUpButtonCell.menu {
let menu = NSMenu(title: "")
for item in orgMenu.items {
if let object = item.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
menu.addItem(withTitle: shortTitle, action: nil, keyEquivalent: "")
}
}
}
popUpButtonCell.menu = menu
let size = super.intrinsicContentSize
popUpButtonCell.menu = orgMenu
return size
}
}
return super.intrinsicContentSize
}
Ok, so I found out how I can modify the title. I create a cell subclass where I override setting the title based on the selected item. In my case I check the represented object as discussed in the question.
final class MyPopUpButtonCell: NSPopUpButtonCell {
override var title: String! {
get {
guard let selectedCurrency = selectedItem?.representedObject as? ISO4217.Currency else {
return selectedItem?.title ?? ""
}
return selectedCurrency.rawValue
}
set {}
}
}
Then in my button subclass I set the cell (I use xibs)
override func awakeFromNib() {
guard let oldCell = cell as? NSPopUpButtonCell else { return }
let newCell = MyPopUpButtonCell()
newCell.menu = oldCell.menu
newCell.bezelStyle = oldCell.bezelStyle
newCell.controlSize = oldCell.controlSize
newCell.autoenablesItems = oldCell.autoenablesItems
newCell.font = oldCell.font
cell = newCell
}
A downside though is that I have to copy over all attributes of the cell I configured in Interface Builder. I can of course just set the cell class in Interface Builder, which makes this superfluous.
One thing I haven't figured out yet is how I can have the button have the correct intrinsic content size now. It still tries to be as wide as the longest regular title.
And the second thing I haven't figured out is how to make this work with bindings. If the buttons content is provided via Cocoa Bindings then I can only bind the contentValues, and the cell's title property is never called.

PFQueryTableViewController in swift not loading data

I am creating an app that loads data into a UITableView from Parse. I used some code that seems to work for loading the retrieved data into the cells in the table, but it is buggy and I did some research and found the PFQueryTableViewController. This seems like the solution to my problem, but all the documentation is in Objective-C (which I do not have any experience coding in), and there are no tutorials from what I could find on how to use this class in Swift. There are a few snippets that talk about how to initialize it, but nothing that really goes through how to set it up in Swift. I tried to go through the Obj-C documentation and set it up myself in Swift but I can't seem to get it right and would really appreciate some help. Does anyone have a suggested resource or has anyone set one up and could step through how to do this in Swift?
EDIT: I have found some resources and tried to get it set up, the table view loads but nothing is loaded into the cells and I am not sure. I am going to leave the code below. The commented out section wasn't relevant to my code but left it for reference. Any further help as to why the table isn't loading would be helpful!
class MyCookbookVC : PFQueryTableViewController {
override init!(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
textKey = "Name"
pullToRefreshEnabled = true
paginationEnabled = true
objectsPerPage = 25
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
tableView.reloadData()
}
override func queryForTable() -> PFQuery! {
let query = PFUser.query()
query.whereKey("CreatedBy", equalTo: PFUser.currentUser())
query.orderByAscending("Name")
//if network cannot find any data, go to cached (local disk data)
if (self.objects.count == 0){
query.cachePolicy = kPFCachePolicyCacheThenNetwork
}
return query
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!, object: PFObject!) -> PFTableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as PFTableViewCell
cell.textLabel?.text = object["Name"] as? String
/*if let profileImage = object["profileImage"] as? PFFile {
cell.imageView.file = profileImage
}
else {
cell.imageView.image = kProfileDefaultProfileImage
}
cell.imageView.layer.cornerRadius = cell.imageView.frame.size.width / 2;
cell.imageView.clipsToBounds = true
cell.imageView.layer.borderWidth = 3.0
cell.imageView.layer.borderColor = UIColor.whiteColor().CGColor */
/* cell.textLabel?.font = UIFont(name: kStandardFontName, size: kStandardFontSize)
cell.textLabel?.textColor = UIColor.whiteColor()
cell.backgroundColor = kbackgroundColor */
return cell
}
}
Your query is querying PFUser when it should it be querying your "Recipe" class. Change this line
let query = PFUser.query()
to
let query = PFQuery.queryWithClassName("Recipe")
I posted my working solution in the Parse Google Group. Maybe it will help you.
https://groups.google.com/forum/#!topic/parse-developers/XxLjxX29nuw
You're using PFUser.query() which is useful for querying for users, but you want to use PFQuery(className: YOUR_PARSE_CLASS)