Como usar um único storyboard uiviewcontroller para várias subclasses

118

Digamos que eu tenha um storyboard que contém UINavigationControllerum controlador de visualização inicial. Seu controlador de visualização raiz é uma subclasse de UITableViewController, que é BasicViewController. Ele IBActionestá conectado ao botão de navegação direito da barra de navegação.

A partir daí, gostaria de usar o storyboard como um modelo para outras visualizações sem ter que criar storyboards adicionais. Digamos que essas visualizações tenham exatamente a mesma interface, mas com o controlador de visualização raiz da classe SpecificViewController1e SpecificViewController2quais são as subclasses de BasicViewController.
Esses 2 controladores de visualização teriam a mesma funcionalidade e interface, exceto para o IBActionmétodo.
Seria assim:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Posso fazer algo assim?
Posso apenas instanciar o storyboard de, BasicViewControllermas ter o controlador de visualização raiz para a subclasse SpecificViewController1e SpecificViewController2?

Obrigado.

verdy
fonte
3
Vale ressaltar que você pode fazer isso com a ponta. Mas se você é como eu, que quer alguns recursos legais que só o storyboard tem (célula estática / protótipo, por exemplo), então acho que estamos sem sorte.
Joseph Lin

Respostas:

57

ótima pergunta - mas, infelizmente, apenas uma resposta fraca. Não acredito que atualmente seja possível fazer o que você propõe, porque não há inicializadores no UIStoryboard que permitem substituir o controlador de visualização associado ao storyboard conforme definido nos detalhes do objeto no storyboard na inicialização. É na inicialização que todos os elementos da IU no stoaryboard são vinculados às suas propriedades no controlador de visualização.

Ele será inicializado por padrão com o controlador de visualização que é especificado na definição do storyboard.

Se você está tentando obter a reutilização de elementos de interface do usuário que você criou no storyboard, eles ainda devem ser vinculados ou associados às propriedades em que o controlador de visualização os esteja usando para que possam "informar" o controlador de visualização sobre os eventos.

Copiar sobre um layout de storyboard não é um grande problema, especialmente se você só precisa de um design semelhante para 3 visualizações, no entanto, se precisar, deve certificar-se de que todas as associações anteriores foram apagadas, ou ocorrerá falhas ao tentar para se comunicar com o controlador de visualização anterior. Você será capaz de reconhecê-los como mensagens de erro KVO na saída do log.

Algumas abordagens que você pode adotar:

  • armazene os elementos da interface do usuário em um UIView - em um arquivo xib e instancie-o de sua classe base e adicione-o como uma subvisão na visão principal, normalmente self.view. Então, você simplesmente usaria o layout de storyboard com controladores de visualização basicamente em branco mantendo seu lugar no storyboard, mas com a subclasse de controlador de visualização correta atribuída a eles. Como eles herdariam da base, eles obteriam essa visão.

  • crie o layout no código e instale-o a partir de seu controlador de visualização de base. Obviamente, essa abordagem anula o propósito de usar o storyboard, mas pode ser o caminho a percorrer no seu caso. Se você tiver outras partes do aplicativo que se beneficiariam da abordagem do storyboard, não há problema em desviar aqui e ali, se apropriado. Neste caso, como acima, você apenas usaria controladores de visualização de banco com sua subclasse atribuída e deixaria o controlador de visualização de base instalar a IU.

Seria bom se a Apple descobrisse uma maneira de fazer o que você propõe, mas o problema de ter os elementos gráficos pré-vinculados à subclasse do controlador ainda seria um problema.

tenha um ótimo ano novo !! fique bem

