Swift
As you can see in this swift code I am trying to create a table view for my logout controller. However, I am facing problems with the "log out" button showing as a cell in my view controller. I don't know what the problem is because it builds without any errors and I have gone and checked several times but can't find the problem.
import UIKit
struct SettingsCellModel {
let title: String
let handler: (() -> Void)
}
final class SettingsViewController: UIViewController {
private var tableView: UITableView {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}
private var data = [[SettingsCellModel]]()
override func viewDidLoad() {
super.viewDidLoad()
configureModels()
view.backgroundColor = .systemBackground
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
private func configureModels() {
let section = [SettingsCellModel(title: "Log Out") { [weak self] in
self?.didTapLogOutButton()
}
]
data.append(section)
}
private func didTapLogOutButton() {
let actionSheet = UIAlertController(title: "Log Out", message: "Are you sure you want to log out", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
actionSheet.addAction(UIAlertAction(title: "Log Out", style: .destructive, handler: { _ in
logOut (completion: { success in
DispatchQueue.main.async {
if success {
//present log in
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .fullScreen
self.present(loginVC, animated: true) {
self.navigationController?.popToRootViewController(animated: false)
self.tabBarController?.selectedIndex = 0
}
}
else {
//error occurred
fatalError("Could not log out user")
}
}
})
}))
actionSheet.popoverPresentationController?.sourceView = tableView
actionSheet.popoverPresentationController?.sourceRect = tableView.bounds
present(actionSheet, animated: true)
}
}
extension SettingsViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = data[indexPath.section][indexPath.row].title
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
data[indexPath.section][indexPath.row].handler()
}
}
Issue is you are using Computed property private var tableView: UITableView { that means every time you access tableView in your code its closure is executed (or its value is evaluated), and because you instantiate a new instance of tableView in its closure you receive different instances of tableView in all the 3 statements
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
hence tableView instance you added as subView is different from the one that has delegate and data source set as self.
What you need
Option 1:
private var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
Option 2: (Preferred)
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
Hope it helps
Related
Code Below doesn't result in Logout Button to Work, what is wrong?!?
import UIKit
import FirebaseAuth
class ProfileViewController: UIViewController {
// Set Log Out Button
#IBOutlet var tableView: UITableView!
// Data option
let data = ["Log Out"]
// Supply Data Source and Delegate Self
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
}
}
// Add an externsion to inform Data Source and Delegate
extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
// Deque row and style text
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = data[indexPath.row]
cell.textLabel?.textAlignment = .center
cell.textLabel?.textColor = .red
return cell
}
// Make logout to occur when user log outs - unhighlight cell
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Create an alert to Log Out
let actionsheet = UIAlertController(title: "",
message: "",
preferredStyle: .actionSheet)
actionsheet.addAction(UIAlertAction(title: "Log Out",
style: .destructive,
handler: { [weak self] _ in
guard let strongSelf = self else {
return
}
do {
try FirebaseAuth.Auth.auth().signOut()
let vc = LogInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
strongSelf.present(nav, animated: true)
}
catch {
print("Failed to log out")
}
}))
actionsheet.addAction(UIAlertAction(title: "Cancel",
style: .cancel,
handler: nil))
present(actionsheet, animated: true)
}
}
Seems like you have used didDeselectRowAt instead of didSelectRowAt. didSelectRowAt is what gets called when user taps on cell and selects it, didDeselectRowAt gets called when cell is deselected.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let actionsheet = UIAlertController(title: "",
message: "",
preferredStyle: .actionSheet)
actionsheet.addAction(UIAlertAction(title: "Log Out",
style: .destructive,
handler: { [weak self] _ in
guard let strongSelf = self else {
return
}
do {
try FirebaseAuth.Auth.auth().signOut()
let vc = LogInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
strongSelf.present(nav, animated: true)
}
catch {
print("Failed to log out")
}
}))
actionsheet.addAction(UIAlertAction(title: "Cancel",
style: .cancel,
handler: nil))
present(actionsheet, animated: true)
}
I been trying to get my head around passing data to various view controllers over the last few days.
I jumped in a bit too too deep at the start and got very confused.
Finally I think I have sorted. However I cannot see where I have gone wrong below. I am close but think i have gone blind from trying so much over the weekend. Looked at callbacks but I think delegates make more sense to me.
I have 2 view controllers. One with a UITableView and one where the data get inputted via text field.
I have no errors. However the input prints in viewcontroller 2 but does not show up in the UITableView.
Finding it hard to see what I've done wrong.
VC1:
import Foundation
import UIKit
private let reuseidentifier = "Cell"
//here
struct Contact {
var fullname: String
}
class ContactController: UITableViewController {
var contacts = [Contact]()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationItem.title = "Contacts"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(handleAddContact))
view.backgroundColor = .white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseidentifier)
}
#objc func handleAddContact () {
//here
let controller = AddContactController()
controller.delegate = self
self.present(UINavigationController(rootViewController: controller), animated: true, completion: nil)
}
//UITABLEVIEW
override func numberOfSections(in tableView: UITableView) -> Int {
return contacts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseidentifier, for: indexPath)
cell.textLabel?.text = contacts[indexPath.row].fullname
return cell
}
}
//here
extension ContactController: AddContactDelegate {
func addContact(contact: Contact) {
self.dismiss(animated: true) {
self.contacts.append(contact)
self.tableView.reloadData()
}
}
}
My VC2 where data gets inputted
import Foundation
import UIKit
//here
protocol AddContactDelegate {
func addContact(contact: Contact)
}
class AddContactController: UIViewController {
//here
var delegate: AddContactDelegate?
let textField: UITextField = {
let tf = UITextField()
tf.placeholder = "Full Name"
tf.textAlignment = .center
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDone))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancel))
view.addSubview(textField)
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textField.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
textField.widthAnchor.constraint(equalToConstant: view.frame.width - 64).isActive = true
textField.becomeFirstResponder()
}
//here
#objc func handleDone(){
print("done")
guard let fullname = textField.text, textField.hasText else {
print("handle error here")
return
}
let contact = Contact(fullname: fullname)
delegate?.addContact(contact: contact)
print(contact.fullname)
}
#objc func handleCancel(){
self.dismiss(animated: true, completion: nil )
}
}
There is no problem with your delegate implementation , You need to implement numberOfRowsInSection and return 1
override func numberOfSections(in tableView: UITableView) -> Int {
return contacts.count
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return 1
}
Or think if you really need this only
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return contacts.count
}
I believe protocol is implemented properly. Only thing I see is you are adding contacts after vc2 is dismissed as part of completion handler. Place the code before calling dismiss:
In func addContact()
contacts.append(contact)
tableView.reloadData()
dismiss()
So I have a custom SwipeCellTableView class that I inherited from when using UITableViewControllers. Now I want to just use that class for an ib outlet table view controller in a regular View Controller. It is proving to be very difficult and seemingly not worth it anymore. Can this be done?
Here is the superclass which inherits from a TableViewController, I have tried to change it to inherit from a view controller but it just doesn't work out
class SwipeTableViewController: UITableViewController, SwipeTableViewCellDelegate {
var cell: UITableViewCell?
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 80.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! SwipeTableViewCell
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
guard orientation == .right else { return nil }
let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
// handle action by updating model with deletion
self.updateModel(at: indexPath)
}
deleteAction.image = UIImage(named: "delete-icon")
return [deleteAction]
}
func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions {
var options = SwipeTableOptions()
options.expansionStyle = .destructive
//options.transitionStyle = .reveal
return options
}
func updateModel(at indexPath: IndexPath){
//update data model
print("Item deleted from super class")
}
Here is the View Controller I'm trying to access it from:
class GoalsViewController: UIViewController, SwipeTableViewController {
#IBOutlet weak var categoryTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func addCategoryPressed(_ sender: UIButton) {
performSegue(withIdentifier: "showgoalsSeg", sender: self)
}
For reference on how I was using it before when using an actual TableViewController:
class CategoryViewController: SwipeTableViewController {
var categories: Results<Category>? //optional so we can be safe
override func viewDidLoad() {
super.viewDidLoad()
loadCategory()
tableView.rowHeight = 80.0
tableView.separatorStyle = .none
}
//MARK: - Tableview Datasource Methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//Only get the count of categories if it's nil, else 1
return categories?.count ?? 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//fetching cell from super view
let cell = super.tableView(tableView, cellForRowAt: indexPath)
cell.textLabel?.text = categories?[indexPath.row].name ?? "No Categories Added Yet"
cell.backgroundColor = UIColor(hexString: categories?[indexPath.row].color ?? "000000")
return cell
}
//MARK: - Tableview Delegate Methods
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "goToItems", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! ToDoListViewController
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedCategory = categories?[indexPath.row]
}
}
//MARK: - Add New Categories
#IBAction func addButtonPressed(_ sender: Any) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Category", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Add Category", style: .default) { (action) in
let newCategory = Category()
newCategory.name = textField.text!
newCategory.color = UIColor.randomFlat.hexValue()
self.save(category: newCategory)
}
alert.addAction(action)
alert.addTextField { (field) in
textField = field
textField.placeholder = "Add a new category"
}
present(alert, animated: true, completion: nil)
}
func save(category: Category){
let realm = try! Realm()
do {
try realm.write{
realm.add(category)
}
} catch {
print("error saving context")
}
tableView.reloadData()
}
override func updateModel(at indexPath: IndexPath) {
super.updateModel(at: indexPath)
let realm = try! Realm()
if let categoryForDeletion = self.categories?[indexPath.row]{
do{
try realm.write{
realm.delete(categoryForDeletion)
}
} catch {
print("error deleting cell")
}
//tableView.reloadData()
}
}
func loadCategory(){
let realm = try! Realm()
categories = realm.objects(Category.self)
tableView.reloadData()
}
Is this even worth persuing? Or doable?
I have created an app that is a uitableview that has 5 arrays of cell [1, 2, 3, 4, 5] that can edit when the cell is tapped. I have accomplish that when you tapped a cell it shows an alertview and a textfield. You can edit the cell in short. And i have save the edit when it goes to another view controller. But the problem is when i try to close the app and open it, It goes back to the default number of messages like [1, 2, 3, 4, 5]. Any tips how can i save the edited cell but at the same time when the uitableview is opened it shows the array. Sorry for my english. Thanks btw i am new to programming sorry for this code
*Messages.Swift = tableview
class Messages: UITableViewController {
#IBOutlet var uitableView: UITableView!
var tField: UITextField!
var index: Int?
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
//Get the array
if let array = UserDefaults.standard.object(forKey:"messages") as? Array<String> {
self.app.helper.messages = array
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return app.helper.messages.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let messagesCell = app.helper.messages[indexPath.row]
cell.textLabel?.text = messagesCell
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let path = tableView.indexPathForSelectedRow
index = path?.row
changeMessage()
}
func changeMessage() {
let alert = UIAlertController(title: "Message", message: "Enter new preset message", preferredStyle: UIAlertControllerStyle.alert)
alert.addTextField(configurationHandler: configurationTextField)
let doneAction = UIAlertAction(title: "Save", style: UIAlertActionStyle.default, handler:{ (UIAlertAction) in
let modelString = self.tField.text
self.app.helper.messages[self.index!] = modelString!
self.app.helper.saveToDefaults()
self.tableView.reloadData()
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(doneAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
func configurationTextField(textField: UITextField!){
if (textField) != nil {
tField = textField
textField.text = app.helper.messages[index!]
}
}
}
*Helper.swift
class Helper {
var messages: [String] = ["1", "2", "3", "4", "5"]
func saveToDefaults() {
let defaults = UserDefaults.standard
defaults.set(messages, forKey: "messages")
defaults.synchronize()
}
}
try
override func viewWillAppear(_ animated: Bool) {
//Get the array
if let array = UserDefaults.standard.object(forKey:"messages") as? Array<String> {
self.app.helper.messages = array
}
//make this array global(declare it below var index: Int?)
tableView.reloadData()
}
and
let messagesCell = array[indexPath.row]
Try to load user default before setting the table view delegate. Because the table view is loading with the dataset you assigned in helper, not with the dataset from user default.
override func viewDidLoad() {
super.viewDidLoad()
//Get the array
if let array = UserDefaults.standard.object(forKey:"messages") as? Array<String> {
self.app.helper.messages = array
}
tableView.delegate = self
tableView.dataSource = self
}
Or you can reload the table view in view will appear after loading the dataset from user defaut
override func viewWillAppear(_ animated: Bool) {
//Get the array
if let array = UserDefaults.standard.object(forKey:"messages") as? Array<String> {
self.app.helper.messages = array
}
tableView.reloadData()
}
picture
How can i resize this table view so that I can place 2 buttons in the top of the screen without overlapping with the table view? On my screen I only have the button. there is no table view container.
My code:
class FriendListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var friendArray: [Friends] = [];
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: UIScreen.mainScreen().bounds, style: UITableViewStyle.Plain)
tableView.delegate = self
tableView.dataSource = self
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.view.addSubview(self.tableView)
let entityDescription = NSEntityDescription.entityForName("Friends", inManagedObjectContext:
managedObjectContext)
let request = NSFetchRequest()
request.entity = entityDescription
var friendObjs = [Friends]()
do {
friendObjs = try managedObjectContext.executeFetchRequest(request) as! [Friends]
}
catch {
// show something
}
for friend in friendObjs {
friendArray.append(friend)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return friendArray.count;
}
// assign the values in your array variable to a cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell:UITableViewCell=UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
cell.textLabel!.text = friendArray[indexPath.row].firstName;
return cell;
}
// Register when user taps a cell via alert message
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let alert = UIAlertController(title: "Selected Item", message: "clicked", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
One option is to add a header view which contains your buttons. This would require setting the headerView property of the tableView or using tableView delegate methods.
Another option is to add a view and add it to the top of your frame. You would then need to adjust the frame of the table view upon initialize to add it below that custom view.
Something I noticed as well in order for your data to appear is to reload the table view after your fetch completes.
Did you tried putting a tableview header, You can create a view and set the frames matching your need and add 2 buttons. set the tableview's tableviewviewheader property with the view object you just created.
let headerView = UIView(frame: CGRectMake(0, 0, tableView.frame.size.width, 50))
Add your buttons as subview to headerView and then
self.tableView.tableHeaderView = headerView