如何将单个故事板uiviewcontroller用于多个子类


118

假设我有一个情节提要,其中包含UINavigationController作为初始视图控制器。其根视图控制器是子类UITableViewController,这是BasicViewController。它具有IBAction连接到导航栏的右侧导航按钮的功能。

从那里开始,我想将情节提要用作其他视图的模板,而不必创建其他情节提要。说这些视图将具有完全相同的接口,但是具有类的根视图控制器,SpecificViewController1并且SpecificViewController2是的子类BasicViewController
IBAction方法外,这两个视图控制器将具有相同的功能和接口。
就像下面这样:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

我可以那样做吗?
可我只是实例化的故事板BasicViewController,但有根视图控制器子类SpecificViewController1SpecificViewController2

谢谢。


3
可能值得指出的是,您可以使用笔尖进行此操作。但是,如果您像我一样想要一些仅故事板具有的出色功能(例如静态/原型单元),那么我想我们很走运。
约瑟夫·林

Answers:


57

好问题-但不幸的是,答案很la脚。我不认为当前可以按照您的建议进行操作,因为UIStoryboard中没有初始化程序,该初始化程序可以覆盖故事板中初始化时故事板中的对象详细信息中定义的与故事板关联的视图控制器。在初始化时,stoaryboard中的所有UI元素都将链接到其在视图控制器中的属性。

默认情况下,它将使用情节提要定义中指定的视图控制器进行初始化。

如果要重用在情节提要中创建的UI元素,它们仍必须链接或关联到视图控制器使用它们的属性,以便它们能够“告诉”视图控制器有关事件的信息。

复制情节提要布局并没什么大不了的,特别是如果您只需要对3个视图进行类似的设计,但是如果这样做,则必须确保清除所有先前的关联,否则在尝试时会崩溃与先前的视图控制器进行通信。您将能够在日志输出中将它们识别为KVO错误消息。

您可以采取几种方法:

  • 将UI元素存储在UIView中-xib文件中,并从您的基类实例化它,并将其作为子视图添加到主视图(通常是self.view)中。然后,您将只使用情节提要布局,将基本上空白的视图控制器保留在情节提要中的位置,但为其分配正确的视图控制器子类。因为他们将从基础继承,所以他们会得到这种看法。

  • 在代码中创建布局,然后从基本视图控制器安装它。显然,这种方法无法达到使用情节提要的目的,但可能是您应采用的方式。如果应用程序的其他部分可以从情节提要方法中受益,则可以在适当的地方到处走动。在这种情况下,就像上面一样,您只需使用分配了子类的银行视图控制器,然后让基本视图控制器安装UI。

如果Apple想出一种方法来完成您的建议,那将是很好的选择,但是将图形元素与控制器子类预先链接的问题仍然是一个问题。

祝你新年快乐!很好


那很快。正如我所想,这是不可能的。目前,我只提供了一个BasicViewController类并提供了额外的属性来指示它将充当哪个“类” /“模式”,从而提出了一个解决方案。不管怎么说,还是要谢谢你。
verdy 2013年

