I'm new to iOS and Swift and I'm trying to learn a little by creating a simple Todo app. The problem I came across is that no matter how I implement the code (followed multiple tutorials) and storyboards, the data doesn't show and the custom cells is not customized (it looks exactly how the default cells look even though I've customized it). I already connected my delegate and dataSource
Edit: I already assigned the reuse identifier
TodosView.swift
import UIKit
class TodosView: UIViewController {
#IBOutlet weak var todosTable: UITableView!
var todos: [Todo] = []
override func viewDidLoad() {
super.viewDidLoad()
todosTable.delegate = self
todosTable.dataSource = self
self.addTodo()
}
func addTodo() {
let todo1 = Todo(text: "My first todo")
let todo2 = Todo(text: "My second todo")
todos = [todo1, todo2]
}
}
extension TodosView: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return todos.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let todo = todos[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell") as! TodoCell
cell.setTodo(todo: todo)
return cell
}
}
TodoCell.swift
import UIKit
class TodoCell: UITableViewCell {
#IBOutlet weak var todoText: UILabel!
func setTodo(todo: Todo) {
todoText.text = todo.text
}
}
Todo.swift
import Foundation
struct Todo {
var text: String
var done: Bool
init(text: String, done: Bool = false) {
self.text = text
self.done = done
}
}
I succeeded in using your code to successfully generate your Todo rows (I did not reloadData() after calling addTodo());
Having proven that your code does work, it leads me to believe that you have an issue somewhere in your Storyboard setup, more-so than you do in your code itself. A few suggestions:
Verify your custom cell is subclassed as a TodoCell. You can do this by clicking on your TodoCell in Interface Builder, and in the Identity Inspector tab, verify you have this set to TodoCell:
This is likely not the issue as your app would more than likely crash if your cells were not subclassed properly.
Verify you have set the cell identifier in Interface Builder. Again, click on the TodoCell in Interface Builder, go to the Attributes Inspector tab, and verify identifier is set to TodoCell:
Also, do make sure that you've actually connected your tableView and todoText UILabel to your code. I see you have #IBOutlets to these items, but if you were copying and pasting from a tutorial, it's possible you typed in the items and never actually connected them. The gray circle next to your IBOutlet for both the tableView and UILabel should be filled in, like so:
If it's empty, you may not have a connection, which could explain the issue. Again, I copied and pasted your code verbatim and set things per the above suggestions; I do not believe that reloadData() or setting the number of sections will help the issue (as your code did not have them and it's working on my end).
You need to reload your tableview after updating the datasource :-
func addTodo() {
let todo1 = Todo(text: "My first todo")
let todo2 = Todo(text: "My second todo")
todos = [todo1, todo2]
todosTable.reloadData()
}
Edit
I also noticed by looking at the JWC comment you didn't have the numberOfSection method implemented so you must also add the number of section delegate method.
You are adding the data to array after creating the tableView.
Change this line :
var todos: [Todo] = []
to
var todos: [Todo]! {
didSet {
self.todosTable.reloadData()
}
}
and in numberOfRowsInSection :
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard (todos != nil) else {
return 0
}
return todos.count
}
you can use this
func addTodo() {
let todo1 = Todo(text: "My first todo")
let todo2 = Todo(text: "My second todo")
todos = [todo1, todo2]
DispatchQueue.main.async {
self.todosTable.reloadData()
}
}
This might help you
Related
I know that there is a thread dedicated for asking autosizing table view cell in here: why UITableViewAutomaticDimension not working?
but maybe may case little bit different.
so I want to make autosizing of my table view cell based on the text assigned of my label. I think I have tried to set UITableViewAutomaticDimension and also have tried to set the all constraints (top, bottom, right left) with respect to its superview/ cell container. and I have also set the number of line to be zero like this
here is the code of my VC:
class ChatLogVC: UIViewController {
#IBOutlet weak var chatLogTableView: UITableView!
var messageList = [ChatMessage]()
override func viewDidLoad() {
super.viewDidLoad()
listenForMessages()
chatLogTableView.estimatedRowHeight = UITableView.automaticDimension
chatLogTableView.rowHeight = UITableView.automaticDimension
}
private func listenForMessages() {
// Get data from firebase
let fromId = currentUser.uid
let toId = otherUser.uid
let ref = Database.database().reference(withPath: "/user-messages/\(fromId)/\(toId)")
ref.observe(DataEventType.childAdded, with: { (snapshot) in
let messageDictionary = snapshot.value as? [String : Any] ?? [:]
let message = ChatMessage(dictionary: messageDictionary)
self.messageList.append(message)
self.chatLogTableView.reloadData()
})
}
}
//MARK: - UI Table View Data Source & Delegate
extension ChatLogVC : UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellFrom = tableView.dequeueReusableCell(withIdentifier: "fromChatViewCell", for: indexPath) as! fromChatViewCell
let chatMessage = messageList[indexPath.row]
cellFrom.chatMessageData = chatMessage
cellFrom.otherUserData = otherUser
return cellFrom
}
and here is the table view cell:
class fromChatViewCell: UITableViewCell {
#IBOutlet weak var fromChatLabel: UILabel!
#IBOutlet weak var fromProfilePictureImageView: UIImageView!
var chatMessageData : ChatMessage?
var otherUserData : User?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
updateUI()
}
func updateUI() {
guard let message = chatMessageData, let otherUser = otherUserData else {return}
guard let url = URL(string: otherUser.profileImageURL) else {return}
fromChatLabel.text = message.text
fromProfilePictureImageView.kf.setImage(with: url, options: [.transition(.fade(0.2))])
}
}
}
I have also tried to connect the datasource and delegate to the VC:
and here is my label constraint:
and I have also set the number of line to be zero:
and here is my problem:
if in my storyboard I set the text of my label using just few word, then the autosizing doesn't work, it seems that it will only show one line of text even though actually the text from firebase is more than one line
result (app running):
but if I set the text on my label using a lot of words, then the autosizing table view cell will work and will show all the string from the firebase database:
it will expand, but the autosizing still doens't work since the label still clipping
what went wrong in here? I am really confused
I have tried to change bottom label constraint priority to 200, sometimes the autosizing worked but the other time it will not. maybe my problem related to asynchrounous problem, I have tried to reload the table view rght after get the massge from firebase. but I don't know the root cause of my problem
I am trying to create a program on Xcode that allows the user to enter multiple data into a table view through a text field (when a button is clicked). When the data is added I would like it to be stored and not be deleted after the app is closed - for this part I believe that I would have to use NSUserDefaults, however, I am unsure how I would save an array of strings? (I'm only familiar with storing a single string).
This is what my view controller currently looks like.
I have not done much on my view controller at all but this is what it currently has.
import UIKit
class NewViewController: UIViewController {
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Let's tackle this step-by-step...
TL;DR - For your convenience, I've put the final code into a sample project on Github. Feel free to use any or all of the code in your apps. Best of luck!
Step 1 - Conform to UITableView Protocols
"...enter multiple data into a table view..."
At a minimum, UITableView requires you to conform to two protocols in order to display data: UITableViewDelegate and UITableViewDataSource. Interface Builder handles the protocol declaration for you if you use the built-in UITableViewController object, but in your case you cannot use that object because you only want the UITableView to take up a portion of the view. Therefore, you must implement the protocols yourself by adding them to ViewController's signature:
Swift 4
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
}
Step 2 - Implement UITableView Protocol Required Methods
Now that you have the protocols declared, Xcode displays an error until three required methods are implemented inside of your ViewController class. The bare minimum implementation for these methods is:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
You'll implement these methods later, but at this point your code should compile.
Step 3 - Connect UITableView's Protocols to ViewController
Since you are using a standard UITableView object, ViewController is not connected by default to the code you just implemented in the protocol methods. To make a connection, add these lines to viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Alternatively, you could use the CONTROL + DRAG technique in Interface Builder to connect the delegate and data source from your UITableView to ViewController.
NOTE: In this case, self refers to the ViewController since you're inside of the ViewController class.
Step 4 - UITextField Setup
"...through a text field..."
You previously added an IBOutlet for your UITextField that is connected to Interface Builder, so there is nothing more to do here.
Step 5 - IBAction for the Add Button
(when a button is clicked)."
You need to add an IBAction to your ViewController class and connect it to your Add Button in Interface Builder. If you prefer to write code and then connect the action, then add this method to your ViewController:
#IBAction func addButtonPressed(_ sender: UIButton) {
}
If you use Interface Builder and the CONTROL + DRAG technique to connect the action, the method will be added automatically.
Step 6 - Add an Array Property to Store Data Entries
"...save an array of strings..."
You need an array of strings to store the user's entries. Add a property to ViewController that is initialized as an empty array of strings:
var dataArray = [String]()
Step 7 - Finish Implementing UITableView Protocol Methods
At this point you have everything you need to finish implementing UITableView's protocol methods. Change the code to the following:
//1
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Do nothing
}
//2
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
//3
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataArray[indexPath.row]
return cell
}
In the future, if you want to do something when the user taps a cell, you will want to add code to tableView(_:didSelectRowAt:).
You now create the same number of rows as the number of values in dataArray.
To make this work with Interface Builder, make sure you go to the Attributes Inspector for your UITableViewCell and set the Cell Identifier to Cell. Check out the documentation for more on Dequeuing Cells.
Step 8 - Finish Implementing addButtonPressed(_:)
As suggested in #dani's answer, in the action you need to implement code that appends the user's text to the array, but only if the text is not blank or empty. It is also a good idea to check if dataArray already contains the value you entered using dataArray.contains, depending on what you want to accomplish:
if textField.text != "" && textField.text != nil {
let entry = textField.text!
if !dataArray.contains(entry) {
dataArray.append(entry)
textField.text = ""
}
tableView.reloadData()
}
Step 9 - Persist Data with UserDefaults
"When the data is added I would like it to be stored and not be deleted after the app is closed."
To save dataArray to UserDefaults, add this line of code after the line that appends an entry inside of the addButtonPressed(_:) action:
UserDefaults.standard.set(dataArray, forKey: "DataArray")
To load dataArray from UserDefaults, add these lines of code to viewDidLoad() after the call to super:
if let data = UserDefaults.standard.value(forKey: "DataArray") as? [String] {
dataArray = data
}
Try the following:
Create an array that will store all the text entered via the UITextField (ie. var array = [String]()
In the action of that add button, append the text the user has entered in the text field to the array.
if text.text != "" && !text.text.isEmpty {
// append the text to your array
array.append(text.text!)
text.text = "" // empty the `UITextField`
}
In your tableView methods, make the numberOfRows return array.count and just add a UILabel for your custom UITableViewCell that will display each entered item from the array in a separate cell.
if you want to display your data in tableview you need to implement tableview delegates. add a table view cell with a label on it
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
let NSUD_DATA = "dataarray_store"
var dataArray : NSMutableArray!
var userDefault = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
dataArray = NSMutableArray()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:- create a button for adding the strings to array and while clicking that button
func onClickButton(){
let string = text.text
dataArray.add(string)
userDefault.set(dataArray, forKey: NSUD_DATA)
}
for getting array stored in userdefault
func getData() -> NSMutableArray?{
if userDefault.object(forKey: NSUD_DATA) != nil{
return userDefault.array(forKey: NSUD_DATA) as! NSMutableArray
}
return nil
}
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var entertxt: UITextField!
#IBOutlet weak var save: UIButton!
#IBOutlet weak var tableview: UITableView!
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
if let data = UserDefaults.standard.object(forKey: "todolist") as?[String]
{
names = data
}
}
#IBAction func submit(_ sender: Any) {
if entertxt.text != "" {
names.append(entertxt.text!)
UserDefaults.standard.set(names, forKey: "todolist")
tableview.reloadData()
entertxt.text = ""
}
else
{
print("data not found")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! myTableViewCell
cell.namelable.text = names[indexPath.row]
return cell
}
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.
My goal is to make a grouped tableView, but for somehow the data is not added to the table View
Here's the story board picture
I added a table View on top of view controller which is
and the code that I wrote seems like it don't work
import UIKit
import Alamofire
import SwiftyJSON
import KeychainAccess
class SettingsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let keychain = Keychain(server: "https://genietesting.herokuapp.com", protocolType: .HTTPS)
var profile: [String]?
let aboutGenie = [
"How it works",
"About",
"Contact"
]
override func viewDidLoad() {
super.viewDidLoad()
let firstName = keychain[string: "first_name"]
profile = [
firstName!
]
tableView.dataSource = self
tableView.delegate = self
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return profile!.count
} else {
return aboutGenie.count
}
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 {
return "Profile"
} else {
return "About Genie"
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let tableCell = tableView.dequeueReusableCellWithIdentifier("myCell")
return tableCell!
}
}
and of course, I want to make it clickable so that it would go to its own viewController
After some suggestion, I changed most of my codes above and the result is still the same but this time it shows the header
The result is
airsoftFreak,
There are multiple mistakes I can figure out
There is no IBOutlet for your tableView which is added on top of your ViewController.
So you must be having something like
#IBOutlet var tableView: UITableView!
Your SettingsViewController only confirms to UITableViewDataSource and not to UITableViewDelegate. If you wamt to get didSelectRowAtIndexPath to be triggerred you have to confirm to UITableViewDelegate
class SettingsViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
As many have noticed and mentioned in their answer you will have to set your viewController as delegate for both UITableViewDelegate,UITableViewDataSource so
self.tableView.dataSource = self
self.tableView.delegate = self
The way you are instantiating cell is wrong as well :) Yopu should not create tableViewCell everytime for each cell :) Go to your TableView in storyBoard add a prototype cell, decorate it the way you want and the set the reusableIndentifier for it. Lets say reusableIndentifier you set is 'myCell'
your cellForRowAtIndexPath will change to
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//assuming you have different cells for each section
switch indexPath.section {
case 0: let tableCell = tableView.dequeueReusableCellWithIdentifier("myCell")
tableCell.textLabel.text = profile[indexPath.row]
return tableCell
//in swift switch has to be exhaustive so default
default: let secondSectionCell = tableView.dequeueReusableCellWithIdentifier("second_section_cell_identifier")
secondSectionCell.textLabel.text =aboutGenie[indexPath.row]
return secondSectionCell
}
}
Try to drag (ctrl+drag) the tableview to the yellow button at the top of the viewcontroller. You will now see to options: datasource and delegate. Choose one of these to and perform the action again for the other. Now the tableview should be linked to your code.
If the option to make it clickable was a question as well:
With the function didSelectRowAtIndexpath, you can achieve this. There should be a lot of stacks about this issue available.
You probably have not wired the UITableView delegate and dataSource methods to the viewController. You can do this in two ways.
1. programatically
create a tableViewOutlet
override fun viewDidLoad() {
super.viewDidLoad()
yourTableViewOutlet.delegate = self
yourTableViewOutlet.dataSource = self
}
in interfaceBuilder
a) open the document outline in the storyboard.
b) control drag from your tableView to your ViewController.
c) connect delegate and dataSource one by one.
click on the cell will fire the delegate method didSelectRowAtIndexPath
self.tableView.delegate and self.tableView.datasource
override func viewDidLoad() {
super.viewDidLoad()
let firstName = keychain[string: "first_name"]
profile = [
firstName!
]
self.tableView.delegate = self
self.tableView.datasource = self
}
I have a UICollectionView containing a matrix of text fields. Since the number of text fields per row can be high, I implemented my custom UICollectionViewLayout to allow scrolling in this screen. When the user submits the form, I want to validate the value entered in every text field, thus I need to loop all the cells.
The problem that I'm facing is that I was using collectionView.cellForItemAtIndexPath for this but then found out that it fails with invisible cells, as I saw on this this question.
I understand the approach in the answer to store the values of the data source (in arrays) and then to loop the data source instead, however I don't know how to do this. I tried using function editingDidEnd as an #IBAction associated to the text field but I don't know how to get the "coordinates" of that text field. My idea behind this is to store the value just entered by the user in a two-dimensions array that I'll use later on to loop and validate.
Many thanks for your help in advance!
You don't have to loop invisible cells. Keep using datasource approach. What you are looking for is the way to map textFields to the datasource.
There are many solutions, but the easy one is using Dictionary.
Here's the code for UITableViewDataSource but you can apply it to UICollectionViewDataSource
class MyCustomCell: UITableViewCell{
#IBOutlet weak var textField: UITextField!
}
class ViewController: UIViewController{
// datasource
var textSections = [ [ "one" , "two" , "three"] , [ "1" , "2" , "3"] ]
// textField to datasource mapping
var textFieldMap: [UITextField:NSIndexPath] = [:]
// MARK: - Action
func textChanged(sender: UITextField){
guard let indexPath = textFieldMap[sender] else { return }
guard let textFieldText = sender.text else { return }
textSections[indexPath.section][indexPath.row] = textFieldText
}
#IBAction func submitButtonTapped(){
// validate texts here
for textRow in textSections{
for text in textRow{
if text.characters.count <= 0{
return print("length must be > 0.")
}
}
}
performSegueWithIdentifier("goToNextPage", sender: self)
}
}
extension ViewController: UITableViewDataSource{
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("identifer") as! MyCustomCell
// set value corresponds to your datasource
cell.textField.text = textSections[indexPath.section][indexPath.row]
// set mapping
textFieldMap[cell.textField] = indexPath
// add action-target to textfield
cell.textField.addTarget(self, action: "textChanged:", forControlEvents: .EditingChanged)
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return textSections[section].count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return textSections.count
}
}