Navigate to ViewController from #IBDesignable UIView didSelectRowAt UITableView - swift

I have created a #IBDesignable View file and implemented tableview in it. Data is coming and tableView in working but only I am facing issue at the time of select cell which has to open to other ViewController. Because its a #IBDesignable view file, not a viewController.swift
How to navigate to viewController from #IBDesignable UIView tableview cell select with passing values?
Error: has no member 'navigationController' if I use self.
Code:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let actionType = AppData?.items?[indexPath.row].actionType
switch actionType {
case 5:
print(actionType ?? 0)
let urlLink = AppData?.items?[indexPath.row].actionUrl
let titleText = AppData?.items?[indexPath.row].textValue
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
vc.url = urlLink ?? ""
vc.titleText = titleText ?? ""
self.navigationController.pushViewController(vc, animated: true)
default:
print(actionType ?? 0)
}
}

You can access the parent view controller using this extension:
extension UIView {
var viewController: UIViewController? {
var responder: UIResponder? = self
while responder != nil {
if let responder = responder as? UIViewController {
return responder
}
responder = responder?.next
}
return nil
}
}
You can use it like this in your code:
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
vc.url = urlLink ?? ""
vc.titleText = titleText ?? ""
self.viewcontroller?.navigationController?.pushViewController(vc, animated: true)
Note: Although you can get the parent controller of UIView, it is not
recommended as the UIView should not be aware of the controller. I
suggest using Delegates.

Your UITableView is a UIView so it's not a UIViewController and has not UINavigationController.
You should create a delegate in your UITableView that will be implemented by your UIViewController.
Something like that :
protocol MyTableViewDelegate: class {
func didSelectRow(url: String, titleText: String)
}
Add a delegate variable in your table view like :
weak var delegate: MyTableViewDelegate?
and call your delegate like this :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let actionType = AppData?.items?[indexPath.row].actionType
switch actionType {
case 5:
print(actionType ?? 0)
let urlLink = AppData?.items?[indexPath.row].actionUrl
let titleText = AppData?.items?[indexPath.row].textValue
self.delegate.didSelectRow(url: url, titleText: titleText)
default:
print(actionType ?? 0)
}
}
In your view controller :
you should add your table view custom class into your ViewController,
then add delegate : yourTableView.delegate = self
implement delegate :
extension MyViewController: MyTableViewDelegate {
func didSelectRow(url: String, titleText: String) {
let vc = storyboard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
vc.url = urlLink
vc.titleText = titleText
self.navigationController.pushViewController(vc, animated: true)
}
}

Related

Swift 5; pushViewController is not working

I'm testing Swift and I created a table list to show JSON data, now I would like to show details for each cell, clicking on it, into a new View Controller.
Testing the App I got the click on the cell but I don't see the new View.
This is the code capturing the click into the ViewController.swift:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
{
private let tableView: UITableView = {
let table = UITableView(frame: .zero,
style: .grouped)
table.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
return table
}()
private let tableView: UITableView = {
let table = UITableView(frame: .zero,
style: .grouped)
table.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
parseJSON()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
}
....
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
vc.selectedImage = "images/logo_2-mini.png"
print("Click \(indexPath) " + vc.selectedImage!) // I see it!
navigationController?.pushViewController(vc, animated: true) // I don't see it
}
}
}
This is the DetailViewController
import UIKit
class DetailViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var selectedImage: String?
override func viewDidLoad() {
super.viewDidLoad()
print("Pre")
if let imageToLoad = selectedImage{
imageView.image = UIImage(named: imageToLoad)
print("ok")
}
}
}
I'm too newbie with swift! Can you help me here?
If you want to show the DetailViewController you should change your code to
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let vc = storyboard.instantiateViewController(withIdentifier: "Detail") as? DetailViewController else { return }
vc.selectedImage = "images/logo_2-mini.png"
print("Click \(indexPath) " + vc.selectedImage!) // I see it!
self.present(vc, animated: true)
}
This way you don't need to embed your ViewController into a navigation controller.
Also, I've created the storyboard variable since I don't see its declaration in your code.

Beginner question on passing data between view controllers

