web-dev-qa-db-ja.com

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

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

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

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

そのようなことはできますか?
BasicViewControllerのストーリーボードをインスタンス化するだけで、サブクラスSpecificViewController1およびSpecificViewController2

ありがとう。

108
verdy

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

デフォルトでは、ストーリーボード定義で指定されているView Controllerで初期化されます。

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

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

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

  • uIViewにUI要素を保存します-xibファイルに格納し、基本クラスからインスタンス化し、メインビュー(通常はself.view)にサブビューとして追加します。次に、ストーリーボードレイアウトを使用します。基本的には、ストーリーボード内の場所を保持する空のビューコントローラーを使用しますが、正しいビューコントローラーサブクラスが割り当てられます。それらはベースから継承するため、そのビューを取得します。

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

Appleがあなたが提案したことをする方法を思いついたのなら、それは素晴らしいことですが、グラフィック要素をコントローラーのサブクラスに事前リンクさせるという問題は依然として問題になります。

良いお年をお迎えください!!よくなって

57
CocoaEv

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

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
    }
}
44
Jiří Zahálka

受け入れられた答えが述べているように、ストーリーボードでできるようには見えません。

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

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

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

13
kgaidis

ストーリーボードでカスタムView Controllerのさまざまなサブクラスをインスタンス化することもできますが、これには少し非正統的な手法が含まれます:View Controllerのallocメソッドをオーバーライドする。カスタムView Controllerが作成されると、実際にオーバーライドされたallocメソッドは、サブクラスでallocを実行した結果を返します。

私はさまざまなシナリオでテストしてエラーを受け取っていませんが、より複雑なセットアップに対処できることを保証することはできませんが、答えを前書きする必要があります(しかし、動作しない理由はわかりません) 。また、この方法を使用してアプリを提出していないため、Appleのレビュープロセスによって拒否される可能性が外部にあります(ただし、その理由はわかりません)。

デモンストレーションのために、UIViewControllerというTestViewControllerのサブクラスがあり、UILabel IBOutletとIBActionがあります。ストーリーボードで、View Controllerを追加し、そのクラスをTestViewControllerに修正し、IBOutletをUILabelに、IBActionをUIButtonに接続しました。 TestViewControllerを、前のviewControllerのUIButtonによってトリガーされるモーダルセグエとして提示します。

Storyboard image

どのクラスをインスタンス化するかを制御するために、使用するサブクラスを取得/設定する静的変数と関連するクラスメソッドを追加しました(インスタンス化するサブクラスを決定する他の方法を採用できると思います)。

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の2つのサブクラス、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自体をインスタンス化したい場合もあれば、RedTestViewControllerまたはGreenTestViewControllerをインスタンス化したい場合もあります。前のView Controllerでは、次のようにランダムにこれを行います。

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への参照は、この機能をテストするためのものです。

9
pbasdf

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

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

のような:

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
6
莫振华

厳密にはサブクラスではありませんが、次のことができます。

  1. option-ドキュメントアウトラインで基本クラスビューコントローラーをドラッグしてコピーを作成します
  2. 新しいView Controllerのコピーをストーリーボード上の別の場所に移動します
  3. ClassをIdentity InspectorのサブクラスView Controllerに変更します

ViewControllerWhiskeyViewControllerでサブクラス化した、私が書いた Bloc チュートリアルの例を以下に示します。

animation of the above three steps

これにより、ストーリーボードにView Controllerサブクラスのサブクラスを作成できます。その後、instantiateViewControllerWithIdentifier:を使用して特定のサブクラスを作成できます。

このアプローチには少し柔軟性がありません。ストーリーボード内でのベースクラスコントローラーへのその後の変更は、サブクラスに反映されません。サブクラスのlotがある場合は、他のソリューションのいずれかを使用した方が良いかもしれませんが、これはピンチになります。

5
Aaron Brager

ストーリーボードにあまり依存していない場合は、コントローラー用に別の.xibファイルを作成できます。

適切なファイルの所有者とアウトレットをMainViewControllerに設定し、Main 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はその親のペン先を再利用することができます:

class ChildViewController: MainViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .blue
    }
}
2
nitrous

Objc_setclassメソッドは、childvcのインスタンスを作成しません。しかし、childvcから飛び出している間、childvcのdeinitが呼び出されています。 childvcに個別に割り当てられたメモリがないため、アプリがクラッシュします。 Basecontrollerにはinstanceがありますが、子vcにはありません。

2
user8044830

特に nickgzzjr および JiříZahálka に基づいて、CocoaBobの2番目のコメントの下にコメントを追加します。ストーリーボード名と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
    }

強制的な展開を回避するためにオプションが追加されましたが、メソッドは正しいオブジェクトを返します。

0
Adam Tucholski

あちこちから答えをとって、このきちんとした解決策を思いつきました。

この関数を使用して親View Controllerを作成します。

class ParentViewController: UIViewController {


    func convert<T: ParentViewController>(to _: T.Type) {

        object_setClass(self, T.self)

    }

}

これにより、コンパイラは、子View Controllerが親View Controllerから継承することを保証できます。

次に、サブクラスを使用してこのコントローラにセグエを行いたいときはいつでもできます:

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

}

クールな部分は、ストーリーボードへの参照を自分自身に追加してから、「次の」子View Controllerを呼び出し続けられることです。

0
nickgzzjr

おそらく最も柔軟な方法は、再利用可能なビューを使用することです。

(別のXIBファイルまたはContainer viewそして、ストーリーボードの各サブクラスビューコントローラーシーンに追加します)

0
DanSkeel