如何使用单个故事板uiviewcontroller为多个子类

时间:2013-01-01 15:15:10

标签: objective-c cocoa-touch ios5 uiviewcontroller storyboard

假设我有一个包含UINavigationController作为初始视图控制器的故事板。它的根视图控制器是UITableViewController的子类,它是BasicViewController。它有IBAction,它连接到导航栏的右侧导航按钮
从那里我想使用故事板作为其他视图的模板,而无需创建其他故事板。假设这些视图具有完全相同的界面,但具有SpecificViewController1SpecificViewController2类的根视图控制器,它们是BasicViewController的子类。
除了IBAction方法之外,这两个视图控制器将具有相同的功能和界面 它将如下所示:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

我可以这样做吗? 我是否可以实例化BasicViewController的故事板,但是将根视图控制器设置为子类SpecificViewController1SpecificViewController2
感谢。

15 个答案:

答案 0 :(得分:56)

很好的问题 - 但不幸的是只有一个蹩脚的答案。我不相信目前可以做你提出的建议,因为UIStoryboard中没有初始化程序允许覆盖与故事板相关联的视图控制器,如初始化时故事板中的对象详细信息中所定义。初始化时,stoaryboard中的所有UI元素都链接到视图控制器中的属性。

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

如果您试图重新使用在故事板中创建的UI元素,它们仍然必须链接或关联到视图控制器正在使用它们的属性,以便它们能够“告诉”视图控制器有关事件

复制故事板布局并不是一件大事,特别是如果你只需要3个视图的类似设计,但如果你这样做,你必须确保所有以前的关联被清除,否则它将崩溃当它尝试与前一个视图控制器通信时。您将能够在日志输出中将它们识别为KVO错误消息。

您可以采取以下几种方法:

  • 将UI元素存储在UIView中 - 在xib文件中,并从基类中实例化它,并将其作为子视图添加到主视图中,通常是self.view。然后,您只需使用故事板布局,基本上空白的视图控制器在故事板中保留它们的位置,但分配给它们的是正确的视图控制器子类。由于他们会从基地继承,他们会得到那种观点。

  • 在代码中创建布局并从基本视图控制器安装它。显然,这种方法违背了使用故事板的目的,但可能是你的情况。如果您有应用程序的其他部分可以从故事板方法中受益,那么可以在适当的时候在这里和那里进行偏离。在这种情况下,如上所述,您只需使用已分配子类的银行视图控制器,并让基本视图控制器安装UI。

如果Apple提出了一种方法来实现您的建议,那将会很好,但是将图形元素与控制器子类预链接的问题仍然是一个问题。

祝新年快乐! 好吧

答案 1 :(得分:43)

我们正在寻找的代码是:

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

答案 2 :(得分:12)

正如接受的答案所述,它看起来不像故事板。

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

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

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

答案 3 :(得分:9)

可以让故事板实例化自定义视图控制器的不同子类,尽管它涉及一种稍微不正统的技术:覆盖视图控制器的alloc方法。创建自定义视图控制器时,重写的alloc方法实际上会返回在子类上运行alloc的结果。

我应该在答案前加上条件,虽然我已经在各种情况下对其进行了测试并且没有收到任何错误,但我无法确保它能应对更复杂的设置(但我认为没有理由为什么不应该工作)。此外,我还没有使用这种方法提交任何应用程序,因此有可能被Apple的审核流程拒绝(尽管我再也看不出它应该这样做了。)

出于演示目的,我有一个名为UIViewController的{​​{1}}子类,它有一个UILabel IBOutlet和一个IBAction。在我的故事板中,我添加了一个视图控制器并将其类修改为TestViewController,并将IBOutlet连接到UILabel,将IBAction连接到UIButton。我通过前面的viewController上的UIButton触发的模态segue来呈现TestViewController。

Storyboard image

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

TestViewController.m:

TestViewController

