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

118

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

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

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

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

Спасибо.

Verdy
источник
3
Возможно, стоит отметить, что вы можете сделать это с помощью пера. Но если вы, как и я, хотите иметь некоторые приятные функции, которые есть только в раскадровке (например, статическая / прототипная ячейка), то, думаю, нам не повезло.
Джозеф Лин

Ответы:

57

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

По умолчанию он инициализируется с помощью контроллера представления, указанного в определении раскадровки.

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

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

Вот несколько возможных подходов:

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

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

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

удачного Нового года !! всего хорошего

CocoaEv
источник
Это было быстро. Как я и думал, это невозможно. В настоящее время я придумываю решение, имея только этот класс BasicViewController и дополнительное свойство, указывающее, в каком «классе» / «режиме» он будет действовать. Спасибо, в любом случае.
Verdy 01
2
очень плохо :( Думаю, мне нужно скопировать и вставить тот же контроллер представления и изменить его класс в качестве обходного пути.
Hlung
1
И вот почему мне не нравятся Раскадровки ... почему-то они действительно не работают, когда вы делаете немного больше, чем стандартные представления ...
TheEye
Так грустно слышать, что ты это сказал. Я ищу решение
Тони
2
Есть другой подход: укажите настраиваемую логику в разных делегатах и ​​в prepareForSegue назначьте правильный делегат. Таким образом, вы создаете 1 UIViewController + 1 UIViewController в раскадровке, но у вас есть несколько версий реализации.
plam4u
45

Код строки, которую мы ищем:

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
    }
}
Иржи Захалка
источник
5
Спасибо, просто работает, например:class func instantiate() -> SubClass { let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)! object_setClass(instance, SubClass.self) return (instance as? SubClass)! }
CocoaBob
3
Я не уверен, что понимаю, как это должно работать. Родитель устанавливает свой класс как дочерний? Как же тогда можно иметь несколько детей ?!
user1366265
2
вы, сэр, сделали мой день
Jere
2
Хорошо, ребята, позвольте мне объяснить это немного подробнее: чего мы хотим достичь? Мы хотим создать подкласс нашего ParentViewController, чтобы мы могли использовать его Storyboard для большего количества классов. Итак, волшебная строка, которая делает все это, выделена в моем решении и должна использоваться в awakeFromNib в ParentVC. Затем происходит то, что он использует все методы из вновь установленного ChildVC1, который становится подклассом. Если вы хотите использовать его для большего количества ChildVC? Просто выполните свою логику в awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Удачи.
Jiří Zahálka
10
Будьте очень осторожны при использовании! Обычно это вообще не должно использоваться ... Это просто изменяет указатель isa данного указателя и не перераспределяет память для размещения, например, различных свойств. Одним из индикаторов этого является то, что указатель на selfне меняется. Таким образом, проверка объекта (например, чтение значений _ivar / property) после object_setClassможет вызвать сбои.
Патрик
15

Как говорится в принятом ответе, это не похоже на раскадровку.

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

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

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

kgaidis
источник
Удивительно, но это возможно с раскадровкой благодаря библиотеке времени выполнения ObjC. Отметьте мой ответ здесь: stackoverflow.com/a/57622836/7183675
Адам Тухольски
9

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

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

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

Раскадровка изображения

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

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предназначена для тестирования этой функциональности.

pbasdf
источник
Мы сделали нечто подобное в проекте, но переопределили метод alloc в UIViewController, чтобы получить подкласс от внешнего класса, собирающего полную информацию обо всех переопределениях. Прекрасно работает.
Тим
Кстати, этот метод может перестать работать так быстро, как Apple перестает вызывать alloc на контроллерах представления. Например, класс NSManagedObject никогда не получает метод выделения. Я думаю, что Apple могла бы скопировать код в другой метод: возможно, + allocManagedObject
Тим
7

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

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

лайк :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
莫 振华
источник
Пожалуйста, добавьте полезное объяснение того, что делает ваш код.
codeforester
7
Что с этим произойдет, если вы используете переменные экземпляра из подкласса? Я предполагаю сбой, потому что для этого не хватает памяти. В моих тестах я получал EXC_BAD_ACCESS, поэтому не рекомендую это делать.
Legoless 05
1
Это не сработает, если вы добавите новые переменные в дочерний класс. И детской initтоже не назовут. Такие ограничения делают непригодным для использования любой подход.
Аль Зонке
6

Основываясь, в частности, на ответах 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), но метод возвращает правильные объекты.

Адам Тухольски
источник
5

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

  1. option-потяните контроллер представления базового класса в структуре документа, чтобы сделать копию
  2. Переместите новую копию контроллера представления в отдельное место на раскадровке
  3. Измените Class на контроллер представления подкласса в Identity Inspector

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

анимация трех вышеуказанных шагов

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

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

Аарон Брагер
источник
11
Это не помощник по подклассу, это просто дублирование ViewController.
Ace Green
1
Это не правильно. Он становится подклассом, когда вы меняете Class на подкласс (шаг 3). Затем вы можете внести любые изменения, которые захотите, и подключиться к выходам / действиям в своем подклассе.
Аарон Брагер
6
Я не думаю, что вы уловили концепцию подклассов.
Ace Green
5
Если «более поздние модификации в раскадровке контроллера базового класса не распространяются на подкласс», это не называется «подклассом». Это копирование и вставка.
superarts.org
Базовый класс, выбранный в Identity Inspector, по-прежнему является подклассом. Инициализируемый объект, управляющий бизнес-логикой, по-прежнему является подклассом. Только закодированные данные представления, сохраненные как XML в файле раскадровки и инициализированные с помощью initWithCoder:, не имеют унаследованных отношений. Этот тип отношений не поддерживается файлами раскадровки.
Aaron Brager
4

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

user8044830
источник
2

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

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

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

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
    }
}
азотистый
источник
2

Взяв ответы оттуда и там, я придумал это изящное решение.

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

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

}

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

nickgzzjr
источник
1

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

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

DanSkeel
источник
1
Прокомментируйте, когда вы голосуете против. Я знаю, что не отвечаю прямо на этот вопрос, но предлагаю решение основной проблемы.
DanSkeel
1

Есть простое, очевидное повседневное решение.

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

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

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

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

Как еще ты мог это сделать?

Это базовая часть iOS, такая же простая, как понятие «subview».

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

/*

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


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

Примечание. "_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")
}
Fattie
источник
1

Спасибо за вдохновляющий ответ @ Jiří Zahálka, я ответил на свое решение 4 года назад здесь , но @Sayka предложил мне опубликовать его в качестве ответа, так что вот оно.

Обычно в своих проектах, если я использую Storyboard для подкласса UIViewController, я всегда подготавливаю статический метод, вызываемый instantiate()в этом подклассе, чтобы легко создать экземпляр из Storyboard. Итак, для решения вопроса OP, если мы хотим использовать одну и ту же раскадровку для разных подклассов, мы можем просто использовать setClass()этот экземпляр, прежде чем возвращать его.

class func instantiate() -> SubClass {
    let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)!
    object_setClass(instance, SubClass.self)
    return (instance as? SubClass)!
}
CocoaBob
источник
0

Комментарий Cocoabob от ответа Йиржи Захальки помог мне получить это решение, и оно сработало.

func openChildA() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil);
    let parentController = storyboard
        .instantiateViewController(withIdentifier: "ParentStoryboardID") 
        as! ParentClass;
    object_setClass(parentController, ChildA.self)
    self.present(parentController, animated: true, completion: nil);
}
Sayka
источник