My application has a common base class for all table controllers, and I'm experiencing a strange bug when I define a generic subclass of that table controller base class. The method numberOfSections(in:) never gets called if and only if my subclass is generic.
Below is the smallest reproduction I could make:
class BaseTableViewController: UIViewController {
let tableView: UITableView
init(style: UITableViewStyle) {
self.tableView = UITableView(frame: .zero, style: style)
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Overridden methods
override func viewDidLoad() {
super. viewDidLoad()
self.tableView.frame = self.view.bounds
self.tableView.delegate = self
self.tableView.dataSource = self
self.view.addSubview(self.tableView)
}
}
extension BaseTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell(style: .default, reuseIdentifier: nil)
}
}
extension BaseTableViewController: UITableViewDelegate {
}
Here's the very simple generic subclass:
class ViewController<X>: BaseTableViewController {
let data: X
init(data: X) {
self.data = data
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func numberOfSections(in tableView: UITableView) -> Int {
// THIS IS NEVER CALLED!
print("called numberOfSections")
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("called numberOfRows for section \(section)")
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellFor: (\(indexPath.section), \(indexPath.row))")
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel!.text = "foo \(indexPath.row) \(String(describing: self.data))"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("didSelect: (\(indexPath.section), \(indexPath.row))")
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
If I create a simple app that does nothing but display ViewController:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let nav = UINavigationController(rootViewController: ViewController(data: 3))
self.window?.rootViewController = nav
self.window?.makeKeyAndVisible()
return true
}
}
The table draws correctly but numberOfSections(in:) is never called! As a result, the table only shows one section (presumably because, according to the docs, UITableView uses 1 for this value if the method isn't implemented).
However, if I remove the generic declaration from the class:
class ViewController: CustomTableViewController {
let data: Int
init(data: Int) {
....
}
// ...
}
then numberOfSections DOES get called!
This behavior doesn't make any sense to me. I can work around it by defining numberOfSections in CustomTableViewController and then having ViewController explicitly override that function, but that doesn't seem like the correct solution: I would have to do it for any method in UITableViewDataSource that has this problem.
This is a bug / shortcoming within the generic subsystem of swift, in conjunction with optional (and therefore: #objc) protocol functions.
Solution first
You'll have to specify #objc for all the optional protocol implementations in your subclass. If there is a naming difference between the Objective C selector and the swift function name, you'll also have to specify the Objective C selector name in parantheses like #objc (numberOfSectionsInTableView:)
#objc (numberOfSectionsInTableView:)
func numberOfSections(in tableView: UITableView) -> Int {
// this is now called!
print("called numberOfSections")
return 1
}
For non-generic subclasses, this already has been fixed in Swift 4, but obviously not for generic subclasses.
Reproduce
You can reproduce it quite easy in a playground:
import Foundation
#objc protocol DoItProtocol {
#objc optional func doIt()
}
class Base : NSObject, DoItProtocol {
func baseMethod() {
let theDoer = self as DoItProtocol
theDoer.doIt!() // Will crash if used by GenericSubclass<X>
}
}
class NormalSubclass : Base {
var value:Int
init(val:Int) {
self.value = val
}
func doIt() {
print("Doing \(value)")
}
}
class GenericSubclass<X> : Base {
var value:X
init(val:X) {
self.value = val
}
func doIt() {
print("Doing \(value)")
}
}
Now when we use it without generics, everything works find:
let normal = NormalSubclass(val:42)
normal.doIt() // Doing 42
normal.baseMethod() // Doing 42
When using a generic subclass, the baseMethod call crashes:
let generic = GenericSubclass(val:5)
generic.doIt() // Doing 5
generic.baseMethod() // error: Execution was interrupted, reason: signal SIGABRT.
Interestingly, the doIt selector could not be found in the GenericSubclass, although we just called it before:
2018-01-14 22:23:16.234745+0100 GenericTableViewControllerSubclass[13234:3471799] -[TtGC34GenericTableViewControllerSubclass15GenericSubclassSi doIt]: unrecognized selector sent to instance 0x60800001a8d0
2018-01-14 22:23:16.243702+0100 GenericTableViewControllerSubclass[13234:3471799] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TtGC34GenericTableViewControllerSubclass15GenericSubclassSi doIt]: unrecognized selector sent to instance 0x60800001a8d0'
(error message taken from a "real" project)
So somehow the selector (e.g. Objective C method name) cannot be found.
Workaround: Add #objc to the subclass, as before. In this case we don't even need to specify a distinct method name, since the swift func name equals the Objective C selector name:
class GenericSubclass<X> : Base {
var value:X
init(val:X) {
self.value = val
}
#objc
func doIt() {
print("Doing \(value)")
}
}
let generic = GenericSubclass(val:5)
generic.doIt() // Doing 5
generic.baseMethod() // Doing 5
If you provide default implementations of the delegate methods (numberOfSections(in:), etc.) in your base class and override them in your subclasses where appropriate, they will be called:
extension BaseTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
...
class ViewController<X>: BaseTableViewController {
...
override func numberOfSections(in tableView: UITableView) -> Int {
// now this method gets called :)
print("called numberOfSections")
return 1
}
...
An alternative approach would be to develop your base class based on UITableViewController which already brings most of the things you need (a table view, delegate conformance, and default implementations of the delegate methods).
EDIT
As pointed out in the comments, the main point of my solution is of course what the OP explicitly didn't want to do, sorry for that... in my defense, it was a lengthy post ;) Still, until someone with a deeper understanding of Swift's type system comes around and sheds some light on the issue, I'm afraid that it still is the best thing you can do if you don't wand to fall back to UITableViewController.
Related
I know how to preserve the action we have done on UITableView, after scrolling back and forth.
Now Iam doing a simple UITableView on MVVM
which has a Follow button . like this.
Follow button changes to Unfollow after click and resets after scrolling.
Where and How to add the code to prevent this?
Here is the tableview Code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Vm.personFollowingTableViewViewModel.count
}
var selectedIndexArray:[Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowList_MVVM.PersonFollowingTableViewCell.identifier , for: indexPath) as? PersonFollowingTableViewCell else{
return UITableViewCell()
}
cell.configure(with: Vm.personFollowingTableViewViewModel[indexPath.row])
cell.delegate = self
return cell
}
and configure(with: ) function
#objc public func didTapButton(){
let defaultPerson = Person(name: "default", username: "default", currentFollowing: true, image: nil)
let currentFollowing = !(person?.currentFollowing ?? false)
person?.currentFollowing = currentFollowing
delegate?.PersonFollowingTableViewCell(self, didTapWith: person ?? defaultPerson )
configure(with: person ?? defaultPerson)
}
func configure(with person1 : Person){
self.person = person1
nameLabel.text = person1.name
usernameLabel.text = person1.username
userImageview.image = person1.image
if person1.currentFollowing{
//Code to change button UI
}
custom delegate of type Person is used
I guess your main issue is with Button title getting changed on scroll, so i am posting a solution for that.
Note-: Below code doesn’t follow MVVM.
Controller-:
import UIKit
class TestController: UIViewController {
#IBOutlet weak var testTableView: UITableView!
var model:[Model] = []
override func viewDidLoad() {
for i in 0..<70{
let modelObject = Model(name: "A\(i)", "Follow")
model.append(modelObject)
}
}
}
extension TestController:UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TestTableCell
cell.dataModel = model[indexPath.row]
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
extension TestController:Actions{
func followButton(cell: UITableViewCell) {
let indexPath = testTableView.indexPath(for: cell)
model[indexPath!.row].buttonTitle = "Unfollow"
testTableView.reloadRows(at: [indexPath!], with: .automatic)
}
}
class Model{
var name: String?
var buttonTitle: String
init(name: String?,_ buttonTitle:String) {
self.name = name
self.buttonTitle = buttonTitle
}
}
Cell-:
import UIKit
protocol Actions:AnyObject{
func followButton(cell:UITableViewCell)
}
class TestTableCell: UITableViewCell {
#IBOutlet weak var followButtonLabel: UIButton!
#IBOutlet weak var eventLabel: UILabel!
var dataModel:Model?{
didSet{
guard let model = dataModel else{
return
}
followButtonLabel.setTitle(model.buttonTitle, for: .normal)
eventLabel.text = model.name
}
}
weak var delegate:Actions?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func followAction(_ sender: Any) {
delegate?.followButton(cell:self)
}
}
To convert this into MVVM approach, there are few things you need to change and move out.
The loop I have in viewDidLoad shouldn’t be there. That will be some API call, and should be handled by viewModel, and viewModel can delegate that to other repository to handle or handle itself. Upon receiving response viewModel update its state and communicate with View (in our case tableView) to re-render itself.
Code in extension where I am updating model object shouldn’t be in controller (model[indexPath!.row].buttonTitle = "Unfollow"), that has to be done by viewModel, and once the viewModel state changes it should communicate with view to re-render.
The interaction responder (Button action) in Cell class, should delegate action to viewModel and not controller.
Model class should be in its own separate file.
In short viewModel handles the State of your View and it should be the one watching your model for updates, and upon change it should ask View to re-render.
There are more things you could do to follow strict MVVM approach and make your code more loosely coupled and testable. Above points might not be 100% correct I have just shared some basic ideas i have. You can check article online for further follow up.
The above answer works . But I have gone through what suggested by #Joakim Danielson to find what exactly happens when you are updating the View and Why it is not updating on ViewModel
So I made an update to delegate function
ViewController delegate function
func PersonFollowingTableViewCell1( _ cell: PersonFollowingTableViewCell, array : Person, tag : Int)
Here, I called the array in the Viewmodel and assigned the values of array in func argument to it.
like ViewModel().Vmarray[tag].currentFollow = array[tag].currentFollow
I'm following a swift development course for beginners and am trying to make a very simple app that creates new tasks with an entered text once a button is pressed, but I am encountering a few errors that I can't seem to understand.
The errors happen in my ViewController and the editor tells me my Core Data Entity does not possess an attribute named "corename" while it very well does.
Here is a screenshot of the errors : 3 errors
And here is my code :
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var tasks : [Taskentity] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
//Get the data from Core data
getData()
//Reload the table view
tableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return tasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath : IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let task = tasks[indexPath.row]
if (task.isImportant == true){
cell.textLabel?.text = "😅 \(tasks.corename!)"
} else {
cell.textLabel?.text = tasks.corename!
}
return cell
}
func getData() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
tasks = try context.fetch(Taskentity.fetchRequest())
} catch {
print("Fetching Data")
}
}
}
Tasks is a Array of Taskentities, you probably meant to access task.corename not tasks.corename
if (task.isImportant == true){
cell.textLabel?.text = "😅 \(task.corename!)"
} else {
cell.textLabel?.text = task.corename!
}
And for the TableViewDelegate problem, just make sure to implement all necessary funcs... You are missing 1:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
I'm currently having an issue. I'm creating a game and I want to be able to use a UITableView to show data (Like levels). However, I'm using strictly SpriteKit and can't seem to get the UITableView and SpritKit to work.
I tried creating a variable in my 'GameScene' class (which is an SKScene) called 'gameTableView' and its value set to a class I made called 'GameRoomTableView'.
var gameTableView = GameRoomTableView()
The class had the value of 'UITableView' (notice that I did not set it to UITableViewController).
class GameRoomTableView: UITableView {
}
I was able to add the tableView as a subview of my SKView. I did this in my 'DidMoveToView' function that's inside my GameScene class. In which got the view to show.
self.scene?.view?.addSubview(gameRoomTableView)
However, I do not know how to change things like the number of sections and how to add cells.The class won't let me access those type of things unless it's a viewController and with that I'd need an actual ViewController to get it to work. I've seen many games use tableViews but I'm not sure how they got it to work, haha.
Please don't hesitate to tell me what I'm doing wrong and if you know of a better way of going about this. Let me know if you have any questions.
Usually I don't prefer subclass the UITableView as you doing, I prefer to use the UITableView delegate and datasource directly to my SKScene class to control both table specs and data to my game code.
But probably you have your personal scheme so I make an example to you follow your request:
import SpriteKit
import UIKit
class GameRoomTableView: UITableView,UITableViewDelegate,UITableViewDataSource {
var items: [String] = ["Player1", "Player2", "Player3"]
override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
self.delegate = self
self.dataSource = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Section \(section)"
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You selected cell #\(indexPath.row)!")
}
}
class GameScene: SKScene {
var gameTableView = GameRoomTableView()
private var label : SKLabelNode?
override func didMove(to view: SKView) {
self.label = self.childNode(withName: "//helloLabel") as? SKLabelNode
if let label = self.label {
label.alpha = 0.0
label.run(SKAction.fadeIn(withDuration: 2.0))
}
// Table setup
gameTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
gameTableView.frame=CGRect(x:20,y:50,width:280,height:200)
self.scene?.view?.addSubview(gameTableView)
gameTableView.reloadData()
}
}
Output:
I am very new to swift and when I say new, I mean I just started this morning, I am having an issue I tried googling for but can't seem to find a solution.
I have this swift file:
import UIKit
class ProntoController: UITableView {
var tableView:UITableView?
var items = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
}
func viewWillAppear(animated: Bool) {
NSLog("Here")
let url = "API.php"
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { data, response, error in
NSLog("Success")
}.resume()
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("CELL") as? UITableViewCell
return cell!
}
}
My problem is with this section of code:
override func viewDidLoad() {
super.viewDidLoad()
}
I get these two errors:
'UITableView' does not have a member named 'viewDidLoad'
Method does not override any method from its superclass
You need to subclass UITableViewController, you're subclassing UITableView
I have two UIViewControllers HomeViewController and ArrangeViewController.
In ArrangeViewController, I have this code
import UIKit
protocol ArrangeClassProtocol
{
func recieveThearray(language : NSMutableArray)
}
class ArrangeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { // class "ArrangeViewController" has no initializer
var ArrangeClassDelegateObject : ArrangeClassProtocol?
// Global Variables Goes Here
var languageNamesArray: NSMutableArray = ["Tamil","English"]
var userDefaults : NSUserDefaults = NSUserDefaults.standardUserDefaults()
var tempArray : NSMutableArray = NSMutableArray()
// Outlets Goes Here
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Saving the Array in a UserDefaultObject
if userDefaults.boolForKey("languageNamesArrayuserDefaults")
{
tempArray = userDefaults.objectForKey("languageNamesArrayuserDefaults") as NSMutableArray
}
else
{
tempArray = languageNamesArray
}
self.tableView.separatorInset = UIEdgeInsetsZero
// TableView Reordering
self.tableView.setEditing(true, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prefersStatusBarHidden() -> Bool
{
return true
}
// Delegate Methods of the UITableView
func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
return 1
}
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return tempArray.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Arrange", forIndexPath: indexPath) as ArrangeTableViewCell
cell.languageName.font = UIFont(name: "Proxima Nova", size: 18)
cell.languageName.text = tempArray.objectAtIndex(indexPath.row) as NSString
cell.showsReorderControl = true
return cell
}
// Delegate Methods for dragging the cell
func tableView(tableView: UITableView!, editingStyleForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCellEditingStyle
{
return UITableViewCellEditingStyle.None
}
func tableView(tableView: UITableView!, canMoveRowAtIndexPath indexPath: NSIndexPath!) -> Bool
{
return true
}
func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!)
{
var stringToMove = tempArray.objectAtIndex(sourceIndexPath.row) as NSString
tempArray .removeObjectAtIndex(sourceIndexPath.row)
tempArray .insertObject(stringToMove, atIndex: destinationIndexPath.row)
}
func tableView(tableView: UITableView!, targetIndexPathForMoveFromRowAtIndexPath sourceIndexPath: NSIndexPath!, toProposedIndexPath proposedDestinationIndexPath: NSIndexPath!) -> NSIndexPath!
{
let section:AnyObject = tempArray .objectAtIndex(sourceIndexPath.section)
var sectionCount = tempArray.count as NSInteger
if sourceIndexPath.section != proposedDestinationIndexPath.section
{
var rowinSourceSection:NSInteger = (sourceIndexPath.section > proposedDestinationIndexPath.section) ? 0 : (sectionCount-1)
return NSIndexPath(forRow: rowinSourceSection, inSection: sourceIndexPath.row)
}
else if proposedDestinationIndexPath.row >= sectionCount
{
return NSIndexPath(forRow: (sectionCount-1), inSection: sourceIndexPath.row)
}
return proposedDestinationIndexPath
}
// Creating the HomeViewController Object and presenting the ViewController
#IBAction func closeButtonClicked(sender: UIButton)
{
userDefaults.setObject(tempArray, forKey: "languageNamesArrayuserDefaults")
userDefaults.synchronize()
ArrangeClassDelegateObject?.recieveThearray(languageNamesArray)
self.dismissViewControllerAnimated(true, completion: nil)
}
}
In the HomeViewController, I this code.
class HomeViewController: UIViewController, ArrangeClassProtocol {
var ArrangeClassObject : ArrangeViewController = ArrangeViewController() // ArrangeViewController is Constructible with ()
override func viewDidLoad() {
super.viewDidLoad()
self.ArrangeClassObject.ArrangeClassDelegateObject = self
}
func recieveThearray(language: NSMutableArray)
{
println(language)
}
}
I wanted to access the Array that am passing from the ArrangeViewController. But its showing errors that I commented out near to the statements. I also used the optional values with the HomeViewController, It also showing error and crashes the app. Please somebody help to figure this out.
I got this idea by a github post. In that project he used one UIViewController and one another swift class. That is also possible for me. But i want to work it out with these two UIViewControllers.
Thanks for the new code. The problem that's creating your error message is here:
#IBOutlet weak var tableView: UITableView!
If you change this code to:
#IBOutlet weak var tableView: UITableView?
Then the compiler will stop complaining about the lack of an initialiser.
However, that's more a diagnostic tool than an actual fix for the problem, which is that you need an initialiser. I think what's going on is that up until you add an uninitialised property at your own class's level in the hierarchy, you're being provided with a default initialiser. At the point you add your uninitialised property, Swift will stop generating the default initialiser, and expect you to provide your own.
If you check the header for UIViewController, you'll find this advice:
/*
The designated initializer. If you subclass UIViewController, you must call the super implementation of this
method, even if you aren't using a NIB. (As a convenience, the default init method will do this for you,
and specify nil for both of this methods arguments.) In the specified NIB, the File's Owner proxy should
have its class set to your view controller subclass, with the view outlet connected to the main view. If you
invoke this method with a nil nib name, then this class' -loadView method will attempt to load a NIB whose
name is the same as your view controller's class. If no such NIB in fact exists then you must either call
-setView: before -view is invoked, or override the -loadView method to set up your views programatically.
*/
init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!)
So, perhaps just try:
init() {
super.init(nibName: nil, bundle: nil)
}
...which should restore the functionality of the originally-provided initialiser. I'm guessing your IBOutlet will be properly "warmed up" from the Storyboard by the base class's initialiser, and you should again be able to construct the View Controller with an argumentless initialiser.