Cómo usar uiviewcontroller de guión gráfico único para subclase múltiple

118

Digamos que tengo un guión gráfico que contiene UINavigationControllercomo controlador de vista inicial. Su controlador de vista raíz es una subclase de UITableViewController, que es BasicViewController. Tiene el IBActionque está conectado al botón de navegación derecho de la barra de navegación.

Desde allí, me gustaría usar el guión gráfico como plantilla para otras vistas sin tener que crear guiones gráficos adicionales. Digamos que estas vistas tendrán exactamente la misma interfaz pero con el controlador de vista raíz de la clase SpecificViewController1y SpecificViewController2cuáles son subclases de BasicViewController.
Esos 2 controladores de vista tendrían la misma funcionalidad e interfaz excepto por el IBActionmétodo.
Sería como lo siguiente:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

¿Puedo hacer algo así?
¿Puedo crear una instancia del guión gráfico de BasicViewControllerpero tengo un controlador de vista raíz en una subclase SpecificViewController1y SpecificViewController2?

Gracias.

verdy
fuente
3
Vale la pena señalar que puede hacer esto con nib. Pero si eres como yo y quieres algunas características agradables que solo tiene el guión gráfico (celda estática / prototipo, por ejemplo), supongo que no tenemos suerte.
Joseph Lin

Respuestas:

57

gran pregunta, pero desafortunadamente solo una respuesta pobre. No creo que actualmente sea posible hacer lo que propone porque no hay inicializadores en UIStoryboard que permitan anular el controlador de vista asociado con el guión gráfico como se define en los detalles del objeto en el guión gráfico en la inicialización. Es en la inicialización que todos los elementos de la IU en el tablero de almacenamiento están vinculados a sus propiedades en el controlador de vista.

De forma predeterminada, se inicializará con el controlador de vista que se especifica en la definición del guión gráfico.

Si está tratando de obtener la reutilización de los elementos de la interfaz de usuario que creó en el guión gráfico, aún deben estar vinculados o asociados a propiedades en las que el controlador de vista los esté usando para que puedan "decirle" al controlador de vista sobre los eventos.

No es gran cosa copiar sobre el diseño de un guión gráfico, especialmente si solo necesita un diseño similar para 3 vistas, sin embargo, si lo hace, debe asegurarse de que todas las asociaciones anteriores estén borradas, o se bloqueará cuando lo intente para comunicarse con el controlador de vista anterior. Podrá reconocerlos como mensajes de error KVO en la salida del registro.

Un par de enfoques que podría tomar:

  • almacene los elementos de la interfaz de usuario en un UIView, en un archivo xib, cree una instancia de su clase base y agréguelo como una subvista en la vista principal, generalmente self.view. Luego, simplemente usaría el diseño del guión gráfico con controladores de vista básicamente en blanco que ocupan su lugar en el guión gráfico pero con la subclase de controlador de vista correcta asignada a ellos. Como heredarían de la base, obtendrían esa vista.

  • cree el diseño en código e instálelo desde su controlador de vista base. Obviamente, este enfoque frustra el propósito de usar el guión gráfico, pero puede ser el camino a seguir en su caso. Si tiene otras partes de la aplicación que se beneficiarían del enfoque del guión gráfico, está bien desviarse aquí y allá si corresponde. En este caso, como arriba, solo usaría controladores de vista de banco con su subclase asignada y dejaría que el controlador de vista base instale la interfaz de usuario.

Sería bueno que Apple encontrara una forma de hacer lo que propones, pero el problema de tener los elementos gráficos preenlazados con la subclase del controlador seguiría siendo un problema.

que tengas un gran año nuevo !! cuidate

