Как создать подкласс пользовательского UIViewController в Swift?

Я хотел бы создать многоразовый контроллер представления UsersViewControllerBase.

UsersViewControllerBase расширяет UIViewController, реализует двух делегатов (UITableViewDelegate, UITableViewDataSource) и имеет два представления (UITableView, UISegmentedControl)

Цель состоит в том, чтобы унаследовать реализацию UsersViewControllerBase и настроить сегментированные элементы управления в классе UsersViewController.

class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
  @IBOutlet weak var segmentedControl: UISegmentedControl!
  @IBOutlet weak var tableView: UITableView!
  //implementation of delegates
}

class UsersViewController: UsersViewControllerBase {
}

UsersViewControllerBase присутствует в раскадровке и все розетки подключены, идентификатор указан.

Вопрос в том, как я могу инициировать UsersViewController, чтобы он наследовал все представления и функциональные возможности UsersViewControllerBase

Когда я создаю экземпляр UsersViewControllerBase, все работает

let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase

Но когда я создаю экземпляр UsersViewController, я получаю nil выходов (я создал простой UIViewController и присвоил ему класс UsersViewController в раскадровке)

let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController

Похоже, представления не наследуются.

Я бы ожидал, что метод init в UsersViewControllerBase получит контроллер с представлениями и выходами из раскадровки:

  class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
      @IBOutlet weak var segmentedControl: UISegmentedControl!
      @IBOutlet weak var tableView: UITableView!
      init(){
        let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
        self = usersViewControllerBase //but that doesn't compile
      }
    }

И я бы инициализировал UsersViewController:

let usersViewController = UsersViewController()

Но к сожалению это не работает


person Alexey    schedule 19.11.2015    source источник
comment
Ну, они унаследованы, так что что-то еще идет не так (например, настройка вашей сцены раскадровки). Вы не забыли подключить эти розетки?   -  person matt    schedule 19.11.2015
comment
нет, я не подключал розетки в UsersViewController, потому что думал, что они уже подключены (поэтому, вероятно, унаследованы) в UsersViewControllerBase. И идея не в том, чтобы воссоздать тот же самый вид в UsersViewController, а в том, чтобы наследовать его от UsersViewControllerBase.   -  person Alexey    schedule 19.11.2015


Ответы (3)


Когда вы создаете экземпляр контроллера представления через instantiateViewControllerWithIdentifier, процесс выглядит следующим образом:

  • он находит сцену с этим идентификатором;
  • он определяет базовый класс для этой сцены; и
  • он возвращает экземпляр этого класса.

И тогда, когда вы впервые получите доступ к view, он будет:

  • создайте иерархию представлений, как показано в этой сцене раскадровки; и
  • подключить розетки.

(Этот процесс на самом деле сложнее, но я пытаюсь свести его к ключевым элементам этого рабочего процесса.)

Смысл этого рабочего процесса в том, что выходы и базовый класс определяются уникальным идентификатором раскадровки, который вы передаете instantiateViewControllerWithIdentifier. Таким образом, для каждого подкласса вашего базового класса вам нужна отдельная сцена раскадровки и подключены выходы к этому конкретному подклассу.

Однако существует подход, который позволит выполнить то, что вы просили. Вместо того, чтобы использовать сцену раскадровки для контроллера представления, вы можете вместо этого реализовать контроллер представления loadView (не путать с viewDidLoad) и программно создать иерархию представления, необходимую классу контроллера представления. Раньше у Apple было хорошее введение в этот процесс в их Руководстве по программированию View Controller для iOS, но с тех пор это обсуждение было удалено, но его все еще можно найти в их устаревшая документация .

Сказав это, я лично не был бы вынужден вернуться к старому миру программно созданных представлений, если бы для этого не было очень веских оснований. Я мог бы быть более склонен отказаться от подхода подкласса контроллера представления и принять что-то вроде одного класса (что означает, что я вернулся в мир раскадровок), а затем передать ему некоторый идентификатор, который диктует поведение, которое я хочу от этого конкретного экземпляра. эта сцена. Если вы хотите сохранить некоторую элегантность объектно-ориентированного подхода, вы можете создать экземпляры пользовательских классов для источника данных и делегировать на основе некоторого свойства, которое вы установили в этом классе контроллера представления.

Я был бы более склонен пойти по этому пути, если бы вам требовалось действительно динамическое поведение контроллера представления, а не программно созданные иерархии представлений. Или, что еще проще, примените исходный подход к созданию подклассов контроллера представления и просто примите во внимание, что вам потребуются отдельные сцены в раскадровке для каждого подкласса.

