Как использовать одиночный раскадровщик 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")
}