Як використовувати uiviewcontroller для одиночної розгортки для декількох підкласів


118

Скажімо, у мене є дошка розкладок, яка містить UINavigationControllerяк початковий контролер перегляду. Його контролером кореневого виду є підклас UITableViewController, який є BasicViewController. Він має IBActionпідключений до правої кнопки навігації на панелі навігації.

Звідти я хотів би використати рекламну дошку як шаблон для інших подань, не створюючи додаткових дощок. Скажімо , ці уявлення матимуть такий самий інтерфейс , але з видом на кореневої контролер класу SpecificViewController1і SpecificViewController2які є підкласами BasicViewController.
Ці 2 контролери перегляду мали б однаковий функціонал та інтерфейс, за винятком IBActionметоду.
Це було б так:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Чи можу я зробити щось подібне?
Чи можу я просто BasicViewControllerстворити загальну дошку розкадровки, але мати контролер кореневого виду для підкласу SpecificViewController1та SpecificViewController2?

Дякую.


3
Можливо, варто зазначити, що ви можете це зробити за допомогою ручки. Але якщо ви схожі на мене, які хочуть приємних функцій, які має лише розкадровка (наприклад, стійка / прототип комірки), то, мабуть, нам не щастить.
Джозеф Лін

Відповіді:


57

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

Він за замовчуванням буде ініціалізований з контролером перегляду, який вказаний у визначенні дошки розкадрування.

Якщо ви намагаєтесь отримати повторно використання елементів інтерфейсу, створених на аркуші розкадри, вони все одно повинні бути пов’язані або пов'язані з властивостями, в яких коли-небудь контролер перегляду використовує їх для того, щоб мати можливість "повідомити" контролер перегляду про події.

Це не так вже й велика копія над макетом розкадровки, особливо якщо вам потрібен лише аналогічний дизайн на 3 перегляди, однак, якщо ви це зробите, ви повинні переконатися, що всі попередні асоціації очищені, інакше при спробі він може отримати збої. для зв'язку з попереднім контролером перегляду. Ви зможете розпізнати їх як повідомлення про помилки KVO у висновку журналу.

Кілька підходів, якими ви можете скористатися:

  • зберігайте елементи інтерфейсу у UIView - у файлі xib та інстанціюйте його із базового класу та додайте його як підвід в основний вигляд, як правило, self.view. Тоді ви б просто використали макет розкладної плати з в основному порожніми контролерами подання, які займають своє місце на дошці, але з призначеним їм підкласом контролера правильного перегляду. Оскільки вони успадкують від бази, вони отримають такий погляд.

  • створити макет у коді та встановити його з базового контролера перегляду. Очевидно, що цей підхід перемагає мету використання розкадровки, але у вашому випадку може бути шлях. Якщо у вас є інші частини програми, які могли б скористатися підходом до розкадрівки, нормально відхилятися тут і там, якщо це доречно. У цьому випадку, як і вище, ви просто використовуєте контролери подання даних з присвоєним підкласом і дозволяєте контролеру базового виду встановити інтерфейс користувача.

Було б добре, якби Apple придумала спосіб зробити те, що ви пропонуєте, але питання щодо попереднього зв’язання графічних елементів із підкласом контролера все ще залишатиметься проблемою.

чудовий Новий рік !! добре


Це було швидко. Як я думав, це було б неможливо. В даний час я придумав рішення, маючи саме цей клас BasicViewController і маю додаткове властивість вказувати, для якого "класу" / "режиму" він буде діяти. Все одно, дякую.
verdy

