ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

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

谢谢。

值得指出的是,您可以使用 nib 执行此操作。但是,如果您像我一样想要一些只有情节提要才有的好功能(例如静态/原型单元),那么我想我们不走运。

K
Khawar

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

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

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

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

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

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

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

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

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


那很快。正如我所想,这是不可能的。目前我想出了一个解决方案,只有那个 BasicViewController 类,并有额外的属性来指示它将作为哪个“类”/“模式”。不管怎么说,还是要谢谢你。
太糟糕了:(我想我必须复制并粘贴相同的视图控制器并将其类更改为一种解决方法。
这就是为什么我不喜欢情节提要......一旦你做的比标准视图多一点,它们就不会真正起作用......
听到你这么说,好难过。我正在寻找解决方案
还有另一种方法:在不同的委托中指定自定义逻辑,并在 prepareForSegue 中分配正确的委托。这样,您在 Storyboard 中创建了 1 个 UIViewController + 1 个 UIViewController,但您有多个实现版本。
J
Jiří Zahálka

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

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)! }
我不确定我是否理解这应该如何工作。父母将其班级设置为孩子的班级?那你怎么可能有多个孩子?!
先生,你让我开心
“类型”在哪里设置?在用户定义的运行时属性中?还是在代码中?
使用时要非常小心!通常这根本不应该使用...这只是更改给定指针的 isa 指针,并且不会重新分配内存以适应不同的属性。一个指标是指向 self 的指针没有改变。因此,在 object_setClass 之后检查对象(例如读取 _ivar / 属性值)可能会导致崩溃。
F
Fattie

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

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

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

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


令人惊讶的是,由于 ObjC 运行时库,故事板似乎可以做到这一点。在这里查看我的答案:stackoverflow.com/a/57622836/7183675
p
pbasdf

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

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

出于演示目的,我有一个名为 TestViewControllerUIViewController 子类,它有一个 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 的两个子类: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 的引用用于测试此功能。


我们在项目中做了类似的事情,但是重写了 UIViewController 的 alloc 方法以从外部类获取子类,收集有关所有重写的完整信息。完美运行。
顺便说一下,这个方法可能会停止工作,就像 Apple 停止在视图控制器上调用 alloc 一样。对于示例 NSManagedObject 类从不接收 alloc 方法。我认为Apple可以将代码复制到另一种方法:也许+allocManagedObject
A
Adam Tucholski

特别基于 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(将包含垃圾)。


莫振华

试试这个,在instanceViewControllerWithIdentifier 之后。

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

喜欢 :

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

请添加一些有关您的代码功能的有用说明。
如果您使用子类中的实例变量,会发生什么情况?我猜是崩溃,因为没有分配足够的内存来适应它。在我的测试中,我得到了 EXC_BAD_ACCESS,所以不推荐这个。
如果您将在子类中添加新变量,这将不起作用。并且孩子的 init 也不会被调用。这样的限制使所有方法都无法使用。
S
Sergey Grischyov

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

选项-拖动文档大纲中的基类视图控制器以制作副本将新视图控制器副本移动到情节提要上的单独位置在身份检查器中将类更改为子类视图控制器

以下是我编写的 Bloc 教程中的一个示例,将 ViewControllerWhiskeyViewController 子类化:

https://i.stack.imgur.com/NiXj4.gif

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

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


这不是子类伴侣,这只是复制 ViewController。
那是不对的。当您将 Class 更改为子类时,它成为子类(步骤 3)。然后你可以做任何你想做的改变,并连接到你的子类中的出口/动作。
我认为您没有得到子类化的概念。
如果“情节提要中对基类控制器的后期修改不传播到子类”,则不称为“子类化”。这是复制和粘贴。
在 Identity Inspector 中选择的基础类仍然是子类。被初始化并控制业务逻辑的对象仍然是一个子类。只有编码的视图数据,作为 XML 存储在情节提要文件中并通过 initWithCoder: 初始化,没有继承关系。故事板文件不支持这种类型的关系。
u
user8044830

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


n
nitrous

如果您不太依赖故事板,您可以为控制器创建一个单独的 .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
    }
}

C
Community

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

只需将现有的故事板/控制器放在新的故事板/控制器中。 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")
}

C
CocoaBob

感谢@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)!
}

我试过这个,当我访问子类的属性时,有时会出现堆缓冲区溢出。似乎当您以这种方式设置类时,内存没有正确地重新分配给子类,所以我要说....这种方法可能不是一个好主意。
J
Justin Meiners

这是一个不依赖于 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)
    }
}

这是documentation


D
DanSkeel

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

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


请在您投反对票时发表评论。我知道我没有直接回答这个问题,但我提出了一个解决根本问题的方法。
n
nickgzzjr

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

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

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

}

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


A
Abdul Saleem

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

A
Archangel

这很简单。只需在 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()