CocoaEv
fonte
Aquilo foi rápido. Como pensei, não seria possível. Atualmente eu venho com uma solução tendo apenas aquela classe BasicViewController e tenho propriedades adicionais para indicar em qual "classe" / "modo" ela estará agindo. Obrigado mesmo assim.
verdy
2
muito ruim :( Acho que tenho que copiar e colar o mesmo controlador de visualização e alterar sua classe como uma solução alternativa.
Hlung
1
E é por isso que não gosto de Storyboards ... de alguma forma, eles não funcionam realmente uma vez que você faz um pouco mais do que visualizações padrão ...
TheEye
Tão triste ao ouvir você dizer isso. Estou procurando uma solução
Tony
2
Há outra abordagem: Especifique a lógica personalizada em diferentes delegados e em prepareForSegue, atribua o delegado correto. Dessa forma, você cria 1 UIViewController + 1 UIViewController no Storyboard, mas tem várias versões de implementação.
plam4u
45

O código de linha que procuramos é:

object_setClass(AnyObject!, AnyClass!)

Em Storyboard -> adicionar UIViewController, dê a ele um nome de classe 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
fonte
5
Obrigado, ele simplesmente funciona, por exemplo: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 de
3
Não tenho certeza se entendo como isso deve funcionar. O pai está definindo sua classe como a de um filho? Como você pode ter vários filhos então ?!
user1366265
2
senhor, fez o meu dia
jere
2
OK pessoal, deixe-me explicar um pouco mais detalhadamente: O que queremos alcançar? Queremos criar uma subclasse de nosso ParentViewController para que possamos usar seu Storyboard para mais classes. Portanto, a linha mágica que faz tudo está destacada na minha solução e deve ser usada em awakeFromNib em ParentVC. O que acontece então é que ele usa todos os métodos de ChildVC1 recém-definidos que se tornam uma subclasse. Quer usá-lo para mais ChildVCs? Simplesmente faça sua lógica em awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Boa sorte.
Jiří Zahálka
10
Tenha muito cuidado ao usar isso! Normalmente, isso não deve ser usado ... Isso simplesmente muda o isa ponteiro do ponteiro fornecido e não realoca a memória para acomodar, por exemplo, propriedades diferentes. Um indicador para isso é que o ponteiro para selfnão muda. Portanto, a inspeção do objeto (por exemplo, leitura de _ivar / valores de propriedade) depois object_setClasspode causar travamentos.
Patrik
15

Como afirma a resposta aceita, não parece que seja possível fazer com storyboards.

Minha solução é usar o Nib's - assim como os desenvolvedores os usavam antes dos storyboards. Se você deseja ter um controlador de visualização reutilizável e subclassível (ou mesmo uma visualização), minha recomendação é usar o Nibs.

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

Quando você conecta todas as suas saídas ao "Proprietário do Arquivo" no, MyViewController.xibvocê NÃO está especificando em qual classe o Nib deve ser carregado, você está apenas especificando pares de valores-chave: " esta visão deve ser conectada a este nome de variável de instância ." Ao chamar [SubclassMyViewController alloc] initWithNibName:o processo de inicialização, especifique qual controlador de visualização será usado para " controlar " a visualização que você criou na ponta.

kgaidis
fonte
Surpreendentemente, parece que isso é possível com storyboards, graças à biblioteca de tempo de execução ObjC. Verifique minha resposta aqui: stackoverflow.com/a/57622836/7183675
Adam Tucholski
9

É possível fazer com que um storyboard instancie diferentes subclasses de um controlador de visualização customizado, embora envolva uma técnica um pouco heterodoxa: substituir o allocmétodo para o controlador de visualização. Quando o controlador de visualização customizado é criado, o método de alocação sobrescrito retorna de fato o resultado da execução allocna subclasse.

Devo começar a resposta com a ressalva de que, embora eu tenha testado em vários cenários e não tenha recebido erros, não posso garantir que ele irá lidar com configurações mais complexas (mas não vejo razão para que não funcione) . Além disso, não enviei nenhum aplicativo usando esse método, portanto, há uma chance remota de que ele seja rejeitado pelo processo de revisão da Apple (embora, novamente, não vejo razão para isso).

Para fins de demonstração, tenho uma subclasse de UIViewControllerchamado TestViewController, que tem um UILabel IBOutlet e um IBAction. Em meu storyboard, adicionei um controlador de visualização e TestViewControlleralterei sua classe para , e conectei o IBOutlet a um UILabel e o IBAction a um UIButton. Apresento o TestViewController por meio de uma segue modal acionada por um UIButton no viewController anterior.

Imagem do storyboard

Para controlar qual classe é instanciada, adicionei uma variável estática e métodos de classe associados para obter / definir a subclasse a ser usada (eu acho que pode-se adotar outras maneiras de determinar qual subclasse deve ser instanciada):

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 meu teste, tenho duas subclasses de TestViewController: RedTestViewControllere GreenTestViewController. Cada uma das subclasses tem propriedades adicionais e cada substituição viewDidLoadpara alterar a cor de fundo da visualização e atualizar o texto do 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";
}