对于我的测试,我有两个#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]; } } } 的子类:TestViewControllerRedTestViewController。每个子类都有其他属性,每个都覆盖GreenTestViewController以更改视图的背景颜色并更新UILabel IBOutlet的文本:

RedTestViewController.m:

viewDidLoad

GreenTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor redColor];
    self.testLabel.text = @"Set by RedTestVC";
}

在某些情况下,我可能希望在其他情况下- (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"]; } 方法检查以确保所请求的类名确实是TestViewController的子类,以避免任何混淆。以上对setClassForStoryBoard的引用是为了测试此功能。

答案 4 :(得分:6)

在instantiateViewControllerWithIdentifier之后尝试这个。

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

喜欢:

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

答案 5 :(得分:5)

虽然它不是严格意义上的子类,但您可以:

  1. 选项 -drag文档大纲中的基类视图控制器进行复制
  2. 将新视图控制器副本移至故事板上的单独位置
  3. Class 更改为Identity Inspector中的子类视图控制器
  4. 以下是我编写的Bloc教程的示例,将ViewControllerWhiskeyViewController子类化:

    animation of the above three steps

    这允许您在故事板中创建视图控制器子类的子类。然后,您可以使用instantiateViewControllerWithIdentifier:创建特定的子类。

    这种方法有点不灵活:后来故事板内对基类控制器的修改不会传播到子类。如果您有一个批次的子类,那么使用其他解决方案之一可能会更好,但这样做会很紧张。

答案 6 :(得分:2)

Objc_setclass方法不会创建childvc的实例。但是当弹出childvc时,childvc的deinit正在调用。由于没有为childvc分配内存,app崩溃了。 Basecontroller有一个实例,而子vc没有。

答案 7 :(得分:2)

尤其是根据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警告),但方法将返回正确的对象。

答案 8 :(得分:1)

如果您不太依赖故事板,可以为控制器创建单独的.xib文件。

将相应文件的所有者和出口设置为MainViewController并覆盖主VC中的init(nibName:bundle:),以便其子项可以访问相同的笔尖及其出口。

您的代码应如下所示:

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

答案 9 :(得分:1)

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

只需将现有的情节提要/控制器放在新的Storyobard /控制器中。即作为容器视图。

对于视图控制器,这与“子类化”完全类似。

一切都与子类中的完全一样。

就像通常将视图子视图放入另一个视图一样,自然地,您通常将视图控制器放入另一个视图控制器

您还能怎么做?

这是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")
}

答案 10 :(得分:1)

感谢@JiříZahálka的启发性答案,我在四年前here回答了我的解决方案,但是@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)!
}

答案 11 :(得分:0)

可能最灵活的方法是使用可重复使用的视图。

(在单独的XIB文件或Reading package lists... Done Building dependency tree Reading state information... Done Package php5 is not available but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source E: Package php5 has no installation candidate 中创建一个视图,并将其添加到故事板中的每个子类视图控制器场景)

答案 12 :(得分:0)

从这里到那里获取答案,我想出了一个简洁的解决方案。

使用此功能创建父视图控制器。

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

}

最酷的部分是您可以向其自身添加一个故事板引用,然后继续调用“下一个”子视图控制器。

答案 13 :(得分: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);
}

答案 14 :(得分:0)

很简单。只需在 xib 中定义 BaseViewController,然后像这样使用它:

let baseVC: BaseViewController = BaseViewController(nibName: "BaseViewController", bundle: nil)
let subclassVC: ChildViewController = ChildViewController(nibName: "BaseViewController", bundle: nil)

制作很简单,您可以将标识符提取到一个字段并加载到一个方法中,例如:

public static var baseNibIdentifier: String {
    return "BaseViewController"
}

public static func loadFromBaseNib<T>() -> T where T : UIViewController {
    return T(nibName: self.baseNibIdentifier, bundle: nil)
}

然后你可以这样使用它:

let baseVC: BaseViewController = BaseViewController.loadFromBaseNib()
let subclassVC: ChildViewController = ChildViewController.loadFromBaseNib()