UITableView与控制器分开ViewController

时间:2017-06-27 01:50:49

标签: ios swift uitableview

我是Swift和XCode的新手,今年夏天参加iOS开发课程。我们正在做的许多项目和我正在看到的用于UI元素(如PickerViews,TableViews等)的示例正在定义ViewController.swift文件中的所有内容,该文件充当主视图的控制器。这工作正常,但我开始达到项目复杂性的地步,我真的希望我的所有代码都不会被塞进同一个Swift文件中。我和一位做过iOS开发的朋友谈过,他说这是理智和合理的,并且与正确的面向对象编程完全一致......但我似乎无法让它发挥作用。通过反复试验,我遇到了这种情况:应用程序在模拟器中运行,UITableView出现,但我没有填充条目。当所有代码都在ViewController中时,我可以正常工作,但是一旦我开始尝试创建一个新的控制器类并使该类的实例成为UITableView的dataSource / delegate,我就什么也得不到了。我觉得我要么在这里错过了对Swift的一些核心理解,要么在XCode中对Interface Builder做错了。

我的最终结果应该是一个UITableView,里面有三个条目;目前我正在获得一个没有条目的UITableView。我跟着一些我用谷歌搜索的不同例子,但主要是这个其他的问题:UITableView example for Swift

ViewController.swift:

import UIKit

class ViewController: UIViewController{

    @IBOutlet var stateTableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        var viewController = StateViewController()
        self.stateTableView.delegate = viewController
        self.stateTableView.dataSource = viewController
    }
}

StateViewController.swift:

import UIKit

class StateViewController: UITableViewController{
    var states = ["Indiana", "Illinois", "Nebraska"]

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        return states.count;
    }

    func tableView(cellForRowAttableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell = UITableViewCell(style:UITableViewCellStyle.default, reuseIdentifier:"cell")
        cell.textLabel?.text = states[indexPath.row]
        return cell
    }
}

在XCode中,我将UITableView连接到View Controller;网点设置为dataSourcedelegate,引用网点为stateTableView

我没有得到任何错误;我在ViewController.swift中的`var viewController = StateViewController()'语句中得到警告,它要求我使用常量,但将其切换为常量不会改变行为(这是应该的,我应该这样做,我假设)。

最初我假设错误发生在我的StateViewController.swift文件中,我没有创建一个符合UITableViewDataSource或UITableViewDelegate协议的对象,但如果我将它们添加到类语句中,我会立即得到错误,如“ 'StateViewController'与协议'UITableViewDataSource'的冗余一致性“ - 我读到这是因为继承自UITableViewController也会自动继承其他协议。

我尝试的最后一件事是在StateViewController的tableView函数中引用self.states,但我很确定Swift中的self与Python中的相同,感觉我只是想尝试此时添加魔术词。

我已经调查过我目前有限的Swift知识可以带我,所以任何解释我做错了什么而不仅仅是告诉我要解决什么的答案都将非常感激

5 个答案:

答案 0 :(得分:3)

您的问题是由内存管理问题引起的。您有以下代码:

override func viewDidLoad() {
    super.viewDidLoad()
    var viewController = StateViewController()
    self.stateTableView.delegate = viewController
    self.stateTableView.dataSource = viewController
}

考虑viewController变量的生命周期。它在到达viewDidLoad的末尾时结束。由于表格视图的dataSourcedelegate属性为weak,因此一旦StateViewController结束,就没有强有力的参考可让您的viewDidLoad保持活跃状态​​。由于引用较弱,结果是在达到dataSource结束后,表视图的delegatenil属性将恢复为viewDidLoad

解决方案是为StateViewController创建一个强引用。通过向视图控制器类添加属性来执行此操作:

class ViewController: UIViewController{
    @IBOutlet var stateTableView: UITableView!
    let viewController = StateViewController()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.stateTableView.delegate = viewController
        self.stateTableView.dataSource = viewController
    }
}

现在您的代码可以使用。

一旦你开始工作,请回答Ahmed F的答案。绝对没有理由认为你的StateViewController类应该是一个视图控制器。它在任何意义上都不是视图控制器。它只是一个实现表视图数据源和委托方法的类。

答案 1 :(得分:1)

虽然我发现在同一个viewcontroller中实现dataSource / delegate方法更具可读性和可理解性,但你想要实现的目标也是有效的。但是,StateViewController必须是UITableViewController的子类(我认为这是你误解它的部分),例如(改编自{{3对我来说):

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

处理程序类:

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

答案 2 :(得分:0)

您正在尝试将UITableView的数据源和委托设置为UITableViewController。正如@Ahmad在同一个类(即ViewController)中提到的更容易理解,你可以采用明确的方法将UITableView的数据源和委托与UIViewController分开。您可以优先创建NSObject的子类,并将其用作UITableView的数据源和delgate类。

您还可以使用container view并嵌入UITableViewController。您的所有表视图代码都将移动到您的UITableViewController子类.Hence从View Controller中分离您的表视图逻辑

希望它有所帮助。快乐编码!!

答案 3 :(得分:0)

你也可以使用适配器来解决这个问题,使用超级干净的代码并且易于理解,比如

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])
    }
}

使用 即插即用

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)
    }
}

答案 4 :(得分:0)

我在项目中分离这些问题的方式是,通过创建一个类来跟踪应用程序的状态并对数据执行所需的操作。此类负责获取实际数据(创建硬编码或从持久性存储获取数据)。这是一个真实的例子:

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]
    }
}

然后,我创建实现UITableViewDataSource和UITableViewDelegate协议的单独的类:

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)
    }
}

这是现在从ViewController类中轻松使用它们的方法:

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)")
    }
}

请注意,ViewController并未创建countryStateController对象,因此必须将其注入。我们可以从Flow Controller,协调器或Presenter等中进行操作。我是从AppDelegate进行的,如下所示:

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
    }
    /* ... */
}

如果从未注入它-我们会遇到崩溃时间,因此我们知道我们必须立即对其进行修复。

这是国家/地区类别:

import Foundation

class Country {

    var name: String
    var visited: Bool

    init(name: String, visited: Bool) {
        self.name = name
        self.visited = visited
    }
}

请注意ViewController类的干净程度和苗条程度。它少于50行,并且如果从Interface Builder创建表视图-它将变小8-9行。

上面的

ViewController可以完成它应该做的事情,它是View和Model对象之间的中介。该表是否显示一种类型还是多种类型的单元格并不重要,因此用于注册单元格的代码属于CountrysTableViewDataSource类,该类负责根据需要创建每个单元格。

有些人将CountrysTableViewDataSource和CountrysTableViewDelegate结合在一个类中,但是我认为这违反了单一责任原则。这两个类都需要访问相同的DataProvider / State Controller对象,而ViewController也需要访问该对象。

请注意,View Controller现在可以知道何时调用didSelectRowAt,因此我们需要在UITableViewDelegate内部创建一个附加协议:

protocol CountryCellInteractionDelegate: NSObjectProtocol {

    func didSelectCountry(at index: Int)
}

我们还需要一个委托属性来使通信成为可能:

weak var interactionDelegate: CountryCellInteractionDelegate?

请注意,无论是CountrysTableViewDataSource还是CountrysTableViewDelegate类都不知道ViewController类的存在。使用面向协议的编程-我们甚至可以消除这两个类与CountrysStateController类之间的紧密耦合。