CocoaEv
fuente
Eso fue rápido. Como pensaba, no sería posible. Actualmente se me ocurre una solución teniendo solo esa clase BasicViewController y tengo una propiedad adicional para indicar en qué "clase" / "modo" actuará. Gracias de cualquier manera.
Verdy
2
Muy mal :( Supongo que tengo que copiar y pegar el mismo controlador de vista y cambiar su clase como solución alternativa.
Hlung
1
Y es por eso que no me gustan los
guiones gráficos
Me entristeció oírte decir eso. Estoy buscando una solución
Tony
2
Hay otro enfoque: especifique la lógica personalizada en diferentes delegados y en prepareForSegue, asigne el delegado correcto. De esta manera, crea 1 UIViewController + 1 UIViewController en el Storyboard pero tiene varias versiones de implementación.
plam4u
45

El código de línea que buscamos es:

object_setClass(AnyObject!, AnyClass!)

En Storyboard -> agregar UIViewController, asígnele un nombre de clase 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
    }
}
Jiří Zahálka
fuente
5
Gracias, simplemente funciona, por ejemplo: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
No estoy seguro de entender cómo se supone que funciona esto. ¿El padre está configurando su clase como la de un niño? ¿Cómo puedes tener varios hijos entonces?
user1366265
2
usted señor, ha hecho mi día
jere
2
Bien, chicos, déjenme explicarlo un poco más en detalle: ¿Qué queremos lograr? Queremos subclasificar nuestro ParentViewController para que podamos usar su Storyboard para más clases. Entonces, la línea mágica que lo hace todo está resaltada en mi solución y debe usarse en awakeFromNib en ParentVC. Lo que sucede entonces es que usa todos los métodos de ChildVC1 recién configurado, que se convierte en una subclase. ¿Si desea utilizarlo para más ChildVC? Simplemente haz tu lógica en awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Buena suerte.
Jiří Zahálka
10
¡Ten mucho cuidado al usar esto! Normalmente esto no debería usarse en absoluto ... Esto simplemente cambia el puntero isa del puntero dado y no reasigna la memoria para acomodar, por ejemplo, diferentes propiedades. Un indicador de esto es que el puntero a selfno cambia. Por lo tanto, la inspección del objeto (por ejemplo, leer _ivar / valores de propiedad) después object_setClasspuede causar bloqueos.
Patrik
15

Como dice la respuesta aceptada, no parece que sea posible hacerlo con guiones gráficos.

Mi solución es usar Nib's, al igual que los desarrolladores los usaban antes de los guiones gráficos. Si desea tener un controlador de vista subclase reutilizable (o incluso una vista), mi recomendación es usar Nibs.

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

Cuando conecta todas sus salidas al "Propietario del archivo" en el MyViewController.xibNO está especificando en qué clase se debe cargar el Nib, solo está especificando pares clave-valor: " esta vista debe estar conectada a este nombre de variable de instancia ". Al llamar [SubclassMyViewController alloc] initWithNibName:al proceso de inicialización, se especifica qué controlador de vista se utilizará para " controlar " la vista que creó en la plumilla.

kgaidis
fuente
Sorprendentemente, parece que esto es posible con los guiones gráficos, gracias a la biblioteca de tiempo de ejecución de ObjC. Verifique mi respuesta aquí: stackoverflow.com/a/57622836/7183675
Adam Tucholski
9

Es posible hacer que un guión gráfico cree una instancia de diferentes subclases de un controlador de vista personalizado, aunque implica una técnica ligeramente poco ortodoxa: anular el allocmétodo para el controlador de vista. Cuando se crea el controlador de vista personalizado, el método alloc anulado de hecho devuelve el resultado de la ejecución allocen la subclase.

Debo comenzar la respuesta con la condición de que, aunque lo probé en varios escenarios y no recibí errores, no puedo asegurar que se adapte a configuraciones más complejas (pero no veo ninguna razón por la que no debería funcionar) . Además, no he enviado ninguna aplicación con este método, por lo que existe la posibilidad de que sea rechazada por el proceso de revisión de Apple (aunque, nuevamente, no veo ninguna razón por la que debería hacerlo).

Para fines de demostración, tengo una subclase de UIViewControllercalled TestViewController, que tiene un UILabel IBOutlet y un IBAction. En mi guión gráfico, agregué un controlador de vista y modifiqué su clase TestViewControllery conecté IBOutlet a un UILabel y IBAction a un UIButton. Presento el TestViewController mediante un segue modal desencadenado por un UIButton en el viewController anterior.

Imagen del guión gráfico

Para controlar qué clase se instancia, he agregado una variable estática y métodos de clase asociados, así que obtenga / configure la subclase que se utilizará (supongo que se podrían adoptar otras formas de determinar qué subclase se instanciará):

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];
        }
    }
}