2
занадто погано :( Гадаю, що мені доведеться скопіювати та вставити той же контролер перегляду та змінити його клас як спосіб вирішення.
Hlung

1
І саме тому мені не подобаються
Дошки розмов

Так сумно, почувши, що ти це сказав. Я шукаю рішення
Тоні

2
Існує ще один підхід: вкажіть власну логіку у різних делегатів та підготуйтеForSegue, призначте правильного делегата. Таким чином, ви створюєте 1 UIViewController + 1 UIViewController на Дошці розкадрувань, але у вас є кілька версій реалізації.
plam4u

45

Код рядка, який ми шукаємо:

object_setClass(AnyObject!, AnyClass!)

У розгортці -> додайте 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
Я не впевнений, що розумію, як це має працювати. Батько встановлює свій клас як клас дитини? Як тоді у вас може бути кілька дітей ?!
користувач1366265

2
ви, сер, зробили мій день
1616

2
Добре, хлопці, тож дозвольте мені трохи детальніше пояснити це: чого ми хочемо досягти? Ми хочемо підкласирувати наш ParentViewController, щоб ми могли використовувати його дошку розкадрувань для інших класів. Тож магічна лінія, яка робить все це, висвітлена в моєму рішенні і повинна бути використана в wakefromNib в ParentVC. Потім відбувається те, що він використовує всі методи з нещодавно встановленого ChildVC1, який стає як підклас. Якщо ви хочете використовувати його для більше ChildVC? Просто виконайте свою логіку в awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Удачі.
Іржі Загалка

10
Будьте дуже обережні, використовуючи це! Зазвичай це взагалі не слід використовувати ... Це просто змінює вказівник isa даного вказівника і не перерозподіляє пам'ять для розміщення, наприклад, для різних властивостей. Одним з показників цього є те, що вказівник selfне змінюється. Тож огляд об’єкта (наприклад, зчитування значень _ivar / властивості) після object_setClassможе призвести до збоїв.
Патрік

15

Як зазначається у прийнятій відповіді, це виглядає не так, як це можливо зробити з раскадровками.

Моє рішення - використовувати Nib - подібно до того, як розробники використовували їх до розкладок. Якщо ви хочете мати контролер перегляду, що підлягає багаторазовому використанню, підкласифікований (або навіть перегляд), моя рекомендація - використовувати Nibs.

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

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


Дивно здається, що це можливо за допомогою розповідей, завдяки бібліотеці виконання ObjC. Перевірте мою відповідь тут: stackoverflow.com/a/57622836/7183675
Адам Тухольський

9

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

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

Для демонстраційних цілей, у мене є підклас UIViewControllerпід назвою 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для зміни кольору фону подання та оновлення тексту IBOutlet UILabel:

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це є для перевірки цієї функціональності.


Ми зробили щось подібне в проекті, але переосмисливши метод allo UIViewController, щоб отримати підклас із зовнішнього класу, збираючи повну інформацію про всі зміни. Працює чудово.
Тім

До речі, цей метод може перестати працювати так само швидко, як Apple припиняє виклик alloc на контролери перегляду. Для прикладу, клас NSManagedObject ніколи не отримує метод alloc. Я думаю, що 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, тому не рекомендував цього.
Безголосий

1
Це не спрацює, якщо ви додасте нові змінні у дочірньому класі. І дитину initтеж не назвеш. Такі обмеження роблять весь підхід непридатним.
Аль-Зонке

6

Опираючись, зокрема, на відповіді nickgzzjr та Іржі Загалка плюс коментар під другим від CocoaBob, я підготував короткий загальний метод, що робить саме те, що потрібно ОП. Потрібно лише перевірити назву табло та переглянути ідентифікатор розкадрівки 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
    }

Додаткові опції додаються, щоб уникнути розгортання сили (швидкі попередження), але метод повертає правильні об'єкти.


5

Хоча це не строго підклас, ви можете:

  1. option-витягніть контролер перегляду базового класу в «Контур документа», щоб зробити копію
  2. Перенесіть нову копію контролера перегляду в окреме місце на дошці повідомлень
  3. Змінити клас на контролер подання підкласу в інспекторі ідентичності

Ось приклад з блоку підручника я написав, підкласи ViewControllerз WhiskeyViewController:

