UITableView with controller separate from ViewController

前端 未结 5 1775
感情败类
感情败类 2020-12-22 03:06

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, Ta

相关标签:
5条回答
  • 2020-12-22 03:22

    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!
        }
    }
    
    0 讨论(0)
  • 2020-12-22 03:29

    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!!

    0 讨论(0)
  • 2020-12-22 03:29

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-22 03:43

    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.

    0 讨论(0)
  • 2020-12-22 03:45

    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.

    0 讨论(0)
提交回复
热议问题