複数のサブクラスに単一のストーリーボードuiviewcontrollerを使用する方法


118

UINavigationController最初のビューコントローラーを含むストーリーボードがあるとします。ルートビューコントローラはのサブクラスUITableViewControllerですBasicViewController。これはIBAction、ナビゲーションバーの右のナビゲーションボタンに接続されています

。追加のストーリーボードを作成せずに、ストーリーボードを他のビューのテンプレートとして使用したいと思います。これらのビューはまったく同じインターフェースが、ルート・ビュー・クラスのコントローラを持つことになりますと言うSpecificViewController1SpecificViewController2のサブクラスですBasicViewController
これらの2つのビューコントローラーは、IBActionメソッドを除いて同じ機能とインターフェイスを備えています。
次のようになります。

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

そのようなことはできますか?
ストーリーボードをインスタンス化することはできますが、BasicViewControllerサブクラス化するルートビューコントローラーがSpecificViewController1ありSpecificViewController2ますか?

ありがとう。


3
nibでこれを行うことができることを指摘する価値があるかもしれません。しかし、ストーリーボードだけが持ついくつかの優れた機能(たとえば、静的/プロトタイプセル)が必要な私のような場合、私たちは運が悪いと思います。
ジョセフリン

回答:


57

すばらしい質問ですが、残念ながら答えは不十分です。UIStoryboardには、初期化時にストーリーボードのオブジェクトの詳細で定義されているストーリーボードに関連付けられているビューコントローラーをオーバーライドできる初期化子がないため、提案したことを現在実行できるとは思いません。stoaryboardのすべてのUI要素がビューコントローラーのプロパティにリンクされるのは初期化時です。

デフォルトでは、ストーリーボード定義で指定されているビューコントローラーで初期化されます。

ストーリーボードで作成したUI要素を再利用しようとする場合でも、イベントについてView Controllerに「伝える」ためには、それらを、View Controllerがそれらを使用しているプロパティにリンクまたは関連付ける必要があります。

特に3つのビューに同様のデザインのみが必要な場合は、ストーリーボードレイアウトをコピーすることはそれほど重要ではありませんが、必要な場合は、以前のすべての関連付けがクリアされていることを確認する必要があります。以前のView Controllerと通信します。ログ出力では、それらをKVOエラーメッセージとして認識することができます。

あなたが取ることができるいくつかのアプローチ:

  • UI要素をUIView-xibファイルに保存し、基本クラスからインスタンス化して、メインビュー(通常はself.view)のサブビューとして追加します。次に、ストーリーボード内の場所を保持する基本的に空白のビューコントローラーを使用して、適切なビューコントローラーサブクラスを割り当てたストーリーボードレイアウトを使用します。彼らは基地から継承するので、彼らはその見解を得るでしょう。

  • コードでレイアウトを作成し、ベースビューコントローラーからインストールします。明らかに、このアプローチはストーリーボードを使用する目的に反しますが、あなたの場合はそれが適切な方法かもしれません。ストーリーボードアプローチの恩恵を受けるアプリの他の部分がある場合は、必要に応じて、あちこちに逸脱してもかまいません。この場合、上記のように、サブクラスが割り当てられたバンクビューコントローラーを使用し、ベースビューコントローラーにUIをインストールさせるだけです。

Appleが提案する方法を考え出せたらいいのですが、グラフィック要素をコントローラーサブクラスに事前にリンクさせるという問題は依然として問題です。

よいお年を!よくなって


早かった。思っていたように、それは不可能でしょう。現在、私はそのBasicViewControllerクラスだけで解決策を考え出し、それがどの「クラス」/「モード」として機能するかを示す追加のプロパティを持っています。とにかくありがとう。
ヴェルディ

2
あまりにも悪い:(私はコピー&ペースト同じビューコントローラをし、回避策としてそのクラスを変更する必要が推測。
Hlung

1
そして、これが私がストーリーボードを好きではない理由です...標準のビューより少しだけ多くのことを実行すると、どういうわけかそれらは実際には機能しません...
TheEye

あなたがそれを言ったと聞いてとても悲しい。私は解決策を探しています
トニー・

2
別のアプローチがあります。さまざまなデリゲートでカスタムロジックを指定し、prepareForSegueで正しいデリゲートを割り当てます。このようにして、ストーリーボードに1つのUIViewController + 1つのUIViewControllerを作成しますが、複数の実装バージョンがあります。
plam4u 2017年

45

私たちが探している行のコードは次のとおりです。

object_setClass(AnyObject!, AnyClass!)

Storyboard-> Add 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 2016

2
もし先生は、私の一日を作った
jere

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ポインタを変更するだけで、たとえば異なるプロパティに対応するためにメモリを再割り当てしません。これを示す1つの指標は、へのポインタがself変化しないことです。そのため、オブジェクトの検査(たとえば、_ivar /プロパティ値の読み取り)object_setClassがクラッシュする可能性があります。
Patrik 2017

15

受け入れられた回答が述べているように、それはストーリーボードで行うことが可能であるようには見えません。

私の解決策は、開発者がストーリーボードの前にそれらを使用したのと同じように、ニブを使用することです。再利用可能なサブクラス化可能なビューコントローラー(またはビュー)が必要な場合は、Nibsを使用することをお勧めします。

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

すべてのアウトレットをの「ファイル所有者」に接続する場合MyViewController.xib、Nibをロードするクラスを指定せず、キーと値のペアを指定するだけです。「このビューは、このインスタンス変数名に接続する必要があります。」[SubclassMyViewController alloc] initWithNibName:初期化プロセスを呼び出すときに、nibで作成したビューを「制御」するために使用するビューコントローラーを指定します。


驚くべきことに、ObjCランタイムライブラリのおかげで、これはストーリーボードで可能であるようです。ここで私の答えを確認してください:stackoverflow.com/a/57622836/7183675
Adam Tucholski

9

ストーリーボードにカスタムビューコントローラーのさまざまなサブクラスをインスタンス化させることは可能ですが、これには少し変わったalloc手法、つまりビューコントローラーのメソッドをオーバーライドすることが含まれます。カスタムビューコントローラーが作成されると、オーバーライドされたallocメソッドは、実際allocにはサブクラスで実行した結果を返します。

さまざまなシナリオでテストしてエラーが発生しなかったとしても、より複雑な設定に対応できるかどうかは保証できません(ただし、機能しない理由はわかりません)。 。また、私はこの方法を使用してアプリを提出していないため、Appleのレビュープロセスで拒否される可能性があります(ただし、なぜそうする必要があるのか​​はわかりません)。

デモンストレーションの目的のために、私が持っているのサブクラスUIViewControllerと呼ばれるTestViewControllerUILabel 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];
        }
    }
}

