How is it possible that the navigation controller shown in the screenshot is described in the memory debugger as having 1 children (of type ContactsTableViewController) and at the same time in the lldb prompt is described as having 0 elements?
If you want more context or the code to reproduce it, please look at the code shown in: here
UPDATE:
I haven't run any other commands between the time I captured the memory graph and the time I ran the po command.
The minimal reproducible example is the following playground snippet ( I only have seen this strange behaviour when using a UIViewControllerRepresentable.Coordinator, that's why the minimal example is a bit complex):
import PlaygroundSupport
import SwiftUI
import UIKit
protocol Coordinator {
var nav: UINavigationController { get }
func start()
}
final class FlowCoordinator: Coordinator {
struct Dependencies {
let nav: UINavigationController
let screen1Factory: () -> UIViewController
let screen2Factory: () -> UIViewController
}
var nav: UINavigationController
let screen1Factory: () -> UIViewController
let screen2Factory: () -> UIViewController
init(dependencies: FlowCoordinator.Dependencies) {
self.nav = dependencies.nav
self.screen1Factory = dependencies.screen1Factory
self.screen2Factory = dependencies.screen2Factory
}
func start() {
let vc = screen1Factory() as! ViewController
vc.flowCoordinator = self
nav.pushViewController(vc, animated: false)
print("FlowCoordinator.started: flowCoordinator.nav: ", nav, "VCs count: ", nav.viewControllers.count)
}
func screen2() {
print("Pushing screen2")
let vc = screen2Factory() as! ViewController
vc.flowCoordinator = self
nav.pushViewController(vc, animated: false)
print("Coordinator.viewController.flowCoordinator.nav: ", "VCs count: ",
nav.viewControllers.count)
}
}
final class ViewController: UITableViewController {
weak var flowCoordinator: FlowCoordinator?
// Array of cities to display in the table view
let cities = ["New York", "Paris", "Tokyo"]
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CityCell")
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section
return cities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CityCell", for: indexPath)
// Configure the cell...
cell.textLabel?.text = cities[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
flowCoordinator?.screen2()
}
}
final class AppDependencies {
lazy var flowCoordinator: FlowCoordinator = {
let navigationController = UINavigationController()
navigationController.navigationBar.prefersLargeTitles = true
let flowCoordinatorDependencies = FlowCoordinator.Dependencies(
nav: navigationController,
screen1Factory: makeViewController1,
screen2Factory: makeViewController2)
return FlowCoordinator(dependencies: flowCoordinatorDependencies)
}()
func makeViewController1() -> UIViewController {
let vc = ViewController()
vc.title = "View 1"
return vc
}
func makeViewController2() -> UIViewController {
let vc = ViewController()
vc.title = "View 2"
return vc
}
}
[![enter image description here][1]][1]
struct View1: UIViewControllerRepresentable {
let flowCoordinator: FlowCoordinator
func makeUIViewController(context: Context) -> ViewController {
return context.coordinator.viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
extension View1 {
final class Coordinator: NSObject, UITableViewDelegate {
var viewController: ViewController
var flowCoordinator: FlowCoordinator
init(flowCoordinator: FlowCoordinator) {
self.flowCoordinator = flowCoordinator
self.flowCoordinator.start()
self.viewController = self.flowCoordinator.nav.viewControllers.first as! ViewController
super.init()
viewController.tableView.delegate = self
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("View1.tableView(_:didSelectRowAt:indexPath)")
print("Coordinator.viewController.flowCoordinator.nav: ", viewController.flowCoordinator?.nav ?? "nil", "VCs count: ",
viewController.flowCoordinator?.nav.viewControllers.count ?? "nil")
viewController.flowCoordinator?.screen2()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(flowCoordinator: flowCoordinator)
}
}
struct ContentView: View {
let deps = AppDependencies()
var body: some View {
View1(flowCoordinator: deps.flowCoordinator)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
let deps = AppDependencies()
//deps.flowCoordinator.start()
let nav = deps.flowCoordinator.nav
//let host = nav
let host = UIHostingController(rootView: View1(flowCoordinator: deps.flowCoordinator) )
PlaygroundPage.current.liveView = host
Related
I have a UICollectionView placed inside a UITableViewCell. The collection view has its scroll direction set to horizontal. I have set the collection view and the collection view cell in the table view right, but when I run it the images don't show up and as I see that data doesn't pass right from table view cell to collection view cell. I cannot find anything wrong. I hope someone can find the mistake with a clearer mind.
Imagine that I want the first cell to have a collection view of images horizontally and in the other cells rows of names for example.
You can see my project on this GitHub account: https://github.com/BenSeferidis/Nft-Assets/tree/v5 for better understanding.
Lobby View Controller (Main VC):
The AssetTableViewCell is another custom cell that generates the rest of the data from my models: NFT API
import UIKit
class LobbyViewController: UIViewController {
// MARK: - IBProperties
#IBOutlet weak var tableView: UITableView!
// MARK: - Properties
var data: [DataEnum] = []
var likes:[Int] = []
var numlikes: Int = 0
var nfts: [Nft] = []
let creators : [Creator] = []
var icons: [Icon] = []
var loadData = APICaller()
// MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "AssetTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "AssetTableViewCell")
let nib2 = UINib(nibName: "CreatorsTableViewCell", bundle: nil)
tableView.register(nib2, forCellReuseIdentifier: "CreatorsTableViewCell")
tableView.dataSource = self //method to generate cells,header and footer before they are displaying
tableView.delegate = self //method to provide information about these cells, header and footer ....
downloadJSON {
self.tableView.reloadData()
print("success")
}
loadData.downloadData { (result) in
print(result)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? PresentViewController {
destination.nft = nfts[tableView.indexPathForSelectedRow!.row]
destination.delegate = self
}
}
// MARK: - Methods
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://public.arx.net/~chris2/nfts.json")
URLSession.shared.dataTask(with: url!) { [self] data, response, error in
if error == nil {
do {
self.nfts = try JSONDecoder().decode([Nft].self, from: data!)
let creators = nfts.map { nft in
nft.creator
}
self.data.append(.type1(creators: creators))
self.nfts.forEach { nft in
self.data.append(.type2(nft: nft))
}
DispatchQueue.main.async {
completed()
}
}
catch {
print("error fetching data from api")
}
}
}.resume()
}
}
// MARK: - Extensions
extension LobbyViewController : UITableViewDelegate , UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? 200 : UITableView.automaticDimension
}
//gemizo ta rows tou table
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch self.data[indexPath.item] {
case .type1(let creators):
print("--->", creators)
let cell = tableView.dequeueReusableCell(withIdentifier: "CreatorsTableViewCell",
for: indexPath) as! CreatorsTableViewCell
cell.updateCreators(creators)
return cell
case .type2(let nft):
let cell = tableView.dequeueReusableCell(withIdentifier: "AssetTableViewCell",
for: indexPath) as! AssetTableViewCell
cell.nameLabel?.text = nft.name
cell.nameLabel.layer.cornerRadius = cell.nameLabel.frame.height/2
cell.likesLabel?.text = "\((numlikes))"
let imgUrl = (nft.image_url)
print(imgUrl)
cell.iconView.downloaded(from: imgUrl)
cell.iconView.layer.cornerRadius = cell.iconView.frame.height/2
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetails", sender: self)
}
}
extension LobbyViewController : TestDelegate{
func sendBackTheLikess(int: Int) {
numlikes = int
tableView.reloadData()
}
}
// MARK: - Enums
enum DataEnum {
case type1(creators: [Creator])
case type2(nft: Nft)
}
// MARK: - Struct
struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
Creators TableView Cell :
import UIKit
class CreatorsTableViewCell: UITableViewCell {
//MARK: - IBProtperties
#IBOutlet var creatorsCollectionView: UICollectionView!
//MARK: - Properties
var nft : Nft?
var creators : [Creator] = []
weak var delegate : CreatorsTableViewCellDelegate?
//MARK: - Life Cyrcle
override func awakeFromNib() {
super.awakeFromNib()
creatorsCollectionView.dataSource = self
creatorsCollectionView.delegate = self
let nibName = UINib(nibName: "CollectionViewCell", bundle: nil)
creatorsCollectionView.register(nibName, forCellWithReuseIdentifier: "CollectionViewCell")
}
func updateCreators( _ creators: [Creator]) {
self.creators = creators
}
required init?(coder aDecoder : NSCoder) {
super.init(coder: aDecoder)
}
}
//MARK: - Extensions
extension CreatorsTableViewCell : UICollectionViewDelegate , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: 30, height: 30)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return creators.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = creatorsCollectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell",
for: indexPath) as! CollectionViewCell
cell.renewCreators(creators)
cell.creatorName.text = creators[indexPath.row].user.username
cell.creatorName.layer.cornerRadius = cell.creatorName.frame.height/2
cell.creatorsImg.image = UIImage(named: creators[indexPath.row].profileImgURL )
cell.creatorsImg.layer.cornerRadius = cell.creatorsImg.frame.height/2
return cell
// cell.backgroundColor = .brown
// cell.creatorName?.text = creators[indexPath.row].user.username
// let imgUrl = (creators[indexPath.row].profileImgURL)
// print(imgUrl)
// cell.creatorsImg.downloaded(from: imgUrl)
// return cell
}
}
//MARK: - Protocols
protocol CreatorsTableViewCellDelegate: AnyObject {
func didSelectPhoto(index: Int)
}
CollectionViewCell:
import UIKit
class CollectionViewCell: UICollectionViewCell {
//MARK: - IBProperties
#IBOutlet var creatorsImg: UIImageView!{
didSet {
creatorsImg.contentMode = .scaleAspectFit
}
}
#IBOutlet var creatorName: UILabel!
//MARK: - Properties
var nft : Nft?
var creators : [Creator] = []
//MARK: - Life Cyrcle
override func awakeFromNib() {
super.awakeFromNib()
print(creators)
creatorName.backgroundColor = .systemCyan
creatorsImg.layoutIfNeeded()
creatorsImg.layer.cornerRadius = creatorsImg.frame.height / 2
}
func setUpCollectionViewCell(_ nft: Nft) {
}
func renewCreators( _ creators: [Creator]) {
self.creators = creators
}
}
//MARK: - Protocols
protocol CollectionViewCellDelegate: AnyObject {
func didSelectPhoto(index: Int)
}
Models:
import Foundation
// MARK: - Nft
struct Nft: Codable{
let id:Int
let image_url:String
let name:String
let creator: Creator
}
// MARK: - Icon
struct Icon:Codable{
let image_url:String
}
// MARK: - Creator
struct Creator: Codable {
let user: User
let profileImgURL: String
enum CodingKeys: String, CodingKey {
case user
case profileImgURL = "profile_img_url"
}
}
// MARK: - User
struct User: Codable {
let username: String?
}
APICaller :
import Foundation
final class APICaller {
static let shared = APICaller()
public struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
public func downloadData(completion:#escaping (Result<[Nft], Error>) -> Void )
{
guard let url = URL(string:Constants.url)else{
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
//print(response)
print("here")
guard let data = data , error == nil else{
print("something went wrong")
return
}
print("here4")
//mexri edo exoume parei ta data kai tora me to do-catch tha ta kanoume convert se object
do{
//Decode the response
let nfts = try JSONDecoder().decode([Nft].self, from: data)
completion(.success(nfts))
print(nfts)
}catch{
completion(.failure(error))
}
}
task.resume()
}
}
I'm working on an App where you can track your reading progress for Books.
The HomeViewController contains a TableView that lists the books you have added. It has a progress Bar and shows what page you're on. The AddBookController is for adding Data about a book that gets delegated to the HomeViewController and is then listed as a TableView Row. The BookDetailController is shown when you select a Row and is for updating the Page you're on. I'll provide some screenshots.
I'm stuck at changing the "currentPage" property of the TableView Cell once you update it in the BookDetailViewController. I'm able to send the updatedPage to the HomeViewController (where the TableView is) but I don't know how to align the values of the Cells with the new value. My idea was to use an didSet-observer but I can't figure out how to set it up the right way.
Here is my code:
HomeViewController
class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SendingBookDataProtocol {
var updatedPage = String() {
didSet { // probably at the wrong place but everything I've tried didn't work
print(updatedPage)
tableView?.reloadData()
}
}
var items = [BookItem]()
var item: BookItem?
override func viewDidLoad() {
super.viewDidLoad(){
tableView?.delegate = self
tableView?.dataSource = self
let nib = UINib(nibName: "BookCell", bundle: nil)
tableView?.register(nib, forCellReuseIdentifier: "BookCell")
}
func sendDataToHomeController(bookEntry item:BookItem) {
items.append(item)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let bookDetailVc = self.storyboard?.instantiateViewController(withIdentifier: "BookDetailView") as? BookDetailViewController
let item = items[indexPath.row]
let currentPageInt = Float(item.currentPage)!
let totalPagesInt = Float(item.totalPages)!
let result = Int(totalPagesInt - currentPageInt)
let percentageRead = Int((currentPageInt / totalPagesInt) * 100)
bookDetailVc?.lblName = item.title
bookDetailVc?.lblCurrentPage = Int(Float(item.currentPage)!)
// ...some more Code. Basically just sending the Data to BookDetailViewController
self.navigationController!.pushViewController(bookDetailVc!, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath) as! BookCell
item = items[indexPath.row]
cell.title.text = item?.title
//... and so on
cell.pageNumbers.text = "S. " + item!.currentPage + " / " + item!.totalPages //here currentPage needs to be updated
return cell
}
}
BookDetailViewController
class BookDetailViewController: HomeViewController, UIPickerViewDelegate, UIPickerViewDataSource{
#IBOutlet weak var bookTitle: UILabel!
//...
#IBOutlet weak var numberPicker: UIPickerView!
var lblName = String()
//...
var lblCurrentPage = Int()
override func viewDidLoad() {
super.viewDidLoad()
self.numberPicker.delegate = self
self.numberPicker.dataSource = self
//...
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let valueSelected = pickerData[row] as String
if let homeVc = storyboard?.instantiateViewController(withIdentifier: "getBookData") as? HomeViewController{
homeVc.updatedPage = valueSelected
homeVc.tableView?.reloadData()
}
}
}
AddBookController
protocol SendingBookDataProtocol {
func sendDataToHomeController(bookEntry: BookItem)
}
struct BookItem {
let title,author,currentPage,totalPages:String
let image: UIImage?
}
class AddBookController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var delegate: SendingBookDataProtocol? = nil
#IBAction func buttonSave(_ sender: Any) {
let bookEntry = BookItem(title: textfieldTitle.text!, author: textfieldAuthor.text!, currentPage: fieldCurrentPage.text!, totalPages: fieldTotalPages.text!, image: bookImage)
self.delegate?.sendDataToHomeController(bookEntry: bookEntry)
dismiss(animated: true, completion: nil)
}
}
// rest should be irrelevant
Here are the screenshots:
You have to create a delegate in BookDetailViewController, like this :
protocol BookDetailDelegate: AnyObject {
func updatePage(for bookItem : BookItem)
}
class BookDetailViewController: HomeViewController, UIPickerViewDelegate, UIPickerViewDataSource {
var item: BookItem
var delegate: BookDetailDelegate?
//...
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let valueSelected = pickerData[row] as String
delegate?.updatePage(valueSelected)
}
}
And then in the HomeViewController:
class HomeViewController {
}
//....
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let bookDetailVc = self.storyboard?.instantiateViewController(withIdentifier: "BookDetailView") as? BookDetailViewController
let item = items[indexPath.row]
bookDetailVc?.item = item
bookDetailVc?.delegate = self
self.navigationController!.pushViewController(bookDetailVc!, animated: true)
}
//.....
extension HomeViewController: BookDetailDelegate {
func updatePage(for bookItem: BookItem) {
let index = item.firstIndex(where: {$0.id = bookItem.id}) //here you should have a UUID or something
item[index] = bookItem
tableView.performBatchUpdates({
self.tableView.reloadRows(at: IndexPath(row: index, section: 0, with: .none)}
}, completion: nil))
}
I want to avoid duplicate rewriting same code and create reusable UITableViewController.
I have ExerciseViewController with 3 buttons. Each button push a UITableViewController on the navigation stack. There are three UITableViewControllers: 1) CategoryUITableVC, 2) EquipmentUITableVC, 3) MusclesUITableVC.
All of these three view controllers have almost exactly same layout - cells with labels and accessory buttons. The only difference is that first view controller has got image next to title. Is it worth doing one reusable VC and instantiate it 3 times or maybe better solution create 3 separated VC (but It will be just rewriting almost same code).
I use coordinator pattern.
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
...
//unnecessary code to show
...
// *HERE I INSTANTIATE VIEW CONTROLLERS, I PRESENT THEM MODALLY BUT I WANT TO HAVE NAVIGATION BAR, SO I NEED TO CREATE NEW NAVIGATION CONTROLLERS*
lazy var equipmentVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var equipmentNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: equipmentVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var categoryVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var categoryNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: categoryVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var muscleVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var muscleNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: muscleVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
}
extension ExerciseCoordinator: CustomExerciseDelegate {
func selectCategory() {
navigationController.viewControllers.last?.present(categoryNavController, animated: true, completion: nil)
categoryVC.dataType = .category
}
func selectEquipment() {
navigationController.viewControllers.last?.present(equipmentNavController, animated: true, completion: nil)
equipmentVC.dataType = .equipment
}
func selectMuscles() {
navigationController.viewControllers.last?.present(muscleNavController, animated: true, completion: nil)
muscleVC.dataType = .muscle
}
}
I assign data type to know from where it comes from (CategoryVC/EquipmentVC/MuscleVC) when I will dismiss UITableVC.
Here it is my reusable UITableViewController:
import UIKit
import RealmSwift
class ExerciseCategoryTableViewController: UITableViewController {
var delegate: ExerciseSelectionCriteriaDelegate?
//I use it delegate to send data back after dismiss view
var dataType: DataType?
var data = [String]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData() }}
}
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
if dataType == .category {
let allCategories = RealmService.shared.realm.objects(Category.self)
data = allCategories.compactMap({$0.category})
} else if dataType == .equipment {
let allEquipment = RealmService.shared.realm.objects(Equipment.self)
data = allEquipment.compactMap({$0.equipment})
} else {
let allMuscles = RealmService.shared.realm.objects(Muscles.self)
data = allMuscles.compactMap({$0.muscles})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// below is my own shortcut for dequeue cell, it works
let cell: ExerciseSelectionTableViewCell = tableView.dequeueResuableCell(for: indexPath)
cell.category.text = data[indexPath.row]
if let image = UIImage(named: "\(data[indexPath.row].lowercased())") {
cell.categoryImage.image = image
cell.categoryImage.isHidden = false
} else {
cell.categoryImage.isHidden = true
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
#IBAction func closeViewController(_ sender: UIBarButtonItem) {
closeViewController()
}
#IBAction func saveSelectedCategories(_ sender: UIBarButtonItem) {
saveSelectedData()
}
func saveSelectedData() {
let selectedIndexes = tableView.indexPathsForSelectedRows
if let selectedData = selectedIndexes?.compactMap({data[$0.row]}) {
dismiss(animated: true) {
self.delegate?.selectedFields(for: selectedData, dataType: self.dataType)
}
} else {
dismiss(animated: true) {
self.delegate?.selectedFields(for: nil, dataType: self.dataType)
}
}
}
func closeViewController() {
guard let _ = tableView.indexPathsForSelectedRows else { return dismiss(animated: true, completion: nil)}
Alert.showAlertWithCompletion(on: self, with: "TEXT", message: "TEXT") { _ in
self.dismiss(animated: true, completion: nil)
}
}
}
I will be thankful if you tell me if this approach is correct or maybe better solution are separated view controllers or create protocol with default implementation?
So i am trying to refactor an existing project from MMVM and to add coordinator.
i have the following classes:
protocol Coordinator {
func start()
}
class BaseCoordinator: Coordinator {
private var presenter: UINavigationController
private var genreViewController: ViewController?
private var viewModel = GenreViewModel()
init(presenter: UINavigationController) {
self.presenter = presenter
}
func start() {
let genreViewController = ViewController()
genreViewController.viewModel = viewModel
self.genreViewController = genreViewController
presenter.pushViewController(genreViewController, animated: true)
}
}
class AppCoordinator: Coordinator {
private let window: UIWindow
private let rootViewController: UINavigationController
private var genereListCoordinator: BaseCoordinator?
init(window: UIWindow) {
self.window = window
rootViewController = UINavigationController()
rootViewController.navigationBar.prefersLargeTitles = true
genereListCoordinator = BaseCoordinator(presenter: rootViewController)
}
func start() {
window.rootViewController = rootViewController
genereListCoordinator?.start()
window.makeKeyAndVisible()
}
}
In appDelegate i do as below:
var window: UIWindow?
var applicationCoordinator: AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let window = UIWindow(frame: UIScreen.main.bounds)
let appCordinator = AppCoordinator(window: window)
self.window = window
self.applicationCoordinator = appCordinator
appCordinator.start()
return true
}
VC is:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var viewModel: GenreViewModel!
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(width: self.collectionView.bounds.width, height: 50)
}
self.collectionView.delegate = self
self.collectionView.dataSource = self
collectionView.backgroundView = UIImageView(image: UIImage(named: "131249-dark-grey-low-poly-abstract-background-design-vector.jpg"))
self.viewModel.delegate = self
self.getData()
}
func getData() {
MBProgressHUD.showAdded(to: self.view, animated: true)
viewModel.getGenres()
}
}
extension ViewController: GenreViewModelDelegate {
func didfinish(succsess: Bool) {
MBProgressHUD.hide(for: self.view, animated: true)
if succsess {
self.collectionView.reloadData()
} else {
let action = UIAlertAction(title: "Try again", style: .default, handler: { (action) in
self.getData()
})
Alerts.showAlert(vc: self, action: action)
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GenreCollectionViewCell.reuseIdentifier, for: indexPath) as? GenreCollectionViewCell else {
return UICollectionViewCell()
}
let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
cell.viewModel = cellViewModel
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
viewModel.didSelectGenre(index: (cellViewModel?.id)!)
}
}
VM is :
protocol GenreViewModelDelegate: class {
func didfinish(succsess: Bool)
}
protocol GenreListCoordinatorDelegate: class {
func movieListDidGenre(id: String)
}
class GenreViewModel {
weak var coordinatorDelegate: GenreListCoordinatorDelegate?
var networking = Networking()
var genresModels = [Genres]()
weak var delegate: GenreViewModelDelegate?
func getGenres() {
self.networking.preformNetwokTask(endPoint: TheMoviedbApi.genre, type: Genre.self, success: { [weak self] (response) in
print(response)
if let genres = response.genres {
self?.genresModels = genres
self?.delegate?.didfinish(succsess: true)
} else {
self?.delegate?.didfinish(succsess: false)
}
}) {
self.delegate?.didfinish(succsess: false)
}
}
var count: Int {
return genresModels.count
}
public func cellViewModel(index: Int) -> GenreCollectionViewCellModel? {
let genreCollectionViewCellModel = GenreCollectionViewCellModel(genre: genresModels[index])
return genreCollectionViewCellModel
}
public func didSelectGenre(index: String) {
coordinatorDelegate?.movieListDidGenre(id: index)
}
}
The problem is that when i am trying to inject the viewModel to the ViewController and the push it in the start function it wont work-when the viewDidLoad invoked the viewModel in the VC is nil.
With the same code, I managed to make it work, the viewModel property is populated on my side.
// Coordinator
protocol Coordinator {
func start()
}
class BaseCoordinator: Coordinator {
private var presenter: UINavigationController
private var genreViewController: ViewController?
private var viewModel = ViewModel()
init(presenter: UINavigationController) {
self.presenter = presenter
}
func start() {
let genreViewController = ViewController()
genreViewController.viewModel = viewModel
self.genreViewController = genreViewController
presenter.pushViewController(genreViewController, animated: true)
}
}
// ViewController + ViewModel
struct ViewModel { }
class ViewController: UIViewController {
var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .white
}
}
class AppCoordinator: Coordinator {
private let window: UIWindow
private let rootViewController: UINavigationController
private var genereListCoordinator: BaseCoordinator?
init(window: UIWindow) {
self.window = window
rootViewController = UINavigationController()
rootViewController.navigationBar.prefersLargeTitles = true
genereListCoordinator = BaseCoordinator(presenter: rootViewController)
}
func start() {
window.rootViewController = rootViewController
window.makeKeyAndVisible()
genereListCoordinator?.start()
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var applicationCoordinator: AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let window = UIWindow()
let appCordinator = AppCoordinator(window: window)
self.window = window
self.applicationCoordinator = appCordinator
appCordinator.start()
return true
}
}
Often the issues with Coordinator pattern is to retaining the navigation stack which would not fire viewDidLoad or don't display screen at all for instance. In your case, if only viewModel is missing, I believe it comes from a ViewController constructor issue or an override.
I can see an #IBOutlet in your ViewController which makes me think you are using storyboard / xib file. However, the BaseCoordinator only use a init(), could it be the issue? If using Storyboard, you should try with instantiateViewController(...).
On another note, you should look into retaining all stack of coordinators to handle a full navigation and avoid retaining viewModel or other properties within coordinator when needed. If UINavigationController retain child UIViewController, you wouldn't need to as well.
I am a beginner to iOS coming from the android background and just learned about table view (for me it's an Android ListView). I am trying to separate data source & delegate from view controller. I found some tutorials on how to do so but stuck at figuring out how to send the clicked item to another view controller. The code is below:
class PictureTableViewController: UIViewController {
#IBOutlet weak var pictureTableView: UITableView!
private let picsDataSource: PicturesDataSource
required init?(coder aDecoder: NSCoder) {
self.picsDataSource = PicturesDataSource()
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
pictureTableView.dataSource = picsDataSource
pictureTableView.reloadData()
pictureTableView.delegate = picsDataSource
}
}
class PicturesDataSource: NSObject, UITableViewDataSource, UITableViewDelegate{
private var pictureModels = [PictureModel]()
override init(){
let picModelsDataController = PictureModelsDataController()
pictureModels = picModelsDataController.pictureModels
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pictureModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PictureCell.self)) as! PictureCell
let picModel = pictureModels[indexPath.row]
cell.pictureName = picModel.pictureName
cell.imageItem = picModel.imageItem
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//1 - try loading the "Detail" view controller and typecasting it to be DetailViewController
if let detailViewController = storyboard.instantiateViewController(withIdentifier: "PictureDetailView") as? PictureDetailViewController {
//2 - success! set its selecteImage property
detailViewController.selectedImgName = pictureModels[indexPath.row].pictureName
//3 - now push it onto the navigation controller
navigationController?.pushViewController(detailViewController, animated: true)
}
}
}
Error in: func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){ }. since "storyboard" & "navigationController" are not available in PicturesDataSource class, how can I send clicked item(picture name) to the DetailsViewController
There are StackOverflow answers about separating data source and delegate but did not solve my problem.
Using: Xcode 8.3 beta 6
You can include a reference to main view controller at your table view events handler. Below is a playground code I derived from your example:
import UIKit
// MARK: - Model
struct Picture {
let title: String
let image: UIImage
}
struct PictureModelsDataSource {
let pictures = [
Picture(title: "exampleTitle", image: UIImage(named: "exampleImage")!),
Picture(title: "exampleTitle", image: UIImage(named: "exampleImage")!)
]
}
// MARK - View
class PictureCell: UITableViewCell {
#IBOutlet weak var pictureTitleLabel: UILabel!
#IBOutlet weak var pictureImage: UIImageView!
}
// MARK: - Controller
class PictureTableViewController: UIViewController {
// MARK: - Properties
#IBOutlet weak var pictureTableView: UITableView!
private var pictureListController: PictureListController?
// MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
pictureListController = PictureListController()
pictureListController?.viewController = self
pictureTableView.dataSource = pictureListController
pictureTableView.delegate = pictureListController
pictureTableView.reloadData()
}
}
class PictureDetailViewController: UIViewController {
var selectedPictureTitle: String?
}
class PictureListController: NSObject, UITableViewDataSource, UITableViewDelegate {
// MARK: - Properties
weak var viewController: PictureTableViewController?
private let pictures: [Picture] = {
let pictureModelsDataSource = PictureModelsDataSource()
return pictureModelsDataSource.pictures
}()
// MARK: - View setup
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// MARK: - Event handling
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pictures.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PictureCell.self)) as? PictureCell else {
return UITableViewCell()
}
let picture = pictures[indexPath.row]
cell.pictureTitleLabel.text = picture.title
cell.pictureImage.image = picture.image
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let pictureTitle = pictures[indexPath.row].title
let storyboard = UIStoryboard(name: "exampleStoryboard", bundle: nil)
if let pictureDetailViewController = storyboard.instantiateViewController(withIdentifier: "PictureDetailView") as? PictureDetailViewController {
pictureDetailViewController.selectedPictureTitle = pictureTitle
viewController?.navigationController?.pushViewController(pictureDetailViewController, animated: true)
}
}
}
See StoryBoard object can be obtained by using this
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
Now your second question is about how to get navigation controller. It means how to get currentViewController in your case. This can be get by below code
func getCurrentViewController() -> UIViewController? {
if let rootController = UIApplication.shared.keyWindow?.rootViewController {
var currentController: UIViewController! = rootController
while( currentController.presentedViewController != nil ) {
currentController = currentController.presentedViewController
}
return currentController
}
return nil
}
Now your didSelectRowAt code will look like this
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let detailViewController = storyboard.instantiateViewController(withIdentifier: "PictureDetailView") as? PictureDetailViewController
detailViewController.selectedImgName = pictureModels[indexPath.row].pictureName
self.getCurrentViewController()!.pushViewController(detailViewController, animated: true)
}
You are trying to adhere to MVC, but you are confusing what your actual data source is.
PicturesDataSource is not your data source. It's the code that tells your table how to set itself up.
PictureModelsDataController() is the source from which you get the data that actually populates that table.
All of your posted code should be in the same class:
class PictureTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
Change these lines:
pictureTableView.dataSource = picsDataSource
pictureTableView.delegate = picsDataSource
to
pictureTableView.dataSource = self // Note use of self because this is now the dataSource, not another class
pictureTableView.delegate = self // Note use of self because this is now the delegate, not another class
and remove:
private let picsDataSource: PicturesDataSource
required init?(coder aDecoder: NSCoder) {
self.picsDataSource = PicturesDataSource()
super.init(coder: aDecoder)
}