假设我有一个包含 UINavigationController
作为初始视图控制器的故事板。它的根视图控制器是 UITableViewController
的子类,即 BasicViewController
。它具有连接到导航栏右侧导航按钮的 IBAction
从那里我想将情节提要用作其他视图的模板,而无需创建其他情节提要。假设这些视图将具有完全相同的界面,但具有类 SpecificViewController1
和 SpecificViewController2
的根视图控制器,它们是 BasicViewController
的子类。
这两个视图控制器将具有相同的功能和界面,除了 {4 } 方法。
如下所示:
@interface BasicViewController : UITableViewController
@interface SpecificViewController1 : BasicViewController
@interface SpecificViewController2 : BasicViewController
我可以做这样的事情吗?
我可以只实例化 BasicViewController
的情节提要,但让根视图控制器成为 SpecificViewController1
和 SpecificViewController2
的子类吗?
谢谢。
很好的问题 - 但不幸的是只有一个蹩脚的答案。我不相信目前可以按照您的建议进行操作,因为 UIStoryboard 中没有初始化程序允许覆盖与情节提要相关联的视图控制器,如初始化情节提要中的对象详细信息中定义的那样。在初始化时,故事板中的所有 UI 元素都链接到它们在视图控制器中的属性。
默认情况下,它将使用情节提要定义中指定的视图控制器进行初始化。
如果您试图重用您在情节提要中创建的 UI 元素,它们仍然必须链接或关联到视图控制器使用它们的属性,以便它们能够“告诉”视图控制器有关事件。
复制故事板布局并不是什么大问题,特别是如果您只需要 3 个视图的类似设计,但是如果您这样做,您必须确保清除所有以前的关联,否则它会在尝试时崩溃与前一个视图控制器通信。您将能够在日志输出中将它们识别为 KVO 错误消息。
您可以采取以下几种方法:
将 UI 元素存储在 UIView 中 - 在 xib 文件中,并从您的基类中实例化它,并将其作为子视图添加到主视图中,通常是 self.view。然后,您只需使用情节提要布局,基本上空白的视图控制器在情节提要中保持其位置,但为其分配了正确的视图控制器子类。由于他们将从基地继承,他们将获得这种观点。
在代码中创建布局并从基本视图控制器安装它。显然,这种方法违背了使用情节提要的目的,但在您的情况下可能是要走的路。如果您的应用程序的其他部分可以从情节提要方法中受益,则可以在适当的情况下在这里和那里偏离。在这种情况下,就像上面一样,您只需使用分配了子类的银行视图控制器,然后让基本视图控制器安装 UI。
如果 Apple 想出一种方法来执行您的建议,那就太好了,但是将图形元素与控制器子类预先链接的问题仍然是一个问题。
有一个伟大的新年!好起来
我们正在寻找的行代码是:
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
}
}
class func instantiate() -> SubClass { let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)! object_setClass(instance, SubClass.self) return (instance as? SubClass)! }
self
的指针没有改变。因此,在 object_setClass
之后检查对象(例如读取 _ivar / 属性值)可能会导致崩溃。
正如公认的答案所述,看起来不可能与情节提要有关。
我的解决方案是使用 Nib——就像开发人员在故事板之前使用它们一样。如果你想拥有一个可重用、可子类化的视图控制器(甚至是视图),我的建议是使用 Nibs。
SubclassMyViewController *myViewController = [[SubclassMyViewController alloc] initWithNibName:@"MyViewController" bundle:nil];
当您将所有插座连接到 MyViewController.xib
中的“文件所有者”时,您并没有指定 Nib 应该加载为哪个类,您只是指定键值对:“这个视图应该连接到这个实例变量名称。"调用 [SubclassMyViewController alloc] initWithNibName:
时,初始化过程指定将使用哪个视图控制器来“控制”您在 nib 中创建的视图。
可以让故事板实例化自定义视图控制器的不同子类,尽管它涉及一种稍微不正统的技术:覆盖视图控制器的 alloc
方法。创建自定义视图控制器时,被覆盖的 alloc 方法实际上返回了在子类上运行 alloc
的结果。
我应该以附带条件作为答案的开头,尽管我已经在各种场景中对其进行了测试并且没有收到任何错误,但我无法确保它能够应对更复杂的设置(但我认为它没有理由不工作) .此外,我还没有使用这种方法提交任何应用程序,因此它可能会被 Apple 的审核流程拒绝(尽管我再次认为没有理由这样做)。
出于演示目的,我有一个名为 TestViewController
的 UIViewController
子类,它有一个 UILabel IBOutlet 和一个 IBAction。在我的故事板中,我添加了一个视图控制器并将其类修改为 TestViewController
,并将 IBOutlet 连接到 UILabel 并将 IBAction 连接到 UIButton。我通过由前一个 viewController 上的 UIButton 触发的模态 segue 来呈现 TestViewController。
https://i.stack.imgur.com/MxfVU.png
为了控制实例化哪个类,我添加了一个静态变量和关联的类方法,以便获取/设置要使用的子类(我想可以采用其他方法来确定要实例化哪个子类):
测试视图控制器.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
的两个子类:RedTestViewController
和 GreenTestViewController
。每个子类都有额外的属性,并且每个都覆盖 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
本身,在其他情况下,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"];
}
请注意,setClassForStoryBoard
方法会检查以确保请求的类名确实是 TestViewController 的子类,以避免任何混淆。上面对 BlueTestViewController
的引用用于测试此功能。
特别基于 nickgzzjr 和 Jiří 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(将包含垃圾)。
试试这个,在instanceViewControllerWithIdentifier 之后。
- (void)setClass:(Class)c {
object_setClass(self, c);
}
喜欢 :
SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
EXC_BAD_ACCESS
,所以不推荐这个。
init
也不会被调用。这样的限制使所有方法都无法使用。
虽然它不是严格的子类,但您可以:
选项-拖动文档大纲中的基类视图控制器以制作副本将新视图控制器副本移动到情节提要上的单独位置在身份检查器中将类更改为子类视图控制器
以下是我编写的 Bloc 教程中的一个示例,将 ViewController
与 WhiskeyViewController
子类化:
https://i.stack.imgur.com/NiXj4.gif
这允许您在情节提要中创建视图控制器子类的子类。然后,您可以使用 instantiateViewControllerWithIdentifier:
创建特定的子类。
这种方法有点不灵活:情节提要中对基类控制器的后期修改不会传播到子类。如果您有很多子类,则使用其他解决方案可能会更好,但这将在紧要关头。
initWithCoder:
初始化,没有继承关系。故事板文件不支持这种类型的关系。
Objc_setclass 方法不会创建 childvc 的实例。但是在弹出 childvc 时,正在调用 childvc 的 deinit。由于没有为 childvc 单独分配内存,因此应用程序崩溃。 Basecontroller 有一个 instance ,而 child vc 没有。
如果您不太依赖故事板,您可以为控制器创建一个单独的 .xib 文件。
将适当的文件所有者和出口设置为 MainViewController
并覆盖主 VC 中的 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
}
}
有一个简单、明显的日常解决方案。
只需将现有的故事板/控制器放在新的故事板/控制器中。 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")
}
感谢@Jiří Zahálka 鼓舞人心的回答,我在 4 年前回答了我的解决方案 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)!
}
这是一个不依赖于 Objective-C 类交换技巧的 Swift 解决方案。它使用 instantiateViewController(identifier:creator:)
(iOS 13+)。我假设您在故事板中有视图控制器,标识符为 template
。故事板中分配给视图控制器的类应该是超类:
let storyboard = UIStoryboard(name: "main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "template") { coder in
// The coder provides access to the storyboard data.
// We can now init the preferred UIViewController subclass.
if useSubclass {
return SpecialViewController(coder: coder)
} else {
return BaseViewController(coder: coder)
}
}
可能最灵活的方法是使用可重用视图。
(在单独的 XIB 文件或 Container view
中创建一个视图并将其添加到情节提要中的每个子类视图控制器场景中)
从各处获取答案,我想出了这个巧妙的解决方案。
用这个函数创建一个父视图控制器。
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)
}
}
很酷的部分是你可以添加一个故事板引用到它自己,然后继续调用“下一个”子视图控制器。
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);
}
这很简单。只需在 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()