person Rob    schedule 19.11.2015
comment
Очень хорошее объяснение, большое спасибо. Я предпочитаю использовать подход подклассов и отдельные сцены для каждого подкласса. По крайней мере, я избегаю дублирования кода, а копирование одинаковых сцен в раскадровке не составляет большого труда. - person Alexey; 19.11.2015
comment
Если вы все еще хотите иметь возможность создавать подклассы VC с выходами, вы можете использовать подход .xib. Создайте .xib со всеми представлениями и выходами и создайте для него класс. Теперь UserViewControllerBase добавит этот .xib к своему self.view внутри viewDidLoad() и получит пользовательское представление в качестве члена. затем UsersViewController унаследует базовый класс и будет иметь настраиваемое представление от своего суперкласса, а также будет иметь доступ ко всем выходам через настраиваемое представление члена. Лично мне это решение не нравится, но оно лучше, чем дублирование раскадровки VC. - person gutte; 14.06.2017

Итак, у вас есть базовый класс:

class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    @IBOutlet weak var tableView: UITableView!
    //implementation of delegates
}

[A] И ваш подкласс: class UsersViewController: UsersViewControllerBase { var text = "Hello!" }

[B] Протокол, который будет расширять ваш подкласс:

protocol SomeProtocol {
    var text: String? { get set }
}

[C] И некоторый класс для обработки ваших данных. Например, одноэлементный класс:

class MyDataManager {

    static let shared = MyDataManager()
    var text: String?

    private init() {}

    func cleanup() {
        text = nil
    }
}

[D] И ваш подкласс:

class UsersViewController: UsersViewControllerBase {

    deinit {
        // Revert
        object_setClass(self, UsersViewControllerBase.self)
        MyDataManager.shared.cleanup()
    }
}

extension UsersViewController: SomeProtocol {
    var text: String? {
        get {
            return MyDataManager.shared.text
        }
        set {
            MyDataManager.shared.text = newValue
        }
    }
}

Чтобы правильно использовать подкласс, вам нужно сделать (что-то вроде) это:

class TestViewController: UIViewController {
    ...
    func doSomething() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        //Instantiate as base
        let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
        //Replace the class with the desired subclass
        object_setClass(usersViewController, UsersViewController.self)
        //But you also need to access the property 'text', so:
        let subclassObject = usersViewController as! UsersViewController
        subclassObject.text = "Hello! World."
        //Use UsersViewController object as desired. For example:
        navigationController?.pushViewController(subclassObject, animated: true)
    }
}

РЕДАКТИРОВАТЬ:

As pointed out by @VyachaslavGerchicov, the original answer doesn't work all the time so the section marked as [A] was crossed out. As explained by an answer here:

object_setClass в Swift

... setClass не может добавлять переменные экземпляра к уже созданному объекту.

[B], [C] и [D] были добавлены в качестве обходного пути. Другой вариант для [C] — сделать его частным внутренним классом UsersViewController, чтобы только он имел доступ к этому синглтону.

person yoninja    schedule 12.10.2016
comment
Вы сами пробовали? Он работает так себе - кажется, он позволяет вам переопределять существующие методы, но вам не разрешено добавлять переменные, такие как [CustomObject] - он не работает с EXC_BAD_ACCESS. - person Vyachaslav Gerchicov; 05.02.2021
comment
@VyachaslavGerchicov, привет! Я использовал этот подход раньше, и он работал. Я не уверен, почему это не работает для вас. Можете ли вы создать новый вопрос относительно вашей проблемы, чтобы мы могли решить ее соответствующим образом? Может быть, вы можете дать ссылку на этот вопрос. - person yoninja; 08.02.2021
comment
просто объявите var items: [YourCustomObject]! в UsersViewController и назначьте его в doSomething(). Это не удается при назначении. Пробовал то же самое с String и другими системными классами - работает 50/50 - иногда непредсказуемо выходит из строя - person Vyachaslav Gerchicov; 08.02.2021
comment
@VyachaslavGerchicov, я обновил ответ. - person yoninja; 08.02.2021

Проблема в том, что вы создали сцену в раскадровке, но не предоставили представлению контроллера представления никаких подпредставлений или не подключили никаких выходов, поэтому интерфейс пуст.

Если вашей целью является повторное использование набора представлений и подпредставлений в связи с экземплярами нескольких разных классов контроллеров представлений, самый простой способ, если вы не хотите создавать их в коде, — поместить их в .xib. файл и загрузить его в коде после собственного процесса загрузки представления контроллера представления (например, в viewDidLoad).

Но если цель состоит в том, чтобы просто «настроить сегментированные элементы сегментированного управления» в разных экземплярах этого контроллера представления, самый простой подход состоит в том, чтобы иметь один класс контроллера представления и один соответствующий дизайн интерфейса и выполнять настройку в коде. Однако вы можете загрузить только этот сегментированный элемент управления из собственного .xib в каждом случае, если это важно для визуального оформления.

person matt    schedule 19.11.2015