I am trying to recreate the Notes app in iOS. I have created an initial View Controller which is just a table view. A user can go to a Detail View Controller to compose a new note with a Title and Body section. When they click Done, I want to manipulate the tableView with note's details.
I am struggling saving the details of what the user entered to use on my initial view controller.
Here's my Notes class which defines the notes data:
class Notes: Codable {
var titleText: String?
var bodyText: String?
}
Here is the Detail View controller where a user can input Note details:
class DetailViewController: UIViewController {
#IBOutlet var noteTitle: UITextField!
#IBOutlet var noteBody: UITextView!
var noteDetails: Notes?
var noteArray = [Notes]()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(updateNote))
noteTitle.borderStyle = .none
}
#objc func updateNote() {
noteDetails?.titleText = noteTitle.text
noteDetails?.bodyText = noteBody.text
noteArray.append(noteDetails!) // This is nil
// not sure if this is the right way to send the details over
// let vc = ViewController()
// vc.noteArray.append(noteDetails!)
if let vc = storyboard?.instantiateViewController(identifier: "Main") {
navigationController?.pushViewController(vc, animated: true)
}
}
}
I also have an array on my initial view controller as well. I think I need this one to store note data to display in the tableView (and maybe don't need the one on my Detail View controller?). The tableView is obviously not completely implemented yet.
class ViewController: UITableViewController {
var noteArray = [Notes]()
override func viewDidLoad() {
super.viewDidLoad()
print(noteArray)
self.navigationItem.setHidesBackButton(true, animated: true)
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeNote))
}
#objc func composeNote() {
if let dvc = storyboard?.instantiateViewController(identifier: "Detail") as? DetailViewController {
navigationController?.pushViewController(dvc, animated: true)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
noteArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
Just using Delegate:
First create delegate protocol with a func to send back note to your viewController
protocol DetailViewControllerDelegate: AnyObject {
func newNoteDidAdded(_ newNote: Note)
}
Next add the delegate variable to DetailViewController, and call func noteDataDidUpdate to send data back to viewController
class DetailViewController: UIViewController {
weak var delegate: DetailViewControllerDelegate?
#objc func updateNote() {
....
delegate?.newNoteDidAdded(newNote)
}
}
finally, set delegate variable to viewController and implement this in ViewController
class ViewController: UIViewController {
....
#objc func composeNote() {
if let dvc = storyboard?.instantiateViewController(identifier: "Detail") as? DetailViewController {
dvc.delegate = self
navigationController?.pushViewController(dvc, animated: true)
}
}
}
extension ViewController: DetailViewControllerDelegate {
func newNoteDidAdded(_ newNote: Note) {
// do some thing with your new note
}
}

Can't show viewcontroller from UITableViewCell

Basically I have a CollectionView inside a tableviewcell and Here's the method I'm using to push to another viewcontroller.
Here's the code :
class recipeRelated: UITableViewCell,UICollectionViewDelegate,UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let resultViewController = storyBoard.instantiateViewController(withIdentifier: "recipeContainerView") as! recipeContainerView
self.window?.rootViewController?.show(resultViewController,sender: self)
}
}
Thank you in advance!
Use this extension:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
This will give any view access to it's first parent controller. So you can use it like this:
self.parentViewController?.show(resultViewController, sender: self)

Testing a UITableView in a UIViewController programatically instantiated