Em algumas ocasiões, posso querer instanciar- TestViewControllerse, em outras ocasiões RedTestViewControllerou GreenTestViewController. No controlador de visualização anterior, faço isso aleatoriamente da seguinte maneira:

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

Observe que o setClassForStoryBoardmétodo verifica se o nome da classe solicitado é de fato uma subclasse de TestViewController, para evitar confusões. A referência acima BlueTestViewControllerpara testar esta funcionalidade.

pbasdf
fonte
Fizemos algo semelhante no projeto, mas substituindo o método de alocação do UIViewController para obter uma subclasse de uma classe externa reunindo todas as informações sobre todas as substituições. Funciona perfeitamente.
Tim,
A propósito, esse método pode parar de funcionar assim que a Apple parar de chamar aloc nos controladores de exibição. Para o exemplo, a classe NSManagedObject nunca recebe o método de alocação. Acho que a Apple poderia copiar o código para outro método: Maybe + allocManagedObject
Tim
7

tente isso, após instantiateViewControllerWithIdentifier.

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

gostar :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
莫 振华
fonte
Adicione alguma explicação útil sobre o que o seu código faz.
codeforester
7
O que acontece com isso, se você usar variáveis ​​de instância da subclasse? Estou supondo que falhou, porque não há memória suficiente alocada para isso. Em meus testes, tenho conseguido EXC_BAD_ACCESS, então não recomendo isso.
Legoless
1
Isso não funcionará se você adicionar novas variáveis ​​na classe filha. E a criança inittambém não será chamada. Essas restrições tornam todas as abordagens inutilizáveis.
Al Zonke
6

Baseando-se particularmente em nickgzzjr e Jiří Zahálka, além de comentários no segundo do CocoaBob, preparei um método genérico curto fazendo exatamente o que o OP precisa. Você só precisa verificar o nome do storyboard e Exibir ID do storyboard dos controladores

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
    }

Opcionais são adicionados para evitar forçar o desempacotamento (avisos do swiftlint), mas o método retorna os objetos corretos.

Adam Tucholski
fonte
5

Embora não seja estritamente uma subclasse, você pode:

  1. option- arraste o controlador de visão da classe base no Document Outline para fazer uma cópia
  2. Mova a nova cópia do controlador de visualização para um local separado no storyboard
  3. Altere a classe para o controlador de visualização de subclasse no Identity Inspector

Aqui está um exemplo de um tutorial do Bloc que escrevi, criando ViewControlleruma subclasse com WhiskeyViewController:

animação das três etapas acima

Isso permite que você crie subclasses de subclasses de view controller no storyboard. Você pode então usarinstantiateViewControllerWithIdentifier: para criar subclasses específicas.

Essa abordagem é um pouco inflexível: as modificações posteriores no storyboard para o controlador da classe base não se propagam para a subclasse. Se você tem muitas subclasses, pode ficar melhor com uma das outras soluções, mas isso servirá em uma pitada.

Aaron Brager
fonte
11
Este não é um companheiro de subclasse, está apenas duplicando um ViewController.
Ace Green
1
Isso não está certo. Torna-se uma subclasse quando você altera Class para a subclasse (etapa 3). Em seguida, você pode fazer as alterações que desejar e conectar-se às saídas / ações em sua subclasse.
Aaron Brager
6
Eu não acho que você entende o conceito de subclasse.
Ace Green
5
Se "modificações posteriores no storyboard para o controlador da classe base não se propagam para a subclasse", isso não é chamado de "subclasse". É copiar e colar.
superarts.org
A classe subjacente, selecionada no Identity Inspector, ainda é uma subclasse. O objeto que está sendo inicializado e controlando a lógica de negócios ainda é uma subclasse. Apenas os dados de visualização codificados, armazenados como XML no arquivo de storyboard e inicializados via initWithCoder:, não têm um relacionamento herdado. Esse tipo de relacionamento não é compatível com arquivos de storyboard.
Aaron Brager
4