2
太糟糕了:(想我必须同级车复制和粘贴相同的视图控制器,改变作为一种解决方法。
Hlung

1
这就是为什么我不喜欢Storyboards的原因……一旦您做的比标准视图多一点,它们就无法真正起作用……
TheEye 2014年

听到您这么说时感到非常难过。我正在寻找解决方案
Tony

2
还有另一种方法:在不同的委托中指定自定义逻辑,然后在prepareForSegue中分配正确的委托。这样,您在情节提要中创建了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
我不确定我了解这应该如何工作。父母将其班级设置为孩子的班级吗?那你怎么能有多个孩子?
user1366265 '16

2
您长官,我过
得很愉快

2
好的,让我更详细地解释一下:我们想要实现什么?我们想对ParentViewController进行子类化,以便可以将其Storyboard用于更多类。因此,在我的解决方案中突出显示了完成所有操作的魔力线,并且必须在ParentVC的awakeFromNib中使用它。然后发生的事情是,它将使用新设置的ChildVC1中的所有方法,该方法将成为子类。如果您想将其用于更多的ChildVC?只需在awakeFromNib中执行逻辑即可。if(type = a){object_setClass(s​​elf,ChildVC1.self)} else {object_setClass(s​​elf.ChildVC2.self)}祝您好运。
吉日Zahálka

10
使用时要非常小心!通常,这根本不应该使用。。。这只是更改给定指针的isa指针,并且不重新分配内存以适应例如不同的属性。一种指示是,指向的指针self没有改变。因此,检查对象(例如读取_ivar /属性值)object_setClass可能会导致崩溃。
Patrik'8

15

正如公认的答案所言,似乎不可能使用情节提要。

我的解决方案是使用Nib's-就像开发人员在情节提要之前使用它们一样。如果您想拥有可重用,可子类化的视图控制器(甚至是视图),我的建议是使用Nibs。

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

当您将所有出口连接到中的“文件所有者”时,MyViewController.xib您没有指定应装入Nib的类,您只是在指定键值对:“ 此视图应连接到此实例变量名。” 当调用[SubclassMyViewController alloc] initWithNibName:初始化过程时,指定将使用哪个视图控制器来“ 控制 ”您在笔尖中创建的视图。


令人惊讶的是,借助ObjC运行时库,故事板可以做到这一点。在这里检查我的答案:stackoverflow.com/a/57622836/7183675
Adam Tucholski

9

情节提要可能会实例化自定义视图控制器的不同子类,尽管它涉及一种稍微不合常规的技术:覆盖alloc视图控制器的方法。创建自定义视图控制器时,实际上,重写的alloc方法返回alloc在子类上运行的结果。

我应该以附带条件作为答案的开头,尽管我已经在各种情况下对其进行了测试并且没有收到任何错误,但是我无法确保它可以应付更复杂的设置(但是我看不出为什么它不起作用) 。另外,我还没有使用此方法提交任何应用程序,因此外界有可能它会被Apple的审查程序拒绝(尽管我再也没有理由这样做)。

出于演示目的,我有一个UIViewController名为的子类TestViewController,它具有UILabel IBOutlet和IBAction。在我的情节提要中,我添加了一个视图控制器,并将其类修改为TestViewController,并将IBOutlet连接到UILabel,并将IBAction连接到UIButton。我通过前面的viewController上的UIButton触发的模式选择来呈现TestViewController。

故事板图片

为了控制哪个类被实例化,我添加了一个静态变量和关联的类方法,以便获取/设置要使用的子类(我想可以采用其他方法确定要实例化哪个子类):

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

为了测试,我有两个子类TestViewControllerRedTestViewControllerGreenTestViewController。子类每个都有其他属性,并且每个重写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自身,在其他情况下RedTestViewControllerGreenTestViewController。在前面的视图控制器中,我随机执行以下操作:

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用来测试此功能。


我们在项目中做过类似的事情,但是重写了UIViewController的alloc方法,以从外部类获取子类,该子类收集有关所有替代的完整信息。完美运作。
2015年

顺便说一句,此方法可能像Apple停止在视图控制器上调用alloc一样停止工作。例如,NSManagedObject类从不接收alloc方法。我认为Apple可以将代码复制到另一种方法:也许+ allocManagedObject
Tim

7

在实例化ViewControllerWithIdentifier之后尝试此操作。

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

喜欢 :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];

请添加一些有关代码功能的有用说明。
codeforester

7
如果您使用子类的实例变量,会发生什么?我猜是崩溃,因为没有足够的内存分配给它。在测试中,我一直在学习EXC_BAD_ACCESS,因此不推荐这样做。
Legoless

1
如果您要在子类中添加新变量,则将无法使用。而且孩子的init也不会被叫。这种限制使所有方法都无法使用。
Al Zonke '18年

6

特别是基于nickgzzjrJiříZahálka的答案以及来自CocoaBob的第二个评论的注释,我已经准备了可以满足OP需求的简短通用方法。您只需要检查情节提要的名称和View Controller情节提要的ID

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. 更改 在Identity检查员子类视图控制器

这是我编写的Bloc教程的一个示例,使用以下子类ViewController进行继承WhiskeyViewController

以上三个步骤的动画

这使您可以在情节提要中创建视图控制器子类的子类。然后,您可以使用instantiateViewControllerWithIdentifier:用来创建特定的子类。

这种方法有点不灵活:在情节提要中对基类控制器进行的后续修改不会传播到子类。如果您有很多子类,那么使用其他解决方案中的一种可能会更好,但这确实很重要。


11
这不是一个子类伴侣,这只是复制一个ViewController。
Ace Green

1
那是不对的。当您将Class更改为子类时,它成为一个子类(步骤3)。然后,您可以进行所需的任何更改,并连接到子类中的输出/操作。
亚伦·布拉格

6
我认为您没有子类化的概念。
艾斯·格林

5
如果“情节提要中对基类控制器的后来修改没有传播到子类”,则不称为“子类”。它是复制并粘贴。
superarts.org

在身份检查器中选择的基础类仍然是子类。正在初始化并控制业务逻辑的对象仍然是子类。只有编码的视图数据(以XML形式存储在情节提要文件中,并通过初始化initWithCoder:)没有继承关系。情节提要文件不支持这种类型的关系。
亚伦·布拉格

4

Objc_setclass方法不会创建childvc的实例。但是在弹出childvc时,正在调用childvc的deinit。由于没有为childvc单独分配内存,因此应用程序崩溃。Basecontroller有一个instance,而子vc没有。


2

如果您不太依赖情节提要,则可以为控制器创建一个单独的.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
    }
}

而且您的Child 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

有一个简单,显而易见的日常解决方案。

只需将现有的情节提要/控制器放入新的Storyobard /控制器中即可。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创建实例。因此,为了解决OP的问题,如果我们想为不同的子类共享相同的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

JiříZahálka回答中的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);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.