Как использовать одиночный раскадровщик uiviewcontroller для нескольких подклассов

Скажем, у меня есть раскадровка, содержащая UINavigationController в качестве начального контроллера представления. Его контроллер корневого представления является подклассом UITableViewController, который равен BasicViewController. Он имеет IBAction, который подключен к правой навигационной кнопке панели навигации


Оттуда я хотел бы использовать раскадровку в качестве шаблона для других представлений без необходимости создавать дополнительные раскадровки. Скажем, что эти представления будут иметь точно такой же интерфейс, но с контроллером корневого представления классов SpecificViewController1 и SpecificViewController2, которые являются подклассами BasicViewController.
Эти 2 диспетчера представлений будут иметь те же функциональные возможности и интерфейс, кроме метода IBAction.
Это будет выглядеть так:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Могу ли я сделать что-то подобное?
Могу ли я просто создать раскадровку BasicViewController, но у вас есть контроллер корневого представления для подкласса SpecificViewController1 и SpecificViewController2?


Спасибо.

Ответ 1

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

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

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

Это не так много, чтобы копировать над раскладкой, особенно если вам нужен только аналогичный дизайн для 3-х представлений, однако если вы это сделаете, вы должны убедиться, что все предыдущие ассоциации очищены или у вас появятся сбои когда он пытается связаться с предыдущим контроллером представления. Вы сможете распознать их как сообщения об ошибках KVO в выходном файле журнала.

Несколько подходов, которые вы могли бы предпринять:

  • сохраняйте элементы пользовательского интерфейса в UIView - в файле xib и создайте его из базового класса и добавьте его в виде суб-представления в главном представлении, обычно self.view. Затем вы просто используете раскладку раскладки, в основном пустые контроллеры представлений, занимающие свое место в раскадровке, но с назначенным им соответствующим классом контроллера. Поскольку они унаследовали бы от базы, они получили бы это представление.

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

Было бы неплохо, если бы Apple придумала способ сделать то, что вы предлагаете, но проблема с графическими элементами, предварительно связанными с подклассом контроллера, все равно будет проблемой.

У нас отличный Новый год! быть хорошо

Ответ 2

Код строки, который мы ищем, это:

object_setClass(AnyObject!, AnyClass!)

В Storyboard → добавить UIViewController, дать ему имя класса ParentVC.

class ParentVC: UIViewController {

    var type: Int?

    override func awakeFromNib() {

        if type = 0 {

            object_setClass(self, ChildVC1.self)
        }
        if type = 1 {

            object_setClass(self, ChildVC2.self)
        }  
    }

    override func viewDidLoad() {   }
}

class ChildVC1: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 0
    }
}

class ChildVC2: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 1
    }
}

Ответ 3

Как говорится в принятом ответе, похоже, что это невозможно сделать с раскадрой.

Мое решение - использовать Nib - точно так же, как разработчики использовали их перед раскадрой. Если вы хотите иметь многоразовый, подклассовый контроллер (или даже представление), я рекомендую использовать Nibs.

SubclassMyViewController *myViewController = [[SubclassMyViewController alloc] initWithNibName:@"MyViewController" bundle:nil]; 

Когда вы соединяете все свои выходы с "Владельцем файла" в MyViewController.xib, вы НЕ указываете, какой класс должен загружать Nib как, вы просто указываете пары ключ-значение: "этот вид должен быть связан с этим имя переменной экземпляра." При вызове [SubclassMyViewController alloc] initWithNibName: процесс инициализации указывает, какой контроллер будет использоваться для управления просмотром, созданным в банке.

Ответ 4

Возможно, что раскадровка создает экземпляры разных подклассов пользовательского контроллера представлений, хотя это включает в себя несколько неортодоксальный метод: переопределение метода alloc для контроллера вида. Когда создается пользовательский контроллер представления, метод переопределенного распределения фактически возвращает результат выполнения alloc в подклассе.

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

Для демонстрационных целей у меня есть подкласс UIViewController, называемый TestViewController, который имеет UILabel IBOutlet и IBAction. В моей раскадровке я добавил контроллер вида и внес свой вклад в класс TestViewController и подключил IBOutlet к UILabel и IBAction к UIButton. Я представляю TestViewController с помощью модального сеанса, запускаемого UIButton на предыдущем диспетчере viewController.