O método Objc_setclass não cria uma instância de childvc. Mas enquanto sai de childvc, deinit de childvc está sendo chamado. Como não há memória alocada separadamente para childvc, o aplicativo trava. O Basecontroller tem uma instância, enquanto o filho vc não tem.

user8044830
fonte
2

Se você não depende muito de storyboards, pode criar um arquivo .xib separado para o controlador.

Defina o proprietário do arquivo apropriado e saídas para o MainViewControllere substituainit(nibName:bundle:) no VC principal para que seus filhos possam acessar o mesmo Nib e suas saídas.

Seu código deve ser assim:

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

E seu filho VC será capaz de reutilizar a ponta dos pais:

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

Pegando respostas aqui e ali, eu encontrei esta solução bacana.

Crie um controlador de visualização pai com esta função.

class ParentViewController: UIViewController {


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

        object_setClass(self, T.self)

    }

}

Isso permite que o compilador garanta que o controlador de visualização filho herde do controlador de visualização pai.

Então, sempre que quiser seguir para este controlador usando uma subclasse, você pode fazer:

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

}

A parte legal é que você pode adicionar uma referência de storyboard a si mesmo e, em seguida, continuar chamando o "próximo" controlador de visualização filho.

nickgzzjr
fonte
1

Provavelmente, a maneira mais flexível é usar visualizações reutilizáveis.

(Crie uma visualização em arquivo XIB separado ou Container viewadicione-a a cada cena do controlador de visualização de subclasse no storyboard)

DanSkeel
fonte
1
Por favor, comente quando você votar negativamente. Sei que não respondo diretamente a essa pergunta, mas proponho uma solução para o problema raiz.
DanSkeel de
1

Existe uma solução simples e óbvia para o dia-a-dia.

Basta colocar o storyboard / controlador existente dentro do novo storyobard / controlador. IE como uma visualização de contêiner.

Este é o conceito exatamente análogo a "subclassificar", para controladores de visualização.

Tudo funciona exatamente como em uma subclasse.

Assim como você costuma colocar uma subvisão de visão dentro de outra visão , naturalmente você costuma colocar um controlador de visão dentro de outro controlador de visão .

De que outra forma você poderia fazer isso?

É uma parte básica do iOS, tão simples quanto o conceito de "subvisão".

É assim tão 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)
    }
}

Agora você obviamente tem listque fazer o que quiser com

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

etc etc.

As visualizações de contêiner são "exatamente como" subclasses da mesma forma que "subvisualizações" são "exatamente como" subclasses.

Claro, obviamente, você não pode "subclassificar um layout" - o que isso significa?

("Subclasse" refere-se ao software OO e não tem conexão com "layouts".)

Obviamente, quando você deseja reutilizar uma visualização, basta subvisualizá-la dentro de outra visualização.

Quando você quiser reutilizar um layout de controlador, basta visualizá-lo em outro controlador.

Este é o mecanismo mais básico do iOS !!


Nota - há anos tem sido trivial carregar dinamicamente outro controlador de visualização como uma visualização de contêiner. Explicado na última seção: https://stackoverflow.com/a/23403979/294884

Observação - "_sb" é apenas uma macro óbvia que usamos para economizar digitação,

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

Obrigado pela resposta inspiradora de @ Jiří Zahálka, eu respondi minha solução há 4 anos aqui , mas @Sayka me sugeriu postá-la como uma resposta, então aqui está.

Em meus projetos, normalmente, se estou usando o Storyboard para uma subclasse UIViewController, sempre preparo um método estático chamado instantiate()nessa subclasse, para criar uma instância do Storyboard facilmente. Portanto, para resolver a questão do OP, se quisermos compartilhar o mesmo Storyboard para diferentes subclasses, podemos simplesmente setClass()ir para essa instância antes de retorná-la.

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

O comentário de Cocoabob da resposta de Jiří Zahálka me ajudou a encontrar essa solução e funcionou bem.

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
fonte