126

假设我有一个包含UINavigationController作为初始视图控制器的故事板。它的根视图控制器是 的子类UITableViewController,即BasicViewController. 它IBAction连接到导航栏的右侧导航按钮

从那里我想将情节提要用作其他视图的模板,而无需创建其他情节提要。假设这些视图将具有完全相同的界面,但具有类的根视图控制器,SpecificViewController1并且SpecificViewController2它们是BasicViewController.
除了方法之外,这两个视图控制器将具有相同的功能和界面IBAction
如下所示:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

我可以做这样的事情吗?
我可以只实例化情节提要BasicViewController但有根视图控制器来子类SpecificViewController1SpecificViewController2吗?

谢谢。

4

15 回答 15

60

很好的问题 - 但不幸的是只有一个蹩脚的答案。我不相信目前可以按照您的建议进行操作,因为 UIStoryboard 中没有初始化程序允许覆盖与情节提要相关联的视图控制器,如初始化情节提要中的对象详细信息中定义的那样。在初始化时,故事板中的所有 UI 元素都链接到它们在视图控制器中的属性。

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

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

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

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

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

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

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

有一个伟大的新年!好起来

于 2013-01-01T17:51:21.120 回答
44

我们正在寻找的行代码是:

object_setClass(AnyObject!, AnyClass!)

在 Storyboard -> 添加 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
    }
}
于 2015-08-19T19:03:31.687 回答
15

正如公认的答案所述,看起来不可能与情节提要有关。

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

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

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

于 2015-03-19T14:18:29.513 回答
9

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

我应该以附带条件作为答案的开头,尽管我已经在各种场景中对其进行了测试并且没有收到任何错误,但我无法确保它能够应对更复杂的设置(但我认为它没有理由不工作) . 此外,我还没有使用这种方法提交任何应用程序,因此它可能会被 Apple 的审核流程拒绝(尽管我再次认为没有理由这样做)。

出于演示目的,我有一个UIViewControllercalled的子类TestViewController,它有一个 UILabel IBOutlet 和一个 IBAction。在我的故事板中,我添加了一个视图控制器并将其类修改为TestViewController,并将 IBOutlet 连接到 UILabel 并将 IBAction 连接到 UIButton。我通过由前一个 viewController 上的 UIButton 触发的模态 segue 来呈现 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:RedTestViewControllerGreenTestViewController. 每个子类都有额外的属性,并且每个覆盖都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是为了测试这个功能。

于 2015-03-23T20:47:46.323 回答
6

试试这个,在instanceViewControllerWithIdentifier 之后。

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

像 :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
于 2017-02-04T01:56:35.213 回答
6

特别基于nickgzzjrJiří Zahálka的答案以及来自 CocoaBob 的第二个答案下的评论,我准备了简短的通用方法来完全满足 OP 的需求。您只需要检查故事板名称和视图控制器故事板 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 警告),但方法返回正确的对象。

另外:您需要初始化仅存在于子类中的属性,然后再从转换对象中读取它们(如果子类具有这些属性而BasicViewController没有)。这些属性不会自动初始化,并且在初始化之前尝试读取它们会导致崩溃。因为它们在那里有强制转换的效果,所以即使是弱变量也很可能不会被设置为 nil(将包含垃圾)。

于 2019-08-23T08:53:55.180 回答
5

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

  1. option- 拖动文档大纲中的基类视图控制器进行复制
  2. 将新的视图控制器副本移动到情节提要上的单独位置
  3. 在 Identity Inspector 中将Class更改为子类视图控制器

这是我编写的Bloc教程中的一个示例,子ViewController类化为WhiskeyViewController

以上三个步骤的动画

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

这种方法有点不灵活:情节提要中对基类控制器的后期修改不会传播到子类。如果您有很多子类,则使用其他解决方案可能会更好,但这将在紧要关头。

于 2015-03-22T03:25:46.183 回答
4

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

于 2017-05-21T18:14:22.487 回答
2

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

将适当的 File's Owner 和 outlets 设置为 Main VC 中的MainViewController和覆盖init(nibName:bundle:),以便其子级可以访问相同的 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
    }
}

您的子 VC 将能够重用其父级的 nib:

class ChildViewController: MainViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .blue
    }
}
于 2017-07-28T13:05:11.910 回答
2

有一个简单、明显的日常解决方案。

只需将现有的故事板/控制器放在新的故事板/控制器中。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")
}
于 2019-10-07T16:27:37.873 回答
2

感谢@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)!
}
于 2020-06-11T03:59:59.433 回答
1

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

(在单独的 XIB 文件中创建一个视图,或者Container view将其添加到情节提要中的每个子类视图控制器场景中)

于 2016-04-28T09:39:08.953 回答
1

从各处获取答案,我想出了这个巧妙的解决方案。

用这个函数创建一个父视图控制器。

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

}

很酷的部分是你可以添加一个故事板引用到它自己,然后继续调用“下一个”子视图控制器。

于 2019-04-11T20:19:20.930 回答
0

Cocoabob 来自 Jiří Zahálka 的评论帮助我获得了这个解决方案,并且效果很好。

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);
}
于 2020-06-09T20:28:58.330 回答
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()
于 2021-06-23T07:19:18.620 回答