私のテストでは、TestViewControllerRedTestViewControllerとの2つのサブクラスがあり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は、この機能をテストするためのものです。


プロジェクトでも同様のことを行いましたが、UIViewControllerのallocメソッドをオーバーライドして、すべてのオーバーライドに関する完全な情報を収集する外部クラスからサブクラスを取得します。完璧に動作します。
Tim

ちなみに、Appleがビューコントローラでallocの呼び出しを停止すると、このメソッドは通常の動作を停止する可能性があります。この例では、NSManagedObjectクラスはallocメソッドを受け取りません。Appleはコードを別のメソッドにコピーできると思います。おそらく+ allocManagedObject
Tim

7

instantiateViewControllerWithIdentifierの後でこれを試してください。

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

お気に入り :

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

コードが何をするのかについて、役立つ説明を追加してください。
codeforester 2017

7
サブクラスのインスタンス変数を使用すると、どうなりますか?それに合うだけの十分なメモリが割り当てられていないので、私はクラッシュを推測しています。私のテストでは、を取得しているEXC_BAD_ACCESSので、これはお勧めしません。
Legoless

1
子クラスに新しい変数を追加する場合、これは機能しません。と子さんinitも呼ばれません。このような制約により、すべてのアプローチが使用できなくなります。
Al Zonke、2018年

6

特にnickgzzjrJiříZahálkaの回答と、CocoaBobの2番目のコメントに基づくコメントに基づいて、OPが必要とするものを正確に実行する短い汎用メソッドを用意しました。ストーリーボード名とView Controllersストーリーボード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. 新しいView Controllerのコピーをストーリーボード上の別の場所に移動します
  3. Identity InspectorでClassをサブクラスView Controllerに変更します。

これは私が書いたBlocチュートリアルの例で、次のようにサブクラス化ViewControllerしていWhiskeyViewControllerます:

上記の3つのステップのアニメーション

これにより、ストーリーボードにビューコントローラーサブクラスのサブクラスを作成できます。次に、を使用instantiateViewControllerWithIdentifier:して特定のサブクラスを作成できます。

このアプローチは少し柔軟性がありません。ストーリーボード内でのベースクラスコントローラへのその後の変更は、サブクラスに伝播しません。あなたが持っている場合は、多くのサブクラスのを、あなたは他の解決策の一つとしたほうが良いかもしれませんが、これはピンチで行います。


11
これはサブクラスの仲間ではなく、単にViewControllerを複製しているだけです。
エースグリーン

1
そうではありません。Classをサブクラスに変更すると、サブクラスになります(ステップ3)。次に、必要な変更を加えて、サブクラスのアウトレット/アクションに接続できます。
アーロンブラジャー2017

6
サブクラス化の概念は理解できないと思います。
エースグリーン

5
「ストーリーボード内でのベースクラスコントローラへのそれ以降の変更がサブクラスに伝播しない」場合、「サブクラス化」とは呼ばれません。コピー&ペーストです。
superarts.org

Identity Inspectorで選択された基になるクラスは、依然としてサブクラスです。初期化され、ビジネスロジックを制御するオブジェクトは、依然としてサブクラスです。ストーリーボードファイルにXMLとして保存され、を介して初期化された、エンコードされたビューデータのみinitWithCoder:が、継承された関係を持ちません。このタイプの関係は、ストーリーボードファイルではサポートされていません。
アーロンブラジャー2017

4

Objc_setclassメソッドは、childvcのインスタンスを作成しません。しかし、childvcからポップアウトしている間、childvcのdeinitが呼び出されています。childvcに個別に割り当てられたメモリがないため、アプリがクラッシュします。Basecontrollerにはインスタンスがありますが、子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
    }
}

そしてあなたの子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

シンプルで明白な日常の解決策があります。

既存のストーリーボード/コントローラーを新しいストーリーボード/コントローラー内に配置するだけです。コンテナビューとしての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年前に自分の解決策に返信しましが答えとして投稿することを提案したので、ここにあります。

私のプロジェクトでは、通常、UIViewControllerサブクラスにStoryboardを使用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);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.