I want to inject my data manager into my view controller and test it.
The ViewController:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var alert: AlertViewController?
var coreDataManager: CoreDataManagerProtocol?
init(coreDataManager: CoreDataManagerProtocol) {
self.coreDataManager = coreDataManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.coreDataManager = CoreDataManager()
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
#IBAction func addItem(_ sender: Any) {
alert = UIStoryboard(name: Constants.alertStoryBoard, bundle: nil).instantiateViewController(withIdentifier: Constants.alerts.mainAlert) as? AlertViewController
alert?.title = "Enter your task"
alert?.presentToWindow()
alert?.delegate = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreDataManager?.getTasks().count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = coreDataManager?.getTasks()[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = data?.value(forKey: Constants.entityNameAttribute) as? String
return cell
}
}
I want to test this through injecting at mock into the view controller:
class CoreDataManagerMock: CoreDataManagerProtocol {
var storeCordinator: NSPersistentStoreCoordinator!
var managedObjectContext: NSManagedObjectContext!
var managedObjectModel: NSManagedObjectModel!
var store: NSPersistentStore!
func getTasks() -> [NSManagedObject] {
managedObjectModel = NSManagedObjectModel.mergedModel(from: nil)
storeCordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
do {
store = try storeCordinator.addPersistentStore(
ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
// catch failure here
}
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = storeCordinator
var localTasks = [NSManagedObject]()
let entityOne = NSEntityDescription.insertNewObject(forEntityName: Constants.entityName, into: managedObjectContext)
entityOne.setValue(false, forKey: Constants.entityCompletedattribute)
entityOne.setValue("Enter your task", forKey: Constants.entityNameAttribute)
localTasks.append(entityOne)
return localTasks
}
func save(task: String) {
//
}
}
But I'm struggling to test this.
I can't request a cell as I'm not instantiating from the Storyboard (and I can't, since I need to inject the mock core manager.
In every test I try to run tableView resolves to nil
Here is my attempt, thinking I can test my cellForRowAt function directly:
func testtv() {
let CDM = CoreDataManagerMock()
let viewController = ViewController(coreDataManager: CDM)
viewController.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
viewController.tableView(UITableViewMock(), cellForRowAt: IndexPath(row: 0, section: 0))
let actualCell = viewController.tableView?.cellForRow(at: IndexPath(row: 0, section: 0) )
let test = viewController.tableView(UITableViewMock(), cellForRowAt: IndexPath(row: 0, section: 0))
XCTAssertEqual(actualCell?.textLabel?.text, actualCell?.textLabel?.text)
}
But as tableView is nil is nil so I can't register the cell. How can I test cellForRow(at: when I inject my dependency as above?
How I did testing with UIStoryboard instantiated UIViewControllers was by doing this.
// STEP 1
// instantiate the storyboard
let storyboard = UIStoryboard(name: "SomeStoryboard", bundle: nil)
// STEP 2
// instantiate the UIViewController using the storyboard
let sampleViewController = storyboard.instantiateViewController(withIdentifier: "SampleViewController") as! SampleViewController
// STEP 3
// set the properties you need to set
sampleViewController.xxxxx = "foo"
sampleViewController.yyyyy = "bar"
// STEP 4
// call the view so the ui objects would be instantiated
// not doing this would cause a crash
// this also calls `viewDidLoad()`
_ = sampleViewController.view
So in your case since this is how the code would look like:
func testtv() {
let storyboard = UIStoryboard(name: "INSERT STORYBOARD NAME HERE", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
viewController.coreDataManager = CoreDataManagerMock()
_ = viewController.view
// I'm not sure about the code below this comment
// but the above code should work
viewController.tableView(UITableViewMock(), cellForRowAt: IndexPath(row: 0, section: 0))
let actualCell = viewController.tableView?.cellForRow(at: IndexPath(row: 0, section: 0) )
let test = viewController.tableView(UITableViewMock(), cellForRowAt: IndexPath(row: 0, section: 0))
XCTAssertEqual(actualCell?.textLabel?.text, actualCell?.textLabel?.text)
}
ViewController
Why this is, is because calling instantiateViewController(withIdentifier: _) uses init?(coder aDecoder: NSCoder)
You can remove this snippet below because you don't need it and it won't work since the UIViewController you're instantiating is from a storyboard.
init(coreDataManager: CoreDataManagerProtocol) {
self.coreDataManager = coreDataManager
super.init(nibName: nil, bundle: nil)
}
What you could do instead is create a static method to instantiate your UIViewController with the parameters you need. Example:
class SomeViewController: UIViewController {
var someObject: SomeObject!
static func instantiate(someObject: SomeObject) -> SomeViewController {
let storyboard = UIStoryboard(name: "SomeStoryboard", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "SomeViewController")
viewController.someObject = someObject
return viewController
}
}
// usage
let vc = SomeViewController.instantiate(someObject: SomeObject())
// or
let vc: SomeViewController = .instantiate(someObject: SomeObject())

How to inject presenter to CustomTableViewCell as dependancy injection

i'm learning Dependency Injection and created an app by MVP.
I could inject presenters for a few VCs in AppDelegate using storyboard.
But now I created CustomTableViewCell with .xib that has UIButton on the cell so that I would like to process the detail of it in the presenter file.
I tried to create UINib(nibName~ in AppDelegate but when the presenter in CustomTableViewCell is called, it is nil.
I heard that an instance of a cell called in 'AppDelegate' is going to be disposed of. However I don't know any ways not to make presenter nil.
AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let mainTabVC = UIStoryboard(name: "MainTab", bundle: nil).instantiateInitialViewController() as! MainTabViewController
let userDefault = UserDefault()
let presenter = MainTabPresenter(view: mainTabVC, udManager: userDefault)
mainTabVC.inject(presenter: presenter, userDefaultManager: userDefault)
let addTabVC = UIStoryboard(name: "AddTab", bundle: nil).instantiateInitialViewController() as! AddTabViewController
let alertHandller = AlertHandller()
let addPresenter = AddTabPresenter(view: addTabVC, mainView: mainTabVC, alertHandller: alertHandller)
addTabVC.inject(presenter: addPresenter)
let settingTabVC = UIStoryboard(name: "SettingTab", bundle: nil).instantiateInitialViewController() as! SettingTabViewController
let settingPresenter = SettingTabPresenter(view: settingTabVC)
settingTabVC.inject(presenter: settingPresenter, alertHandller: alertHandller)
let vcs = [mainTabVC, addTabVC, settingTabVC]
let mainTabBar = UIStoryboard(name: "MainView", bundle: nil).instantiateInitialViewController() as! MainTabBarController
mainTabBar.setViewControllers(vcs, animated: false)
// no probs until here (inject functions work)
// this is disposed??
let listCell = UINib(nibName: "ListCell", bundle: nil).instantiate(withOwner: ListCell.self, options: nil).first as! ListCell
let cellPresenter = ListCellPresenter(view: mainTabVC)
listCell.inject(presenter: cellPresenter)
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = mainTabBar
window?.makeKeyAndVisible()
return true
}
VC that tableViewDataSource is written
class MainTabViewController: UIViewController {
private let shared = Sharing.shared
private var presenter: MainTabPresenterInput!
private var userDefaultManager: UserDefaultManager!
func inject(presenter: MainTabPresenterInput, userDefaultManager: UserDefaultManager) {
self.presenter = presenter
self.userDefaultManager = userDefaultManager
}
override func viewDidLoad() {
super.viewDidLoad()
tableViewSetup()
}
func tableViewSetup(){
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: "ListCell", bundle: nil), forCellReuseIdentifier: "ListCell")
}
}
extension MainTabViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shared.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell") as! ListCell
if let item = presenter.item(row: indexPath.row) {
cell.configure(item: item)
}
return cell
}
}
customTableViewCell
class ListCell: UITableViewCell {
private let shared = Sharing.shared
private var presenter: ListCellPresenterInput!
func inject(presenter: ListCellPresenterInput) {
self.presenter = presenter
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func favButtonTapped(_ sender: Any) {
let row = self.tag
switch shared.items[row].fav {
case true:
favButton.setImage(UIImage(named: "favIconNon"), for: .normal)
case false:
favButton.setImage(UIImage(named: "favIcon"), for: .normal)
}
presenter.countFavIcon(rowAt: row) // this presenter is nil
}
func configure(item: Item) {
    ・・・
}
}
cell presenter
protocol AddTabPresenterInput {
func addButtonTapped(item item: Item?)
}
protocol AddTabPresenterOutput: AnyObject {
func clearFields()
func showAlert(alert: UIAlertController)
}
final class AddTabPresenter: AddTabPresenterInput {
private weak var view: AddTabPresenterOutput!
private weak var mainView: MainTabPresenterOutput!
private weak var alertHandller: AlertHandllerProtocol!
let shared = Sharing.shared
init(view: AddTabPresenterOutput, mainView: MainTabPresenterOutput, alertHandller: AlertHandllerProtocol) {
self.view = view
self.mainView = mainView
self.alertHandller = alertHandller
}
func addButtonTapped(item item: Item?) {
print("called")
mainView.updateView()
}
}
How Can I solve the issue that presenter is nil?
Hopefully some of you would help me out.
Thank you.
UITableView uses reusable cells, you can find a lot of info in google, here is one of the articles
https://medium.com/ios-seminar/why-we-use-dequeuereusablecellwithidentifier-ce7fd97cde8e
So creating an instance of UITableViewCell in AppDelegate you're doing nothing, it is immediately dispose
Create presenter
// somewhere in your MainTabViewController
let cellPresenter = ListCellPresenter(view: self)
To inject presenter you should use tableView(_:cellForRowAt:) method
// later in code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell") as! ListCell
cell.inject(presenter: cellPresenter)
if let item = presenter.item(row: indexPath.row) {
cell.configure(item: item)
}
return cell
}