Notify UITable from an other Thread - ios

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.

Related

How to pass data in selected cell to another view controller?

The piece of code below prints the content of whichever cell is clicked on in my TableView.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(self.cell[indexPath.row])
}
I want to use the result that is printed in a label on another ViewController.
How do I get the string value from the function and then use it on on the other view? My thought is to use a global variable but I need to get the string value out first.
For example, You can use simple organization of a singleton of another ViewController (SecondScreen) with var main (in case, as usual, when SecondScreen inited via a Storyboard):
class SecondScreen : UIViewController {
// 1. add this var
static var main : SecondScreen? = nil
// 2. Your some UI element
#IBOutlet weak var textButton: UIButton!
// 3. add this method
func updateUI(string : String) {
textButton.setTitle(string, for: .normal)
}
// 4. setting a var
override func viewDidLoad() {
if SecondScreen.main == nil {
SecondScreen.main = self
}
}
// ... another your and standard methods
}
And you can update your SecondScreen like this:
let v = SecondScreen.main
v?.updateUI(string: "yourString")
Also I recommend you to call method async:
DispatchQueue.main.async {
SecondScreen.main?.updateUI(withString : string)
}
I suggest you to learn more about singletons...
At first, when you create a tableView, you have to collect data (string here) of cells in an array or another data collection. And you can get a needed data (strings) with indexPath variable in the method didSelectRowAt. And you can pass the string to another ViewController (let use SecondViewController) with several ways.
Here is an example:
// declaration an array of your strings
var array : [String] = ["First", "Second", "Third", ...]
...
// getting a string from method:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let string = array[indexPath.row]
print(string)
// next, for example, you need to pass the string to a singleton SecondViewController with static var **main**:
SecondViewController.main?.neededString = string
}
Don't forget update in async DispatchQueue:
DispatchQueue.main.async {
SecondViewController.main?.updateUI(withString : string)
}

UITableView with controller separate from ViewController

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.

First cell of custom UITableViewCell is blank

I am querying parse for some information and displaying the info in a UITableViewCell, there is only one entry in the table, a test entry with fictional information but the table is displaying the first cell blank and then the second cell with the information in the table. I have attempted to google this and also add more rows of testing info but either way the table displays one cell at the beginning with no info
Here is my code:
var query = PFQuery(className: "marathons")
query.orderByAscending("end")
query.findObjectsInBackgroundWithBlock { (marathons, error:NSError?) -> Void in
if(error == nil ){
//success
for marathon in marathons! {
self.Name.append(marathon["Name"] as! String)
self.entryNumber.append(marathon["Number"] as! Int)
self.totalEntries.append(marathon["entries"] as! Int)
self.runnerDistance.append(marathon["distance"] as! Int)
}
self.TableView.reloadData()
}else {
print(error)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: TableView
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Name.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let singleCell: TableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell") as! TableViewCell
singleCell.Name.text = names[indexPath.row]
singleCell.entryNumber.text = "\(entryNumbers[indexPath.row])"
singleCell.totalEntries.text = "\(entires[indexPath.row])"
singleCell.runnerDistance.text = "\(distance[indexPath.row])"
return singleCell
}
Name, entryNumber, totalEntries, and runnerDistance defined as:
#IBOutlet var Name: UILabel!
#IBOutlet var entryNumber: UILabel!
#IBOutlet var totalEntries: UILabel!
#IBOutlet var runnerDistance: UILabel!
Any advice? I am using swift, parse as my back end, and XCODE 7
Where are the model objects defined in your code? You're appending to objects named Name, entryNumber, totalEntries, and runnerDistance, but you're trying to assign values from properties named entryNumbers entries and distance.
Edit:
I see what's going on here. Based off your comment, you're initializing your arrays like this:
var names = [String()]
That actually creates an array with one empty string in it [""]. When you call append, you're adding another element to the array, which makes you wind up with ["","John"]. if you want an empty array you should be creating it like this:
var names = [String]()

Connecting TableView within ViewController - self.tableView.reloadData() not working in Swift

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!

dispatch_async() block didn't finish before UITableViewDataSource methods

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.

Resources