Para mi prueba tengo dos subclases de TestViewController: RedTestViewControllery GreenTestViewController. Cada una de las subclases tiene propiedades adicionales y cada una se reemplaza viewDidLoadpara cambiar el color de fondo de la vista y actualizar el texto de 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";
}

En algunas ocasiones podría querer crear TestViewControlleruna instancia de sí mismo, en otras ocasiones RedTestViewControllero GreenTestViewController. En el controlador de vista anterior, hago esto al azar de la siguiente manera:

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"];
}

Tenga en cuenta que el setClassForStoryBoardmétodo verifica para asegurarse de que el nombre de clase solicitado sea de hecho una subclase de TestViewController, para evitar confusiones. La referencia anterior a BlueTestViewControllerestá ahí para probar esta funcionalidad.

pbasdf
fuente
Hemos hecho algo similar en el proyecto, pero anulando el método alloc de UIViewController para obtener una subclase de una clase externa que recopila la información completa sobre todas las anulaciones. Funciona perfectamente.
Tim
Por cierto, este método puede dejar de funcionar tan rápido como Apple deja de llamar a alloc en los controladores de vista. Para el ejemplo, la clase NSManagedObject nunca recibe el método alloc. Creo que Apple podría copiar el código a otro método: quizás + allocManagedObject
Tim
7

intente esto, después de instantiateViewControllerWithIdentifier.

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

me gusta :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
莫 振华
fuente
Agregue una explicación útil sobre lo que hace su código.
codeforester
7
¿Qué sucede con esto, si usa variables de instancia de la subclase? Supongo que se bloquea, porque no hay suficiente memoria asignada para adaptarse a eso. En mis pruebas, he estado obteniendo EXC_BAD_ACCESS, así que no recomiendo esto.
Legoless
1
Esto no funcionará si agrega nuevas variables en la clase secundaria. Y el niño inittampoco se llamará. Tales limitaciones hacen que todo enfoque sea inutilizable.
Al Zonke
6

Basándome particularmente en las respuestas de nickgzzjr y Jiří Zahálka más el comentario debajo del segundo de CocoaBob, he preparado un método genérico corto que hace exactamente lo que OP necesita. Solo necesita verificar el nombre del guión gráfico y el ID del guión gráfico de 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
    }

Se agregan opcionales para evitar el desenvolvimiento forzado (advertencias de swiftlint), pero el método devuelve los objetos correctos.

Adam Tucholski
fuente
5

Aunque no es estrictamente una subclase, puede:

  1. option-arrastra el controlador de vista de clase base en el esquema del documento para hacer una copia
  2. Mueva la nueva copia del controlador de vista a un lugar separado en el guión gráfico
  3. Cambiar Clase al controlador de vista de subclase en el Inspector de identidad

Aquí hay un ejemplo de un tutorial de Bloc que escribí, subclasificado ViewControllercon WhiskeyViewController:

animación de los tres pasos anteriores

Esto le permite crear subclases de subclases de controlador de vista en el guión gráfico. A continuación, puede utilizar instantiateViewControllerWithIdentifier:para crear subclases específicas.

Este enfoque es un poco inflexible: las modificaciones posteriores dentro del guión gráfico al controlador de clase base no se propagan a la subclase. Si tiene muchas subclases, puede estar mejor con una de las otras soluciones, pero esto funcionará en un apuro.

Aaron Brager
fuente
11
Este no es un compañero de subclase, esto es solo duplicar un ViewController.
Ace Green
1
Eso no está bien. Se convierte en una subclase cuando cambia Clase a la subclase (paso 3). Luego, puede realizar los cambios que desee y conectarse a los puntos de venta / acciones de su subclase.
Aaron Brager
6
No creo que entiendas el concepto de subclases.
Ace Green
5
Si "las modificaciones posteriores dentro del guión gráfico al controlador de clase base no se propagan a la subclase", no se llama "subclase". Es copiar y pegar.
superarts.org
La clase subyacente, seleccionada en el Inspector de identidad, sigue siendo una subclase. El objeto que se inicializa y controla la lógica empresarial sigue siendo una subclase. Solo los datos de vista codificados, almacenados como XML en el archivo del guión gráfico e inicializados mediante initWithCoder:, no tienen una relación heredada. Este tipo de relación no es compatible con archivos de guiones gráficos.
Aaron Brager
4