анімація вищезазначених трьох кроків

Це дозволяє створювати підкласи підкласів контролерів перегляду в режимі розкадровки. Потім можна використовувати instantiateViewControllerWithIdentifier:для створення конкретних підкласів.

Такий підхід є дещо негнучким: пізніші модифікації в раскадровці до контролера базового класу не поширюються на підклас. Якщо у вас багато підкласів, можливо, вам буде краще з одним з інших рішень, але це буде зроблено вкрай дрібно.


11
Це не товариш підкласу, це лише копіювання ViewController.
Туз Зелений

1
Це не правильно. Він стає підкласом при зміні класу на підклас (крок 3). Тоді ви можете вносити будь-які зміни, які хочете, і підключити до торгових точок / дій у своєму підкласі.
Аарон Брагер

6
Я не думаю, що ви зрозуміли поняття підкласифікації.
Туз Зелений

5
Якщо "пізніші модифікації в раскадровці до контролера базового класу не поширюються на підклас", це не називається "підкласифікація". Це копіювати та вставляти.
superarts.org

Основний клас, обраний інспектором ідентичності, все ще є підкласом. Об'єкт, який ініціалізується та керує бізнес-логікою, все ще є підкласом. Тільки кодовані дані перегляду, що зберігаються як XML у файлі розкадрівки та ініціалізовані через initWithCoder:, не мають успадкованих відносин. Цей тип відносин не підтримується файлами розповідей.
Аарон Брагер

4

Метод Objc_setclass не створює примірник childvc. Але поки вискакує з childvc, денітування Childvc викликає дзвінок. Оскільки для childvc немає пам’яті, виділеної окремо, програма виходить з ладу. У Basecontroller є екземпляр, тоді як доменний відеоролик не має.


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

}

Прикольна частина полягає в тому, що ви можете додати посилання на дошку розкадрування на себе, а потім продовжувати дзвонити до "наступного" контролера дитячого перегляду.


1

Напевно, найбільш гнучким способом є використання видів повторного використання.

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


1
Будь ласка, коментуйте, коли ви подаєте заявку. Я знаю, що на це питання я не відповідаю безпосередньо, але пропоную вирішити основну проблему.
DanSkeel

1

Є просте, очевидне, повсякденне рішення.

Просто помістіть існуючу раскадровку / контролер всередині нової сюжетної карти / контролера. IE як перегляд контейнера.

Це точно аналогічна концепція "підкласифікації" для, контролерів перегляду.

Все працює точно так само, як у підкласі.

Подібно до того, як ви зазвичай ставите підрозгляд перегляду всередині іншого виду , природно, ви зазвичай ставите контролер перегляду всередині іншого контролера перегляду .

Як ще ти міг це зробити?

Це основна частина 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

тощо.

Перегляди контейнерів є «подібно до» підкласингу так само, як «підперегляди» є «подібно до» підкласифікації.

Звичайно, очевидно, що ви не можете "підкіркувати макет" - що це може означати?

("Підкласифікація" стосується програмного забезпечення OO і не має зв'язку з "макетами".)

Очевидно, що коли ви хочете повторно використовувати подання, ви просто перегляньте його всередині іншого перегляду.

Коли ви хочете повторно використовувати макет контролера, ви просто переглядаєте його в іншому контролері.

Це як найосновніший механізм 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")
}

1

Дякую за натхненну відповідь @ Jiří Zahálka, я відповів на своє рішення 4 роки тому тут , але @Sayka запропонував мені опублікувати це як відповідь, так ось воно.

У своїх проектах, як правило, якщо я використовую Storyboard для підкласу UIViewController, я завжди готую статичний метод, який називається instantiate()в цьому підкласі, щоб легко створити екземпляр із Storyboard. Отже, для вирішення питання ОП, якщо ми хочемо поділити одну і ту ж дошку розкадрів для різних підкласів, ми можемо просто 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)!
}

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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.