Displaying & working with Core Data in NSTableView - swift

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.

Related

Swift MVVM embedded network fetching with image URL

I'm building a simple app with swift by using MVVM binding.
What my app does is simply fetching data from a url and get a json response, and show the info on a table view.
Each cell contains a title, a subtitle, and an image.
However, the image is showing as a string in the json response. So I will need an extra network fetch to get the image for each cell after the 1st network call.
"articles": [
{
"title": "Wall Street tumbles with Nasdaq leading declines Reuters",
"description": "Wall Street's main indexes tumbled on Monday with Nasdaq leading the declines as technology stocks dropped on expectations of a sooner-than-expected rate hike that pushed U.S. Treasury yields to fresh two-year highs.",
"url": "https://www.reuters.com/markets/europe/wall-street-tumbles-with-nasdaq-leading-declines-2022-01-10/",
"urlToImage": "https://www.reuters.com/resizer/2cEiuwViTo_kOe7eWg4Igm8pm_Q=/1200x628/smart/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/POAM3MQFAJJX3MRXQW772WYKCA.jpg",
}]
My question is, how should I modify my code to make the tableCellView more "pure"? Currently it's calling network fetch to get the image. Where should I move that image fetching part from the tableviewcell config function to?
Should I change my Model to to contains the UIImage but NOT the string?
My Model:
struct Articles: Codable {
let articles: [Article]
}
struct Article: Codable {
let title: String
let description: String?
let urlToImage: String?
}
My ViewModel:
struct ViewMode {
var articles: Observable<[Article]> = Observable([])
}
Main functions in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
viewModel.articles.bind { [weak self] _ in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
fetchArticlesFromLocal(fileNmae: "response")
}
func fetchArticlesFromLocal(fileNmae: String) {
networkManager.fetchLocalJson(name: fileNmae) { [weak self] result in
switch result {
case.success(let data):
guard let data = data else {return}
do {
let articles = try JSONDecoder().decode(Articles.self, from: data)
self.viewModel.articles.value = articles.articles.compactMap({
Article(title: $0.title, description: $0.description, urlToImage: $0.urlToImage)
})
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.articles.value?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.cellID, for: indexPath) as? ImageTableViewCell {
if let articles = viewModel.articles.value {
cell.config(with: articles[indexPath.row])
}
}
return UITableViewCell()
}
}
My tableCellView:
func config(with article: Article) {
titleView.text = article.title
descriptionView.text = article.description
networkManager.fetchImage(url: article.urlToImage) { [weak self] result in
switch result {
case .success(let image):
DispatchQueue.main.async {
self?.iconView.image = image
}
case .failure(let error):
return
}
}
}
My binding:
class Observable<T> {
var value: T? {
didSet {
listener?(value)
}
}
typealias Listener = ((T?) -> Void)
var listener: Listener?
init(_ value: T?) {
self.value = value
}
func bind(_ listener: #escaping Listener) {
self.listener = listener
listener(value)
}
}
First of all, you say you use MVVM but I see some issues in implementation: you are passing a Model (Article instance) to the cell, but it should be a ViewModel (ArticleViewModel instance, but you didn't create that struct) since views should not have a direct reference to models in this architecture.
You also seem to add async/networking code inside the custom cell, which also violates the architecture and separation of concerns (even in MVC, that code should not be there, since Network Requests are part of the Service Layer and not the UI layer). The cell/view should know how to configure itself: 1. when image is available and 2. when image is not available... (you call reload on the cell from VC whenever the image becomes available, for instance)
Regarding your question, a better place to add the image downloading code would be cellForRowAt method. ViewModels should not be tied to UIKit generally speaking, so having a URL or String property for the image is fine.
To make the UI display smoothly on scrolling, you probably want to cancel the request: 1. when the image is prepared for reuse aka thrown in the reuse pool or 2. when it disappears from the visible area of the screen.
Here is a good article that uses approach #1: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/
Note that it caches the images in memory, but you may also want to cache them on disk.
View Controller is generally also considered to be more of a View, so technically we are still mixing Networking with UI. Perhaps it's better to use repository pattern either directly as a dependency of the View Controller when using MVC or a dependency of VC's View Model. View Model should not be aware if the Repository uses web requests to get the images or has them cached in memory or on disk, so use a protocol and Dependency Injection for that (also View Model may have to calculate the size/height of the cell based on the image size, before the cell is actually configured with that image).
Using this approach would work, but you will again violating some principles, and it's all your choice. Some people say it's ok to have UIKit/UIImage references inside view models (https://lickability.com/blog/our-view-on-view-models/, not sure if their argument should be considered valid though...).
Otherwise, you'll have to use some kind of auxiliary class/object to pass the image urls and do the networking part and keep track of what was already downloaded, what requests should be cancelled etc.
Remember, you don't have to follow MVC or MVVM strictly. Some object has to download those images anyway, and that object ideally should not be none of the view, view model or the data model. Name it as you want, ImageLoadingCoordinator, ImageRepository etc. and make sure it downloads asynchronously and has callbacks and gives the possibility to cancel requests and cache images. Generally speaking, try not to give any object to much responsibility or mix networking code with view code.

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.

Using swift to populate NSTableView rows with a NSPopupButtonCell

I have been trying to change one of the cells in an NSTableView to a pull-down menu, but have been unsuccessful. I read the Apple developer documentation, but it doesn't give an example of how to use NSPopupButtonCell in a NSTableView. I searched forums, including here, and only found one somewhat relevant example, except that it was in objective-c, so it doesn't work for my swift app. Code for the table is here:
extension DeviceListViewController:NSTableViewDataSource, NSTableViewDelegate{
// get the number of rows for the table
func numberOfRows(in tableView: NSTableView) -> Int {
return homedevices.count
}
// use the data in the homedevices array to populate the table cells
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let result = tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
if tableColumn?.identifier == "ID" {
result.textField?.stringValue = homedevices[row].id
} else if tableColumn?.identifier == "Name" {
result.textField?.stringValue = homedevices[row].name
result.imageView?.image = homedevices[row].image
} else if tableColumn?.identifier == "Type" {
result.textField?.stringValue = homedevices[row].type
} else if tableColumn?.identifier == "Button" {
result.textField?.integerValue = homedevices[row].button
}
return result
}
// facilitates data sorting for the table columns
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let dataArrayMutable = NSMutableArray(array: homedevices)
dataArrayMutable.sort(using: tableView.sortDescriptors)
homedevices = dataArrayMutable as! [HomeDevice]
tableView.reloadData()
}
}
I really just want to be able to allow pull-down selection to change the button assigned to a particular homedevice (a simple integer), instead of having to type a number into the textfield to edit this value. Unfortuantely, when I add the popupbuttoncell to my table in IB, all of the views for my table cells are removed. So I may need to create the table differently. But most of the things I have read about and tried have caused runtime errors or display an empty table.
EDIT:
Day 3:
Today I have been reading here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html
and many other places too, but I don't have rep to post any more links.
I have added a NSPopupButton in IB, but am not sure how to set the value. I tried result.objectValue = homedevices[row].button, but that does not work. I suppose that I need an array controller object. So then I tried creating an outlet for the object in my DeviceListViewController like #IBOutlet var buttonArrayController: NSArrayController! I guess that I now need to somehow find a way to connect the array controller to my homedevices array.
so I looked at example code here:
https://github.com/blishen/TableViewPopup
This is in objective-C, which is not a language I am using, but maybe if I keep looking at it at various times over the course of the week, I might figure out how to make a pull-down menu.
So I am continuing to work at this, with no solution currently.
This issue is solved, thanks to #vadian.
The button is inserted as NSPopUpButton object, rather than a NSPopUpButtonCell.
Then the cell gets its own custom class, which I called ButtonCellView as a subclass of NSTableCellView.
Then the created subclass can receive an outlet from the NSPopUpButton to the custom subclass. I can give this a selectedItem variable and create the menu here.
Then in the table view delegate, when making the table, I can just set the selectedItem of my ButtonCellView object to the value from my data array.
It works great!

Watch OS Swift Table load data in order

Watch OS 3/Xcode 8 allows for stand alone watch apps. Can someone provide the best Swift array/method for loading data to a table in storbyboard in order from Interface Controller, updating after each display.
My code right now only selects the first value and displays in table, does not move onto the next value in the table list after use.
let financeupdate = ["London", "Milan", "Bangkok", "NYC"}
loadTableData()
}
private func loadTableData(){
financetable.setNumberofRows(financeupdate.count, withRowType:"GlobalFinanceRowController")
for (index, financeupdateName) in enumerate(finaceupdate_ {
let row=finacetable.rowControllerAtIndes(index) as GlobalFinanceRowController
row.interfaceLabel.setText(financeName)
}
}
You can view this url to see a tutorial doing same. It has following method where multiple rows are being displayed:
var flights = Flight.allFlights()
override func awake(withContext context: Any?) {
super.awake(withContext: context)
flightsTable.setNumberOfRows(flights.count, withRowType: "FlightRow")
for index in 0..<flightsTable.numberOfRows {
guard let controller = flightsTable.rowController(at: index) as? FlightRowController else { continue }
controller.flight = flights[index]
}
}

How to filter table view cells with a UISegmentedControl in Swift?

I've already searched this before asking the question but I didn't find what I need.
I'm building this app where the user puts a task (not going to the app store, just for me and some friends), and the task has a category. For example: school, home, friends, etc. When the user is going to add a new task, there are 2 text fields, the description text field and the category text field. I'm using a UIPickerView so the user picks a category, then, after creating the new task, it will add the category to an array I've created called "categories".
I want to put an UISegmentedControl on top of the table view with the sections:
All - School - Home - Friends
If all is selected, it will show all the cells with no filtering. If not, it will show the cell(s) with the corresponding categories.
I've read that I need to create table view sections to each category, but this would change my code a lot, and I don't even have an idea of how to work with multiple table view sections, I've tried once but it kept repeating the cells of one section in the second.
So how can I filter the cells per category?
Can I just put for example this? :
if //code to check in which section the picker is here {
if let schoolCell = cell.categories[indexPath.row] == "School" {
schoolCell.hidden = true
}
}
Please help me!!!
EDIT:
I have this code by now:
if filterSegmentedControl.selectedSegmentIndex == 1 {
if categories[indexPath.row] == "School" {
}
}
I just don't know where to go from here. How do I recognize and hide the cells?
It seems to me that you may want to take a simpler approach first and get something working. Set up your ViewController and add a tableView and two(2) arrays for your table data. One would be for home and the other for work. Yes, I know this is simple but if you get it working, then you can build on it.
Add a variable to track which data you are displaying.
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var tableView: UITableView!
// You would set this to 0, 1 or 2 for home, work and all.
var dataFilter = 0
// Data for work tasks
var tableDataWork : [String] = ["Proposal", "Send mail", "Fix printer", "Send payroll", "Pay rent"]
// Data for home tasks
var tableDataHome : [String] = ["Car payment", "Mow lawn", "Carpet clean"]
Add these functions for the segmented control.
#IBAction func segmentedControlAction(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex {
case 0:
print("Home")
dataFilter = 0
case 1:
print("Work")
dataFilter = 1
case 2:
print("All")
dataFilter = 2
default:
print("All")
dataFilter = 2
}
reload()
}
func reload() {
dispatch_async( dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("task-cell")
var title: String?
switch dataFilter {
case 0:
title = tableDataHome[indexPath.row]
case 1:
title = tableDataWork[indexPath.row]
case 2:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row - tableDataWork.count]
}
default:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row + tableDataWork.count]
}
}
cell?.textLabel?.text = title
if cell != nil {
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If
switch dataFilter {
case 0: return tableDataHome.count
case 1: return tableDataWork.count
default: return tableDataHome.count + tableDataWork.count
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
You can find the entire project here: https://github.com/ryantxr/segmented-control-app
It depends on your tableview.
If you use NSFetchedResultsController then you need to modify your fetch request. If you use an array directly, just use the filter function in Swift, passing in the condition, e.g. filteredArray = array.filter{$0.isAudioFile} Then, after setting your datasource array to the filtered one, call reloadData on your tableview.
You will need to keep a reference to the full array, and use the filtered one as your datasource in cellForRow...