I'm a newbie learning iOS and Swift so apologies ahead of time. Currently I'm trying to setup a tableView within a viewController and display data in the cells in a portion of the screen. My current problem seems to be in reloading the tableView data after the Alamofire HTTP request in viewDidLoad() is called for numberOfRowsInSection(). Here's the code:
import UIKit
import Alamofire
import SwiftyJSON
class CourseDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var titleLabel: UILabel?
#IBOutlet weak var creditsLabel: UILabel?
#IBOutlet weak var descriptionLabel: UILabel?
#IBOutlet weak var tableView: UITableView!
var detailCourse: Course? {
didSet {
configureView()
}
}
var course: Course!
func configureView() {
self.title = detailCourse?.abbr
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "SectionCell")
tableView.delegate = self
tableView.dataSource = self
if let theCourse: Course = self.detailCourse as Course! {
var abbr: String = theCourse.abbr!
APIService.getCourseByAbbr(abbr) { (data) -> Void in
self.course = Course(courseJSON: data)
// Set labels
self.titleLabel?.text = self.course.title!
self.descriptionLabel?.text = self.course.description!
if let creditsArray = self.course.credits {
let minimumCredit = creditsArray[0] as Int
self.creditsLabel?.text = String(minimumCredit)
}
self.tableView.reloadData()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return course.sections.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let cell = tableView.dequeueReusableCellWithIdentifier("SectionCell", forIndexPath: indexPath) as! SectionTableViewCell
let sectionCell = course.sections[indexPath.row]
cell.termLabel?.text = sectionCell.term
cell.timeLabel?.text = sectionCell.startTime
cell.instructorLabel?.text = sectionCell.instructor
return cell
}
}
When I run, I get the following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I believe that the reason may be that I set up the tableView within the viewController incorrectly.
For the full project, here is a link to the repo: https://github.com/classmere/app/tree/develop
The problem is that you're trying to unwrap an optional whose value is nil. When you declare the course property, since its an optional, its initial value is nil. Usually, optionals are declared with ? and the compiler will prevent you from accessing the underlying value without checking if the value is still nil. In this case however, you've made the course property an expected optional:
var course: Course!
This is like saying "I know that course will always have a value and will never be nil". We don't know that however, since its value is nil until the Alamofire callback successfully completes.
To fix this problem, start by making course a standard optional:
var course: Course?
Now Xcode will complain that you're accessing course without unwrapping it, since your declaration of course no longer unwraps it.
Fix this by forcibly unwrapping everything in the Alamofire callback:
APIService.getCourseByAbbr(abbr) { (data) -> Void in
println("APIService()")
self.course = Course(courseJSON: data)
// Notice we can access self.course using ! since just assigned it above
self.titleLabel?.text = self.course!.title!
self.descriptionLabel?.text = self.course!.description!
if let creditsArray = self.course!.credits {
let minimumCredit = creditsArray[0] as Int
self.creditsLabel?.text = String(minimumCredit)
}
self.tableView.reloadData()
}
Then in cellForRowAtIndexPath, we will use optional chaining to ensure we only access course's properties if they exist:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let cell = tableView.dequeueReusableCellWithIdentifier("SectionCell", forIndexPath: indexPath) as! SectionTableViewCell
if let section = course?.sections[indexPath.row] {
cell.termLabel?.text = section.term
cell.timeLabel?.text = section.startTime
cell.instructorLabel?.text = section.instructor
}
return cell
}
Finally in numberOfRowsForSection make sure to get the actual number of sections instead of always returning 50. We'll use the nil-coalescing operator to return 0 if course is nil:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return course?.sections.count ?? 0
}
That should fix your problem!
Related
I've got a simple UITable implementation and an array which is called fruits. Taken out of this example. The only difference is that I am using a UIViewController for the UITable insted of a UITableViewController, but this should be irrelevant for now.
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var fruits : [String] = Array()
let thread : GetDataForFruitsArrayThread
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
cell.textLabel?.text = fruits[indexPath.row]
return cell
}
}
Now I have got a viewDidLoad function in this class also. In the function I am calling a thread which is getting the array as a parameter. The thread is filling up the array with new values asynchronously. Means I don't know if there will be any values or when do the value appear and are set into the array. So you can imagine what I want to ask on this point: How do I signal the UITableView that I have changed the content of the array once the view has loaded the array into the UITable. In c# there is the NotifyAll() function which notfies all the Listener to an attribute. Is there an easy and performant way to do this in swift, optionally without importing any non-included libraries?
override func viewDidLoad() {
super.viewDidLoad()
self.thread = GetDataForFruitsArrayThread(array: &fruits)
self.thread.StartAsynchronously()
}
EDIT - adding GetDataForFruitsArrayThread
class GetDataForFruitsArrayThread : MyThread {
var array : [String]
init(array: inout [String] ) {
self.array = array
}
override func main() {
// init socket
while(true){
_ = withUnsafeMutablePointer(to: &rcvaddr){
$0.withMemoryRebound(to: sockaddr.self, capacity: 1){
recvfrom(m_socket, &pData, bufferlength, 0, UnsafeMutablePointer($0),
&socketAddressLength)
}
}
var fruit = String(cString:inet_ntoa(rcvaddr.sin_addr), encoding: .ascii)
self.array.append(fruit)
}
}
}
EDIT - 21.11.2017:
Following Sandeep Bhandari's answer I changed the following code in the ViewController:
#IBOutlet weak var tableView1: UITableView!
var fruits : [String] = Array() {
didSet {
self.tableView1.reloadData()
}
}
PROBLEM: I am setting a breakpoint at array.append(fruit) in the GetDataForFruitsArrayThread-class. After that there is a breakpoint in the didSet method. But this breakpoint does not get called after the append()-call! NOTE: The parameter on the init function of the GetDataForFruitsArrayThread-class is an in-out parameter!
UPDATE: I found out that if I do the array.append(fruit)-call on the GetDataForFruitsArrayThread.array in the GetDataForFruitsArrayThread-thread, the Fruits-array in ViewController-class does not get changed, therefore didSet did not get called. How can I change this behaviour?
Because you said you are running a endless process to keep updating your array and you want the UI update once the array changes you can use the below solution.
var fruits : [String] = Array() {
didSet {
self.tableView.reloadData()
}
}
What am I doing ? Simple added a setter to array and whenever value changes I reload the tableView.
EDIT:
The above solution will work only if you modify the inout fruits array you pass to GetDataForFruitsArrayThread
self.thread = GetDataForFruitsArrayThread(array: &fruits)
So don't create one more array property in GetDataForFruitsArrayThread rather directly modify the array passed to GetDataForFruitsArrayThread.
I'm a newbie to Swift and XCode, taking a class in iOS development this summer. A lot of projects we're doing and examples I'm seeing for UI elements like PickerViews, TableViews, etc. are defining everything in the ViewController.swift file that acts as the controller for the main view. This works fine, but I'm starting to get to the point of project complexity where I'd really like all of my code to not be crammed into the same Swift file. I've talked to a friend who does iOS development on the side, he said this is sane and reasonable and well in-line with proper object-oriented programming... but I just can't seem to get it to work. Through trial and error I've gotten to this situation: the app runs in the simulator, the UITableView appears, but I'm not getting it populated with entries. I can get it working just fine when all the code is in the ViewController, but once I start trying to create a new controller class and make an instance of that class the dataSource/delegate of the UITableView I start getting nothing. I feel like I'm either missing some core understanding of Swift here, or doing something wrong with the Interface Builder in XCode.
My end result should be a UITableView with three entries in it; currently I'm getting a UITableView with no entries. I'm following along with a few different examples I've Googled, but primarily this other SO question: UITableView example for Swift
ViewController.swift:
import UIKit
class ViewController: UIViewController{
#IBOutlet var stateTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
var viewController = StateViewController()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
}
StateViewController.swift:
import UIKit
class StateViewController: UITableViewController{
var states = ["Indiana", "Illinois", "Nebraska"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return states.count;
}
func tableView(cellForRowAttableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style:UITableViewCellStyle.default, reuseIdentifier:"cell")
cell.textLabel?.text = states[indexPath.row]
return cell
}
}
In XCode I have the UITableView hooked up to the View Controller; the outlets are set to dataSource and delegate and the referencing outlet is stateTableView.
I'm not getting any errors; I do get a warning on my `var viewController = StateViewController()' statement in ViewController.swift where it wants me to use a constant, but switching it to a constant doesn't change the behavior (this is as it should be, I assume).
Originally I assumed that the error was in my StateViewController.swift file, where I'm not creating an object that adheres to the UITableViewDataSource or UITableViewDelegate protocol, but if I even add them into the class statement I immediately get errors like "Redundant conformance of 'StateViewController' to protocol 'UITableViewDataSource'" - I'm reading that this is because inheriting from UITableViewController automatically inherits the other protocols as well.
The last thing I tried was instead referring to self.states in the StateViewController's tableView functions, but I'm pretty sure self in Swift works the same as it does in Python and it feels like I'm just trying to add magic words at this point.
I've investigated as far as my currently-limited Swift knowledge can take me, so any answer that explains what I'm doing wrong rather than just telling me what to fix would be very appreciated.
Your issue is being caused by a memory management problem. You have the following code:
override func viewDidLoad() {
super.viewDidLoad()
var viewController = StateViewController()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
Think about the lifetime of the viewController variable. It ends when the end of viewDidLoad is reached. And since a table view's dataSource and delegate properties are weak, there is no strong reference to keep your StateViewController alive once viewDidLoad ends. The result, due to the weak references, is that the dataSource and delegate properties of the table view revert back to nil after the end of viewDidLoad is reached.
The solution is to create a strong reference to your StateViewController. Do this by adding a property to your view controller class:
class ViewController: UIViewController{
#IBOutlet var stateTableView: UITableView!
let viewController = StateViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
}
Now your code will work.
Once you get that working, review the answer by Ahmed F. There is absolutely no reason why your StateViewController class should be a view controller. It's not a view controller in any sense. It's simply a class that implements the table view data source and delegate methods.
Although I find it more readable and understandable to implement dataSource/delegate methods in the same viewcontroller, what are you trying to achive is also valid. However, StateViewController class does not have to be a subclass of UITableViewController (I think that is the part that you are misunderstanding it), for instance (adapted from another answer for me):
import UIKit
// ViewController File
class ViewController: UIViewController {
var handler: Handler!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
handler = Handler()
tableView.dataSource = handler
}
}
Handler Class:
import UIKit
class Handler:NSObject, UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myCell")
cell?.textLabel?.text = "row #\(indexPath.row + 1)"
return cell!
}
}
You can also use adapter to resolve this with super clean code and easy to understand, Like
protocol MyTableViewAdapterDelegate: class {
func myTableAdapter(_ adapter:MyTableViewAdapter, didSelect item: Any)
}
class MyTableViewAdapter: NSObject {
private let tableView:UITableView
private weak var delegate:MyTableViewAdapterDelegate!
var items:[Any] = []
init(_ tableView:UITableView, _ delegate:MyTableViewAdapterDelegate) {
self.tableView = tableView
self.delegate = delegate
super.init()
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func setData(data:[Any]) {
self.items = data
reloadData()
}
func reloadData() {
tableView.reloadData()
}
}
extension MyTableViewAdapter: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Hi im \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
delegate?.myTableAdapter(self, didSelect: items[indexPath.row])
}
}
Use Plug and Play
class ViewController: UIViewController, MyTableViewAdapterDelegate {
#IBOutlet var stateTableView: UITableView!
var myTableViewAdapter:MyTableViewAdapter!
override func viewDidLoad() {
super.viewDidLoad()
myTableViewAdapter = MyTableViewAdapter(stateTableView, self)
}
func myTableAdapter(_ adapter: MyTableViewAdapter, didSelect item: Any) {
print(item)
}
}
You are trying to set datasource and delegate of UITableView as UITableViewController. As #Ahmad mentioned its more understandable in same class i.e. ViewController, you can take clear approach separating datasource and delegate of UITableView from UIViewController. You can make subclass of NSObject preferably and use it as datasource and delgate class of your UITableView.
You can also also use a container view and embed a UITableViewController. All your table view code will move to your UITableViewController subclass.Hence seprating your table view logic from your View Controller
Hope it helps. Happy Coding!!
The way I separate those concerns in my projects, is by creating a class to keep track of the state of the app and do the required operations on data. This class is responsible for getting the actual data (either creating it hard-coded or getting it from the persistent store). This is a real example:
import Foundation
class CountriesStateController {
private var countries: [Country] = [
Country(name: "United States", visited: true),
Country(name: "United Kingdom", visited: false),
Country(name: "France", visited: false),
Country(name: "Italy", visited: false),
Country(name: "Spain", visited: false),
Country(name: "Russia", visited: false),
Country(name: "Moldova", visited: false),
Country(name: "Romania", visited: false)
]
func toggleVisitedCountry(at index: Int) {
guard index > -1, index < countries.count else {
fatalError("countryNameAt(index:) - Error: index out of bounds")
}
let country = countries[index]
country.visited = !country.visited
}
func numberOfCountries() -> Int {
return countries.count
}
func countryAt(index: Int) -> Country {
guard index > -1, index < countries.count else {
fatalError("countryNameAt(index:) - Error: index out of bounds")
}
return countries[index]
}
}
Then, I create separate classes that implement the UITableViewDataSource and UITableViewDelegate protocols:
import UIKit
class CountriesTableViewDataSource: NSObject {
let countriesStateController: CountriesStateController
let tableView: UITableView
init(stateController: CountriesStateController, tableView: UITableView) {
countriesStateController = stateController
self.tableView = tableView
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
super.init()
self.tableView.dataSource = self
}
}
extension CountriesTableViewDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return the number of items in the section(s)
return countriesStateController.numberOfCountries()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// return a cell of type UITableViewCell or another subclass
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
let country = countriesStateController.countryAt(index: indexPath.row)
let countryName = country.name
let visited = country.visited
cell.textLabel?.text = countryName
cell.accessoryType = visited ? .checkmark : .none
return cell
}
}
import UIKit
protocol CountryCellInteractionDelegate: NSObjectProtocol {
func didSelectCountry(at index: Int)
}
class CountriesTableViewDelegate: NSObject {
weak var interactionDelegate: CountryCellInteractionDelegate?
let countriesStateController: CountriesStateController
let tableView: UITableView
init(stateController: CountriesStateController, tableView: UITableView) {
countriesStateController = stateController
self.tableView = tableView
super.init()
self.tableView.delegate = self
}
}
extension CountriesTableViewDelegate: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row at index: \(indexPath.row)")
tableView.deselectRow(at: indexPath, animated: false)
countriesStateController.toggleVisitedCountry(at: indexPath.row)
tableView.reloadRows(at: [indexPath], with: .none)
interactionDelegate?.didSelectCountry(at: indexPath.row)
}
}
And this is how easy is to use them from the ViewController class now:
import UIKit
class ViewController: UIViewController, CountryCellInteractionDelegate {
public var countriesStateController: CountriesStateController!
private var countriesTableViewDataSource: CountriesTableViewDataSource!
private var countriesTableViewDelegate: CountriesTableViewDelegate!
private lazy var countriesTableView: UITableView = createCountriesTableView()
func createCountriesTableView() -> UITableView {
let tableViewOrigin = CGPoint(x: 0, y: 0)
let tableViewSize = view.bounds.size
let tableViewFrame = CGRect(origin: tableViewOrigin, size: tableViewSize)
let tableView = UITableView(frame: tableViewFrame, style: .plain)
return tableView
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
guard countriesStateController != nil else {
fatalError("viewDidLoad() - Error: countriesStateController was not injected")
}
view.addSubview(countriesTableView)
configureCountriesTableViewDelegates()
}
func configureCountriesTableViewDelegates() {
countriesTableViewDataSource = CountriesTableViewDataSource(stateController: countriesStateController, tableView: countriesTableView)
countriesTableViewDelegate = CountriesTableViewDelegate(stateController: countriesStateController, tableView: countriesTableView)
countriesTableViewDelegate.interactionDelegate = self
}
func didSelectCountry(at index: Int) {
let country = countriesStateController.countryAt(index: index)
print("Selected country: \(country.name)")
}
}
Note that ViewController didn't create the countriesStateController object, so it must be injected. We can do that from the Flow Controller, from the Coordinator or Presenter, etc. I did it from AppDelegate like so:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let countriesStateController = CountriesStateController()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if let viewController = window?.rootViewController as? ViewController {
viewController.countriesStateController = countriesStateController
}
return true
}
/* ... */
}
If it's never injected - we get a runt-time crash, so we know we must fix it straight away.
This is the Country class:
import Foundation
class Country {
var name: String
var visited: Bool
init(name: String, visited: Bool) {
self.name = name
self.visited = visited
}
}
Note how clean and slim the ViewController class is. It's less than 50 lines, and if create the table view from Interface Builder - it becomes 8-9 lines smaller.
ViewController above does what it's supposed to do, and that's to be a mediator between View and Model objects. It doesn't really care if the table displays one type or many types of cells, so the code to register the cell(s) belongs to CountriesTableViewDataSource class, which is responsible to create each cell as needed.
Some people combine CountriesTableViewDataSource and CountriesTableViewDelegate in one class, but I think it breaks the Single Responsibility Principle. Those two classes both need access to the same DataProvider / State Controller object, and ViewController needs access to that as well.
Note that View Controller had now way to know when didSelectRowAt was called, so we needed to create an additional protocol inside UITableViewDelegate:
protocol CountryCellInteractionDelegate: NSObjectProtocol {
func didSelectCountry(at index: Int)
}
And we also need a delegate property to make the communication possible:
weak var interactionDelegate: CountryCellInteractionDelegate?
Note that neither CountriesTableViewDataSource not CountriesTableViewDelegate class knows about the existence of the ViewController class. Using Protocol-Oriented-Programming - we could even remove the tight-coupling between those two classes and the CountriesStateController class.
I have a custom cell with some simple labels and a UIImage. Everything appears to be set correctly, and stepping through the debugger shows that everything is getting a value and even using the print in the debugger shows that the labels have text. However my table view is still empty when executed. I have been looking at this for too long and cannot figure out the problem.
Here is the cell code
class CurrentFileCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var statusImage: UIImageView!
#IBOutlet weak var dateLabel: UILabel!
var currentContent: AircraftContent! {
didSet{
setStyles(Constants.appStyleSetting)
self.nameLabel.text = currentContent.contentName
self.dateLabel.text = currentContent.contentStatus
self.statusImage.image = UIImage(named: "color_label_circle_green")
}
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
private func setStyles(settings: StyleSettings) {
let tableFont = UIFont(name: settings.bodyFont, size: CGFloat(settings.tableFontSize))
nameLabel.font = tableFont
dateLabel.font = tableFont
// Text color
let tableFontColor = settings.tableFontColor
nameLabel.textColor = tableFontColor
dateLabel.textColor = tableFontColor
}
Here is the ViewController code with a tableview inside.
class CurrentFilesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var content: AircraftContent?
#IBOutlet weak var currentFiles: UILabel!
#IBOutlet weak var downloadingLabel: UILabel!
#IBOutlet weak var readyLabel: UILabel!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.content = loadContent()
setStyles(Constants.appStyleSetting)
//self.tableView.reloadData()
// Do any additional setup after loading the view.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CurrentFileCell", forIndexPath: indexPath) as? CurrentFileCell
cell?.currentContent = content
return cell!
}
func loadContent() -> AircraftContent {
return (NSKeyedUnarchiver.unarchiveObjectWithFile(AircraftContent.ArchiveURL.path!) as? AircraftContent)!
}
private func setStyles(settings: StyleSettings) {
let titleFont = UIFont(name: settings.bodyFont, size: CGFloat(settings.titleFontSize))
let key = UIFont(name: settings.bodyFont, size: CGFloat(settings.tableFontSize))
currentFiles.font = titleFont
downloadingLabel.font = key
readyLabel.font = key
// Text color
let titleFontColor = settings.titleFontColor
currentFiles.textColor = titleFontColor
downloadingLabel.textColor = titleFontColor
readyLabel.textColor = titleFontColor
}
Here are some images showing the debug location where the cell is not empty, and also printing out the label which has a value, but isn't being shown during simulation.
http://imgur.com/a/dBkpe
This is an image showing the prototype cell. The cell has the correct class set as well as the identifier.
http://imgur.com/PKtFTeQ
Lastly another image showing that the prototype cell is linked to the labels within the CurrentFileCell.
http://imgur.com/nW0QUjM
Any help at all with this would be appreciated. I have tried recreating everything but continue to be stumped as it seems like everything is how it should be.
You have to implement the 'heightForRowAtIndexPath' method for the table view
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let height:CGFloat = 75
return height
}
You may consider registering the custom class as it does not appear that you did. You can do that by using the following code in the viewDidLoad of your View Controller.
tableView.registerClass(CurrentFileCell.self, forCellReuseIdentifier: "cell")
If you are using an external nib you will want to use registerNib instead like so:
tableView.registerNib(UINib(name:"ReplaceWithYourNibName", bundle: nil), forCellReuseIdentifier: "ReuseIdentifier")
Of course, replace ReplaceWithYourNibName and ReuseIdentifier with the appropriate values. In addition, if your nib is in a different bundle specify that instead of nil (nil defaults to the main bundle).
However, do not use both registerClass and registerNib as whichever one you call last will be used and they were designed to be mutually exclusive. Whenever you make a custom UITableViewCell you must use either of the two for it to work unless you have set it explicitly in the storyboard.
Also, you could instead, use prototype cells to define your custom cell, which would, I believe, automatically register the cell. But only if you did not use prototype cells make sure to use registerClass or registerNib.
Good luck! Hope this helps!
If your cell is a static cell, then you need to comment out these methods in UITableViewDataSource:
/* override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
} */
I had the same issue.
Data has values and cell rows are showing empty.
I figured it by adding
contentView.addSubview(titleLabel)
where cell values are being set.
I'm creating a playlist app (Mock-Up Below) using Swift and Parse. Each user has the ability add a song (or multiple songs) to the TableView and "Like" or "Dislike" (toggle style) as many songs as they like, including their own. This functionality is very similar to the Instagram "Like" button behaviour.
I'm having a bit of trouble conceptualizing the data model. As shown below, I have a User table and a Playlist table (with all of the added songs):
User Table:
Playlist Table:
I'm having trouble with the next step, which is storing the "Like" data for each user and each song.
UPDATED: I've added a column in the 'PlaylistData' table in Parse called "userVotes". As you can see, I'm appending all of the data into an array called 'voters'. For some reason, I'm getting a fatal run time error - 'fatal error: unexpectedly found nil while unwrapping an Optional value'.
import UIKit
import Parse
class MusicPlaylistTableViewController: UITableViewController {
var usernames = [String]()
var songs = [String]()
var voters = [String]()
var numVotes = 0
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorColor = UIColor.grayColor()
let query = PFQuery(className:"PlaylistData")
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects! as? [PFObject] {
self.usernames.removeAll()
self.songs.removeAll()
self.voters.removeAll()
for object in objects {
let username = object["username"] as? String
self.usernames.append(username!)
let track = object["song"] as? String
self.songs.append(track!)
self.voters = object["userVotes"] as! [String]
print(self.voters)
}
self.tableView.reloadData()
}
} else {
print(error)
}
}
}
override func viewWillAppear(animated: Bool) {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return usernames.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellTrack", forIndexPath: indexPath) as! TrackTableViewCell
cell.username.text = usernames[indexPath.row]
cell.songTitle.text = songs[indexPath.row]
cell.votes.text = "\(numVotes)"
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
}
}
I've also created a TableViewCell swift file for the prototype cell:
import UIKit
import Parse
class TrackTableViewCell: UITableViewCell {
var numVotes = 0
#IBOutlet weak var songTitle: UILabel!
#IBOutlet weak var username: UILabel!
#IBOutlet weak var votes: UILabel!
#IBAction func voteButton(sender: UIButton) {
self.numVotes = self.numVotes + 1
self.votes.text = "\(self.numVotes)"
}
}
At this point, all it does is when the heart is clicked, it increases the vote count by 1 on screen (the user can do this as many times as possible, no data is stored in the database). What I need help with is the best way to implement table(s) which allow me to keep track of which users have voted for each song (or not). I know I need a TRUE / FALSE column somewhere, but I can't seem to figure out how to model it. Any suggestions or links to SWIFT tutorials would be greatly appreciated. A simple explanation on what tables to create (or what columns to add to existing tables) would help the best. Thanks!
There is a UITableView which its cells will be filled by data got with HTTP post request. But UITableView functions executed before the data comes. When the application starts, all three tableView methods executed and then application throws a runtime error. I guess it's because in cellForRowAtIndexPath, messageList is still empty.
Here is the code:
class messageViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var authData : NSDictionary = [:]
var funcLib = functionLibrary()
var messagesList : NSArray = []
var messageCount: Int = 0
#IBOutlet weak var messageTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var authCode = self.authData["auth"] as! String
var userID = self.authData["user_id"] as! String
var messageRequsetBodyData: AnyObject = ["op":"users","op2":"getThisWeekMessages","id":"\(userID)","id2":"","id3":"","authCode":"\(authCode)"] as AnyObject
funcLib.HTTPPostRequest("http://asdasd.asdasdasd.com/services/index.php", bodyData: messageRequsetBodyData){data in
dispatch_async(dispatch_get_main_queue()){
if let data = data{
var messaggesListDic = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as! NSDictionary
println("------MESSAGGES---------")
self.messageCount = messaggesListDic["count"] as! Int
//self.messages = messaggesListDic["messages"] as! NSDictionary
self.messagesList = messaggesListDic["messages"] as! NSArray
println("\(self.messagesList)")
self.messageTableView.reloadData()
}
}
}
self.messageTableView.delegate = self
self.messageTableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func dismissMessageVC(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
println("asdasd")
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
println("asdasd")
println("\(self.messageCount)")
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
println("bdbsdbsdb")
var cell = self.messageTableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? UITableViewCell
let row = indexPath.row
cell!.textLabel!.text = self.messagesList[0]["content"] as? String
return cell!
}
Runtime error description:
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 0 beyond bounds for empty array'
I tried assign the cell label with using cell.textLabel?.text = "asdasd" command and it works. So I think there is no problem with outlets or methods.
How can I assign the data to messageList before cellForRowAtIndexPath executed with using different way?
Yes, if you have a table view that needs to make an asynchronous call to retrieve the data, you should expect the table view data source methods to be called before the asynchronous request is done. But, when the asynchronous request is done, when you simply call tableView.reloadData(), the table view data sources methods will be called a second time. This is a very common pattern.
The issue here, though, is that this code is not gracefully handling the situation that there is no data to display when the table view data sources methods are called the first time. If numberOfRowsForSection returned 0 until data was retrieved (as described by the others, notably by returning messagesList.count(), as suggested by John and Yedidya, rather than returning a fixed number), all would be good.
You return constant value for number of rows even if your message count is zero. Better to return message count.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
println("asdasd")
println("\(self.messageCount)")
return self.messageCount;
}
Replace the numberOfRows function return value with messagesList.count.