Storyboard image

Для управления созданным классом я добавил статическую переменную и связанные с ней методы класса, чтобы получить/установить подкласс, который будет использоваться (я думаю, можно было бы использовать другие способы определения того, какой подкласс должен быть создан):

TestViewController.m:

#import "TestViewController.h"

@interface TestViewController ()
@end

@implementation TestViewController

static NSString *_classForStoryboard;

+(NSString *)classForStoryboard {
    return [_classForStoryboard copy];
}

+(void)setClassForStoryBoard:(NSString *)classString {
    if ([NSClassFromString(classString) isSubclassOfClass:[self class]]) {
        _classForStoryboard = [classString copy];
    } else {
        NSLog(@"Warning: %@ is not a subclass of %@, reverting to base class", classString, NSStringFromClass([self class]));
        _classForStoryboard = nil;
    }
}

+(instancetype)alloc {
    if (_classForStoryboard == nil) {
        return [super alloc];
    } else {
        if (NSClassFromString(_classForStoryboard) != [self class]) {
            TestViewController *subclassedVC = [NSClassFromString(_classForStoryboard) alloc];
            return subclassedVC;
        } else {
            return [super alloc];
        }
    }
}

Для моего теста у меня есть два подкласса TestViewController: RedTestViewController и GreenTestViewController. Каждый из подклассов имеет дополнительные свойства и каждый переопределяет viewDidLoad, чтобы изменить цвет фона в представлении и обновить текст UILabel IBOutlet:

RedTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor redColor];
    self.testLabel.text = @"Set by RedTestVC";
}

GreenTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor greenColor];
    self.testLabel.text = @"Set by GreenTestVC";
}

В некоторых случаях я могу создать экземпляр TestViewController сам, в других случаях RedTestViewController или GreenTestViewController. В предыдущем контроллере представления я делаю это наугад следующим образом:

NSInteger vcIndex = arc4random_uniform(4);
if (vcIndex == 0) {
    NSLog(@"Chose TestVC");
    [TestViewController setClassForStoryBoard:@"TestViewController"];
} else if (vcIndex == 1) {
    NSLog(@"Chose RedVC");
    [TestViewController setClassForStoryBoard:@"RedTestViewController"];
} else if (vcIndex == 2) {
    NSLog(@"Chose BlueVC");
    [TestViewController setClassForStoryBoard:@"BlueTestViewController"];
} else {
    NSLog(@"Chose GreenVC");
    [TestViewController setClassForStoryBoard:@"GreenTestViewController"];
}

Обратите внимание, что метод setClassForStoryBoard проверяет, действительно ли запрошенное имя класса является подклассом TestViewController, чтобы избежать каких-либо путаниц. Ссылка на BlueTestViewController приведена здесь, чтобы проверить эту функциональность.

Ответ 5

попробуйте это, после instantiateViewControllerWithIdentifier.

- (void)setClass:(Class)c {
    object_setClass(self, c);
}

like:

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];

Ответ 6

Хотя это не строго подкласс, вы можете:

  • option -drag контроллер представления базового класса в структуре документа, чтобы сделать копию
  • Переместите копию нового контроллера режима просмотра в отдельное место на раскадровке.
  • Изменить класс на контроллер представления подкласса в инспекторе идентификации.

Вот пример из Bloc учебника, который я написал, подкласса ViewController с WhiskeyViewController:

animation of the above three steps

Это позволяет создавать подклассы подклассов диспетчера представлений в раскадровке. Затем вы можете использовать instantiateViewControllerWithIdentifier: для создания определенных подклассов.

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

Ответ 7

Метод objc_setclass не создает экземпляр childvc. Но при выходе из childvc, deinit of childvc звонит. Поскольку нет памяти, выделенной отдельно для childvc, происходит сбой приложений. У Basecontroller есть экземпляр, тогда как дочерний vc не имеет.

Ответ 8

Если вы не слишком зависимы от раскадровки, вы можете создать отдельный .xib файл для контроллера.

Установите соответствующий владелец файла и выходы в MainViewController и переопределите init(nibName:bundle:) в главном VC, чтобы его дети могли получить доступ к тому же Nib и его выходам.

