I have a tableView with an image and a textField but they are not showing the data. What could be missing?
I have tableColumn identifier set as MainCell...
I converted obj-c from here to swift.
Any suggestions?
import Cocoa
class viewTime: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
var tableContents:NSMutableArray = []
#IBOutlet var tableView: NSTableView!
override func viewDidLoad() {
super.viewDidLoad()
let path: NSString = "/Library/Application Support/Apple/iChat Icons/Flags"
let fileManager:FileManager = FileManager.default
let directoryEnum:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: path as String)!
while let file = directoryEnum.nextObject() as? NSString{
let filePath:NSString = path.appendingFormat("/%#", file)
let obj:NSDictionary = ["image": NSImage(byReferencingFile:filePath as String), "name": file.deletingPathExtension]
print("obj:\(obj)")
tableContents.add(obj)
}
self.tableView.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return tableContents.count
}
private func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView! {
let cellView = tableView.make(withIdentifier: "MainCell", owner: self) as! NSTableCellView
let flag:NSDictionary = tableContents[row] as! NSDictionary
let identifier:NSString = tableColumn!.identifier as NSString
if (identifier == "MainCell") {
cellView.textField?.stringValue = flag["name"] as! String
cellView.imageView!.image = flag["image"] as? NSImage
return cellView
}
return nil
}
}
Translating Objective-C to Swift literally is always a bad idea.
First of all make sure that both datasource and delegate of the table view are connected to the view controller in Interface Builder.
Make sure also that the Identifier of the NSTableCellView is MainCell
Most important change: Create a custom struct, it makes things so much easier.
struct Asset {
var name : String
var image : NSImage
}
Declare the data source array as Swift Array of the struct type.
var tableContents = [Asset]()
This is viewDidLoad in real Swift code. Don't annotate types unless the compiler tells you to do.
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(fileURLWithPath:"/Library/Application Support/Apple/iChat Icons/Flags")
let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [], options: .skipsHiddenFiles, errorHandler: nil)!
for case let fileURL as URL in enumerator {
let asset = Asset(name: fileURL.deletingPathExtension().lastPathComponent, image: NSImage(contentsOf:fileURL)!)
tableContents.append(asset)
}
self.tableView.reloadData()
}
numberOfRows is OK.
tableView:viewForColumn:Row can be simplified as there is only one identifier and one column.
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView = tableView.make(withIdentifier: "MainCell", owner: self) as! NSTableCellView
let asset = tableContents[row]
cellView.textField!.stringValue = asset.name
cellView.imageView!.image = asset.image
return cellView
}
Related
I'm a Rails developer but I need to modify the controller of an application to save some sensors data to an external file.
I have this controller that take some data and populate a TableView, and data are updated every time I push a button in my view.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var currentNames: [Any] = [], voltageNames: [Any] = [], thermalNames: [Any] = []
var currentValues: [Any] = [], voltageValues: [Any] = [], thermalValues: [Any] = []
override func viewDidLoad() {
super.viewDidLoad()
currentNames = currentArray()
voltageNames = voltageArray()
thermalNames = thermalArray()
currentValues = returnCurrentValues()
voltageValues = returnVoltageValues()
thermalValues = returnThermalValues()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch (section) {
case 0:
return currentNames.count
case 1:
return voltageNames.count
case 2:
return thermalNames.count
default:
return 0;
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "cell")!
let string: NSMutableString = ""
switch (indexPath.section) {
case 0:
// cell.textLabel.text =
let name = currentNames[indexPath.row] as! NSString
let number = currentValues[indexPath.row] as! NSNumber
string.appendFormat("%#: %.2lf", name, number.doubleValue)
case 1:
let name = voltageNames[indexPath.row] as! NSString
let number = voltageValues[indexPath.row] as! NSNumber
string.appendFormat("%#: %.2lf", name, number.doubleValue)
case 2:
let name = thermalNames[indexPath.row] as! NSString
let number = thermalValues[indexPath.row] as! NSNumber
string.appendFormat("%#: %.2lf", name, number.doubleValue)
default:
break;
}
cell.textLabel?.text = string as String?
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 3;
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch (section) {
case 0:
return "Current (A), \(currentNames.count) items"
case 1:
return "Voltage (V), \(voltageNames.count) items"
case 2:
return "Temperature (°C), \(thermalNames.count) items"
default:
return "";
}
}
#IBAction func reloadData(_ sender : AnyObject) {
currentValues = returnCurrentValues()
voltageValues = returnVoltageValues()
thermalValues = returnThermalValues()
tableView.reloadData()
}
}
I need to refresh data every second without pushing the button and I want to save this data in a log file.
I create a log.swift file
import Foundation
struct Log: TextOutputStream {
func write(_ string: String) {
let fm = FileManager.default
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
if let handle = try? FileHandle(forWritingTo: log) {
handle.seekToEndOfFile()
handle.write(string.data(using: .utf8)!)
handle.closeFile()
} else {
try? string.data(using: .utf8)?.write(to: log)
}
}
}
var logger = Log()
I know that I can do something every second with
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
}
But I don't understand where to put this.
I just need to save a log with thermalNames and thermalValues
Basically the cell I have in my tableView
let name = thermalNames[indexPath.row] as! NSString
let number = thermalValues[indexPath.row] as! NSNumber
string.appendFormat("%#: %.2lf", name, number.doubleValue)
Then I will parse the file with ruby to convert to a csv...
In order to do that you have to create two functions. One for the action you want to perform and the other one for timer itself
here are the functions:
#objc func save() {
//Your Saving action has to be here
}
You have to call this one in ViewDidLoad
func startTimerForShowScrollIndicator() {
self.timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.save), userInfo: nil, repeats: true)
}
I got a plist object which contains all the words key=english and value=malay and I assigned in to 2 different arrays which is english and malay. Now I want a textfield where I want to search the english word and print the malay word in the label.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
#IBOutlet weak var selectedLabel: UILabel!
#IBOutlet weak var searchText: UITextField!
#IBOutlet weak var wordTable: UITableView!
var english = [String]()
var malay = [String]()
var words: [String: String] = [:]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
wordTable.dataSource = self
wordTable.delegate = self
searchText.delegate = self
if let path = Bundle.main.path(forResource: "words", ofType: "plist"){
if let plistData = FileManager.default.contents(atPath: path){
do {
let plistObject = try PropertyListSerialization.propertyList(from: plistData, options: PropertyListSerialization.ReadOptions(), format: nil)
words = (plistObject as? [String: String])!
english = [String] (words.keys)
malay = [String] (words.values)
} catch {
print("Error Serialize")
}
} else {
print("Error reading data")
}
} else {
print("Property list")
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return english.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: UITableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: "tabelCell")
if cell == nil {
cell = UITableViewCell(
style: UITableViewCellStyle.value2,
reuseIdentifier: "tableCell")
print("creating a table cell")
}
cell!.textLabel!.text = english[indexPath.row]
cell!.detailTextLabel?.text = malay[indexPath.row]
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedLabel.text = malay[indexPath.row]
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// Hide the keyboard
textField.resignFirstResponder()
return true
}
#IBAction func searchBtn(_ sender: UIButton) {
let result = words.filter {$0.key == searchText.text}
if result.count > 0 {
print(result)
selectedLabel.text! = result.values //error
} else {
print("Not found")
}
}
}
the output I expecting is textfield(Bus) which is english word then in the label show me the malay word(Bas)
You have a plist file as a Dictionary. So you can get the dictionary object from the plist file and already answer here.
Make a structure for better data binding.
struct Word {
var english: String
var malay: String
}
Then declare an array of words globally in your ViewController.
var words: [Word] = [] // An empty array
In viewDidLoad: fetch data from plist file.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
wordTable.dataSource = self
wordTable.delegate = self
searchText.delegate = self
if let path = Bundle.main.path(forResource: "words", ofType: "plist") {
if let plistData = FileManager.default.contents(atPath: path){
do {
guard let plistObject = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: String] else {
// Plist is not [String: String]
return
}
// Here you need to change the code. Converting the dictionary into struct array
var words: [Word] = plistObject.map {Word(english: $0.key, malay: $0.value)}
/// Then sort by english word if needed
words.sorted {$0.english < $1.english}
} catch {
print("Error Serialize")
}
} else {
print("Error reading data")
}
} else {
print("Property list")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return words.count
}
Update your cell data as well.
cell!.textLabel!.text = words[indexPath.row].english
cell!.detailTextLabel?.text = words[indexPath.row].malay
And your button action with minor modification:
#IBAction func searchBtn(_ sender: UIButton) {
let result = words.filter {$0.english == searchedText}
if let word = result.first {
selectedLabel.text = word.malay
} else {
selectedLabel.text = "" // No data found
}
}
You can replace $0.english == searchedText with {$0.english.contains(searchedText)} if you want to filter with contains, But in that case you might get the multiple result. I assume that in your case you need it as a translator so use ==.
Why don't you search in your plist object? I think it is simpler
#IBAction func searchBtn(_ sender: UIButton) {
guard let words = plistObject as? [String: String], let key = searchText.text else { return }
selectedLabel.text = words[key] ?? ""
}
Something like this.
I have a 2 problems with displaying data in a table from Firebase.
Nothing displayed in TableView from Firebase
I I can not add a link(child) to a variable
Print is working. I get access to Firebase, but nothing is added to TableView. Please, look at my code and correct where i'm wrong.
It's my model
class Exercises {
var titleExercise = ""
var descriptionExercise = ""
init (titleExercise: String, descriptionExercise: String) {
self.titleExercise = titleExercise
self.descriptionExercise = descriptionExercise
}
}
It's my ViewController
class ExercisesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//MARK: Properties
var refWorkout: String = ""
var workout: TrainingProgram?
var ref: DatabaseReference!
#IBOutlet weak var tableView: UITableView!
var exercises = [Exercises]()
//MARK: Methods
override func viewDidLoad() {
super.viewDidLoad()
fetchExercises()
tableView.dataSource = self
tableView.delegate = self
refWorkout = workout!.title
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return exercises.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ExercisesTableViewCell
let workouts = exercises[indexPath.item]
cell.titleLabel.text = workouts.titleExercise
cell.descriptionLabel.text = workouts.descriptionExercise
return cell
}
func fetchExercises() {
Database.database().reference().child("programs").child("OPEN SPACE").child("exercises").observe(.childAdded) { (snapshot) in
print(snapshot.value)
if let dict = snapshot.value as? [String: AnyObject] {
let newTitle = dict["title"] as! String
let newDescription = dict["description"] as! String
let exerciseTableCell = Exercises(titleExercise: newTitle, descriptionExercise: newDescription)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
And I have second question. It also addresses this issue.
As you can see, I have refWorkout = workout!.title Here comes the title from previous ViewController , and refWorkout is a child for Firebase. If I will write next code
ref = Database.database().reference().child("programs").child(refWorkout).child("exercises")
ref.observe(.childAdded) { (snapshot) in
print(snapshot.value)
}
Everything will work. Print will work. But if I insert this code to func fetchExercises() - > It will look like
func fetchExercises() {
Database.database().reference().child("programs").child(refWorkout).child("exercises").observe(.childAdded)...
My app crashed.
Please help me with two questions. Thank you!
My Firebase structure
This is a common mistake, you are reloading the table view too soon and you don't assign/append the result to the data source array
The observe API works asynchronously, put the line to reload the table view into the closure
func fetchExercises() {
Database.database().reference().child("programs").child("OPEN SPACE").child("exercises").observe(.childAdded) { (snapshot) in
print(snapshot.value)
if let dict = snapshot.value as? [String: Any] { // most likely all values are value type
let newTitle = dict["title"] as! String
let newDescription = dict["description"] as! String
let exercise = Exercises(titleExercise: newTitle, descriptionExercise: newDescription)
self.exercises.append(exercise)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Side note:
You class contains 3 bad practices:
Semantically objects used in collection types should be named in singular form.
Don't declare properties with default values if there is an initializer.
There is too much redundant information in the variable names
And in most cases a struct and even constants are sufficient. I'd recommend
struct Exercise {
let title : String
let description : String
}
In a struct you get the initializer for free.
For some reason when I put my code for my NSTableView in a ViewController, none of the cells appear, but if I put the code in the AppDelegate, everything works great.
Any ideas as to why this is happening? I'm working with a .xib file if that helps at all.
class ViewController: NSViewController{
var delegate: AppDelegate? = nil
#IBOutlet weak var tableView: NSTableView!
override func viewDidLoad() {
super.viewDidLoad()
delegate = NSApplication.shared.delegate as? AppDelegate
tableView.dataSource = self
tableView.delegate = self
}
}
extension ViewController: NSTableViewDataSource{
func numberOfRows(in tableView: NSTableView) -> Int {
print(delegate?.FlightList.count ?? 0)
return delegate?.FlightList.count ?? 0
}
}
extension ViewController: NSTableViewDelegate{
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
var text: String = ""
var cellIdentifier: String = ""
guard let item = delegate?.FlightList[row] else {
return nil
}
if tableColumn == tableView.tableColumns[0] {
text = item.flightName
cellIdentifier = "flightID"
}
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellIdentifier), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = text
return cell
}
return nil
}
}
you need to implement the NSTableViewDataSource, NSTableViewDelegate protocols to tell the table this is the class its getting data from else the
tableView.dataSource = self
tableView.delegate = self
wont work and then you wont need to use the app delegate
Firstly I must note that this is my first GUI-app (XIB) in Swift, in other words I am working on and trying to learn Swift and MacOS software development. I have looked through several questions, here at Stack, as well as the Apple documentation on NSTableView, but I'm stuck.
Trying to make a simple app to read some attributes of selected files. I have a custom NSView where the user drags and drop in a file and it reads some attributes off it - which is ok.
>>> print("\(fileDataToShow)\n\(resultTable)")
Optional([["filename": "foo.jpeg", "state": "1"],["filename": "bar.jpeg", "state": "1"]])
Optional(<NSTableView: 0x101203070>)
The #IBOutlet weak var resultTable: NSTableView!, at top of the file containing the class/NSView, show that it is connected, MainMenu.XIB—ResultTable.
I have come up with following code, in an attempt to display the data in the NSTableView, from my custom class View: NSView {
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
<...>
func numberOfRowsInTableView(in tableView: NSTableView) -> Int {
return fileDataToShow?.count ?? 0
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
var result:NSTableCellView
result = tableView.makeView(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
result.textField?.stringValue = fileDataToShow?[row][(tableColumn?.identifier.rawValue)!]! as! String
return result
}
resultTable?.beginUpdates()
// print(type(of:fileDataToShow)) // Optional<Array<Dictionary<String, Any>>>
resultTable.insertRows(at: IndexSet(integer: fileDataToShow?.count ?? 0), withAnimation: .effectFade)
resultTable.reloadData()
resultTable?.endUpdates()
}
Content of fileDataToShow is ok, but the other lines of code, .beginUpdates() / .insertRows(.., etc. doesn't seem to have any action.
As mentioned, I can't figure this out and don't know where or how to figure this... Anyone got some tips and/or pointers for me ?
I have defined all of the keys in fileDataToShow to correspond with the Identifiers in my XIB.
Hope I have managed to explain my problem in an ok way.
EDIT:
The Debug area giving following output when I run my app:
*** Illegal NSTableView data source (<NSObject: 0x600000000b90>). Must implement numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
EDIT2/Update:
Thank you #vadian, but I still haven't managed to fix this, here's a little update.
Here's my whole file, DropZone.swift:
```
class DropView: NSView/*, NSTableViewDataSource, NSTableViewDelegate*/ {
#IBOutlet weak var resultTable: NSTableView!
let dropZoneEnteredBackgroundColor = CGColor(red: 165/255, green: 165/255, blue: 165/255, alpha: 1.0)
let dropZoneExitedBackgroundColor = CGColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1.0)//NSColor.gray.cgColor
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
self.layer?.backgroundColor = dropZoneExitedBackgroundColor
registerForDraggedTypes([NSPasteboard.PasteboardType.URL,
NSPasteboard.PasteboardType.fileURL])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
self.layer?.backgroundColor = dropZoneEnteredBackgroundColor
return .copy
}
override func draggingEnded(_ sender: NSDraggingInfo) {
self.layer?.backgroundColor = dropZoneExitedBackgroundColor
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList(forType:
NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as?
NSArray else {return false}
var droppedItems: [String: String] = [:]
for path in pasteboard {
guard let fullPath = path as? String else { return false }
let fileManager = FileManager.default
var isDir: ObjCBool = false
if fileManager.fileExists(atPath: fullPath, isDirectory:&isDir) {
if isDir.boolValue {
// the dropped item exists and it's a directory
droppedItems[path as! String] = "folder"
}
else {
// file exists and it's not a directory, hence a normal file
droppedItems[path as! String] = "file"
}
}
}
do {
var fileDataToShow = [[String:Any]]()
for object in droppedItems {
if object.value == "file" {
do {
//let fullPath = object.key
let attributes = try object.key.extendedAttributes() // Array<String>
let filename = object.key.fileName() + "." + object.key.fileExtension()
fileDataToShow.append(["state": "1",
"filename": filename,
"metadata":removed_attributes
])
}
catch {
debugPrint("Error info: \(error)")
}
}
else if object.value == "folder" {
// TODO
}
}
func numberOfRows(in tableView: NSTableView) -> Int {
return fileDataToShow.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let cell = tableView.makeView(withIdentifier: tableColumn?.identifier ?? NSUserInterfaceItemIdentifier(rawValue: ""), owner: self) as! NSTableCellView
// This line could crash if there values which are not String
cell.textField?.stringValue = fileDataToShow[row][tableColumn?.identifier.rawValue ?? ""] as! String
return cell
}
let insertionIndex = fileDataToShow.count
//debugPrint(resultTable) // Optional(<NSTableView: 0x10100ba10>)
//debugPrint(fileDataToShow) // [["filename": "img1.jpeg", "metadata": ["com.apple.metadata..", "com.a..."], "state": "1"]]
resultTable.insertRows(at: IndexSet(integer: insertionIndex), withAnimation: .effectGap)
} // do
return true
}
}
This is now giving the following error:
*** Canceling drag because exception 'NSTableViewException' (reason 'NSTableView error inserting/removing/moving row 2 (numberOfRows: 0).') was raised during a dragging session
Sorry, but have had trouble with this since the last reply from #vadian, so have to ask again.
What am I doing wrong?
EDIT 3:
Appreciate your answers, #vadian, but I do not get this. I have places the numberOfRows and tableView function right underneath the init function. And implemented following code last in the do-block, in an attempt to update the table:
resultTable.beginUpdates()
var i = 0
for row in fileDataToShow {
print("state:",row["state"]!) // 1
print("filename:",row["filename"]!) // file.jpg
print("metadata:",row["metadata"]!) // ["com.apple.metadata..", "com.a..."]
resultTable.insertRows(at: IndexSet(integer: i), withAnimation: .effectFade)
i += 1
}
resultTable.endUpdates()
New lines is added to the table, but they are all empty. How do I - in any way - bind fileDataToShow against resultTable.insertRows.
If you understand my poor spelling and fussy questions :)
Swift is hard but fun to learn!
There are many issues in the code.
numberOfRowsInTableView is numberOfRows(in tableView: in Swift 3+.
The datasource / delegate methods must be on the top level in the class, not in performDragOperation.
You are using too many question marks.
Don not declare the data source array as optional, declare it as empty non-optional array.
var fileDataToShow = [[String:Any]]()
func numberOfRows(in tableView: NSTableView) -> Int {
return fileDataToShow.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let cell = tableView.makeView(withIdentifier: tableColumn.identifier!, owner: self) as! NSTableCellView
// This line could crash if there values which are not String
cell.textField?.stringValue = fileDataToShow[row][tableColumn.identifier!.rawValue)] as! String
cell result
}
To insert a row with animation don't call reloadData(). Get the last index of the array, append the item to the array and insert the row.
Begin-/endUpdates is useless
let insertionIndex = fileDataToShow.count
fileDataToShow.append([:]) // append some suitable dictionary
resultTable.insertRows(at: IndexSet(integer: insertionIndex), withAnimation: .effectGap)