El método Objc_setclass no crea una instancia de childvc. Pero mientras sale de childvc, se llama a deinit of childvc. Dado que no hay memoria asignada por separado para childvc, la aplicación se bloquea. Basecontroller tiene una instancia, mientras que child vc no tiene.

usuario8044830
fuente
2

Si no depende demasiado de los guiones gráficos, puede crear un archivo .xib separado para el controlador.

Establezca el propietario y las salidas del archivo apropiado en MainViewControllery anule init(nibName:bundle:)en el VC principal para que sus hijos puedan acceder al mismo Nib y sus salidas.

Tu código debería verse así:

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

Y su Child VC podrá reutilizar la punta de su padre:

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

Tomando respuestas de aquí y de allá, se me ocurrió esta excelente solución.

Cree un controlador de vista principal con esta función.

class ParentViewController: UIViewController {


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

        object_setClass(self, T.self)

    }

}

Esto permite al compilador asegurarse de que el controlador de vista secundario hereda del controlador de vista principal.

Luego, siempre que quiera pasar a este controlador usando una subclase, puede hacer:

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

}

Lo bueno es que puede agregar una referencia de guión gráfico a sí mismo y luego seguir llamando al controlador de vista secundario "siguiente".

nickgzzjr
fuente
1

Probablemente, la forma más flexible es utilizar vistas reutilizables.

(Cree una vista en un archivo XIB separado o Container viewagréguela a cada escena del controlador de vista de subclase en el guión gráfico)

DanSkeel
fuente
1
Por favor comente cuando vote en contra. Sé que no respondo directamente a esa pregunta, pero propongo una solución para el problema de raíz.
DanSkeel
1

Existe una solución simple, obvia y cotidiana.

Simplemente coloque el storyboard / controlador existente dentro del nuevo storyobard / controller. IE como vista de contenedor.

Este es el concepto exactamente análogo a "subclasificar", para controladores de vista.

Todo funciona exactamente como en una subclase.

Así como normalmente coloca una subvista de vista dentro de otra vista , naturalmente coloca un controlador de vista dentro de otro controlador de vista .

¿De qué otra manera podrías hacerlo?

Es una parte básica de iOS, tan simple como el concepto de "subvista".

Es así de fácil ...

/*

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

Ahora obviamente tienes listque hacer lo que quieras con

list.mode = .blah
list.tableview.reloadData()
list.heading = 'Search!'
list.searchBar.isHidden = false

etcétera etcétera.

Las vistas de contenedor son "como" subclases de la misma forma que las "subvistas" son "como" subclases.

Por supuesto, obviamente, no se puede "subcasar un diseño", ¿qué significa eso?

("Subclasificación" se relaciona con el software OO y no tiene conexión con los "diseños").

Obviamente, cuando desee reutilizar una vista, simplemente la subvista dentro de otra vista.

Cuando desee reutilizar un diseño de controlador, simplemente lo ve dentro de otro controlador.

¡Este es el mecanismo más básico de iOS!


Nota: durante años ha sido trivial cargar dinámicamente otro controlador de vista como una vista de contenedor. Explicado en la última sección: https://stackoverflow.com/a/23403979/294884

Nota: "_sb" es solo una macro obvia que usamos para guardar la escritura,

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
fuente
1

Gracias por la inspiradora respuesta de @ Jiří Zahálka, respondí mi solución hace 4 años aquí , pero @Sayka me sugirió publicarla como respuesta, así que aquí está.

En mis proyectos, normalmente, si estoy usando Storyboard para una subclase UIViewController, siempre preparo un método estático llamado instantiate()en esa subclase, para crear una instancia de Storyboard fácilmente. Entonces, para resolver la pregunta de OP, si queremos compartir el mismo Storyboard para diferentes subclases, podemos simplemente setClass()ir a esa instancia antes de devolverla.

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
fuente
0

El comentario de Cocoabob de la respuesta de Jiří Zahálka me ayudó a obtener esta solución y funcionó bien.

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
fuente