Ваш код должен выглядеть так:

class MainViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: "MainViewController", bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .red
    }
}

И ваш ребенок VC сможет повторно использовать свой родительский нить:

class ChildViewController: MainViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .blue
    }
}

Ответ 9

Принимая ответы здесь и там, я придумал это аккуратное решение.

Создайте родительский контроллер представления с этой функцией.

class ParentViewController: UIViewController {


    func convert<T: ParentViewController>(to _: T.Type) {

        object_setClass(self, T.self)

    }

}

Это позволяет компилятору гарантировать, что дочерний контроллер представления наследуется от родительского контроллера представления.

Затем, когда вы захотите перейти на этот контроллер с помощью подкласса, вы можете сделать:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    if let parentViewController = segue.destination as? ParentViewController {
        ParentViewController.convert(to: ChildViewController.self)
    }

}

Крутая часть заключается в том, что вы можете добавить ссылку на раскадровку к себе, а затем продолжать вызывать "следующий" дочерний контроллер представления.

Ответ 10

Основываясь, в частности, на nickgzzjr и Jiří Zahálka, ответах и комментариях под вторым от CocoaBob, я подготовил короткий обобщенный метод, выполняющий именно то, что нужно OP. Вам нужно только проверить имя раскадровки и идентификатор раскадровки View Controllers

class func instantiate<T: BasicViewController>(as _: T.Type) -> T? {
        let storyboard = UIStoryboard(name: "StoryboardName", bundle: nil)
        guard let instance = storyboard.instantiateViewController(withIdentifier: "Identifier") as? BasicViewController else {
            return nil
        }
        object_setClass(instance, T.self)
        return instance as? T
    }

Добавляются необязательные параметры, чтобы избежать принудительного развертывания (предупреждения swiftlint), но метод возвращает правильные объекты.

Ответ 11

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

(Создайте представление в отдельном файле XIB или Container view и добавьте его в каждую сцену контроллера подкласса в раскадровке)

Ответ 12

Существует простое, очевидное, повседневное решение.

Просто поместите существующую раскадровку/контроллер в новую раскадровку/контроллер. И.Е. в виде контейнера.

Это в точности аналогичная концепция "подкласса" для контроллеров представления.

Все работает точно так же, как в подклассе.

Так же, как вы обычно помещаете подпредставление представления в другое представление, естественно, вы обычно помещаете контроллер представления в другой контроллер представления.

Как еще можно это сделать?

Это базовая часть iOS, столь же простая, как и понятие "подпредставление".

Это так просто...

/*

Search screen is just a modification of our List screen.

*/

import UIKit

class Search: UIViewController {

    var list: List!

    override func viewDidLoad() {
        super.viewDidLoad()

        list = (_sb("List") as! List
        addChild(list)
        view.addSubview(list.view)
        list.view.bindEdgesToSuperview()
        list.didMove(toParent: self)
    }
}

Теперь у вас, очевидно, есть list, чтобы делать с

что угодно
list.mode = .blah
list.tableview.reloadData()
list.heading = 'Search!'
list.searchBar.isHidden = false

и т.д.

Контейнерные представления "просто похожи" на подклассы так же, как "подвиды" похожи на "подклассы".

Разумеется, вы не можете "подклассить макет" - что бы это вообще значило?

("Подклассы" относятся к программному обеспечению ОО и не связаны с "макетами".)

Очевидно, что если вы хотите повторно использовать представление, просто подпишите его в другом представлении.

Если вы хотите повторно использовать макет контроллера, вы просто просматриваете контейнер внутри другого контроллера.

Это как самый основной механизм iOS !!


Примечание. В течение многих лет было тривиально динамически загружать другой контроллер представления как представление контейнера. Объяснено в последнем разделе: fooobar.com/info/3297/...

Примечание. _Sb - это просто очевидный макрос, который мы используем для сохранения ввода,

func _sb(_ s: String)->UIViewController {
    // by convention, for a screen "SomeScreen.storyboard" the
    // storyboardID must be SomeScreenID
    return UIStoryboard(name: s, bundle: nil)
       .instantiateViewController(withIdentifier: s + "ID")
}