UIViewからUIViewControllerにアクセスしますか?


185

取得する組み込みの方法がありUIView、そのにはUIViewController?私はあなたがUIViewControllerそのUIViewviaに到達できることを知っています[self view]が、逆参照があるかどうか疑問に思っていましたか?

回答:


46

これは長い間受け入れられてきた答えなので、より良い答えで修正する必要があると感じています。

必要性に関するいくつかのコメント:

  • ビューは、ビューコントローラに直接アクセスする必要はありません。
  • 代わりに、ビューはビューコントローラから独立している必要があり、さまざまなコンテキストで機能する必要があります。
  • ビューがビューコントローラーとやり取りするビューが必要な場合、推奨される方法、およびAppleがCocoa全体で行うことは、デリゲートパターンを使用することです。

これを実装する方法の例を次に示します。

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

ビューはデリゲートとインターフェイスし(UITableViewたとえば、そうです)、ビューコントローラーに実装されているか、最終的に使用する他のクラスに実装されているかは関係ありません。

私の元の答えは次のとおりです:私はこれをお勧めしません。ViewControllerへの直接アクセスが達成される残りの答えもお勧めしません

それを行うための組み込みの方法はありません。にを追加しIBOutletUIViewこれらをInterface Builderで接続することで回避できますが、これはお勧めできません。ビューは、ビューコントローラについて認識してはなりません。代わりに、@ Phil Mが提案するようにして、デリゲートとして使用するプロトコルを作成する必要があります。


26
それは非常に悪いアドバイスです。ビューからビューコントローラーを参照しないでください
Philippe Leybaert、

6
@MattDiPasquale:はい、それは悪いデザインです。
Philippe Leybaert、2011年

23
@Phillipe Leybaert私は、ビューがコントローラーへの参照を保持せずにコントローラーでいくつかのアクションを呼び出すボタンクリックイベントの最適な設計についてのあなたの考えを知りたいと思っています。ありがとう
Jonathon Horsman、2011

3
@PhilippeLeybaert Appleのサンプルプロジェクトは、特定のAPI機能の使用法を示す傾向があります。多くの場合、トピックの簡潔なデモンストレーションを提供するために、優れたまたはスケーラブルなデザインを犠牲にしています。これに気づくまでに長い時間がかかりましたが、理にかなっていますが、残念です。多くの開発者がこれらの実用的なプロジェクトをAppleのベストデザインプラクティスへのガイドとして採用していると思います。
Benjohn

11
「あなたはこれをしてはいけない」についてのこのすべての言葉はそれだけです。ビューがそのビューコントローラーについて知りたい場合は、プログラマが決定します。限目。
Daniel Kanaan

203

Brockによって投稿された例を使用して、UIViewControllerではなくUIViewのカテゴリになるように変更し、再帰的にして、すべてのサブビューが親UIViewControllerを(うまくいけば)見つけることができるようにしました。

@interface UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController;
- (id) traverseResponderChainForUIViewController;
@end

@implementation UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController {
    // convenience function for casting and to "mask" the recursive function
    return (UIViewController *)[self traverseResponderChainForUIViewController];
}

- (id) traverseResponderChainForUIViewController {
    id nextResponder = [self nextResponder];
    if ([nextResponder isKindOfClass:[UIViewController class]]) {
        return nextResponder;
    } else if ([nextResponder isKindOfClass:[UIView class]]) {
        return [nextResponder traverseResponderChainForUIViewController];
    } else {
        return nil;
    }
}
@end

このコードを使用するには、コードを新しいクラスファイル(私は「UIKitCategories」と名付けました)に追加し、クラスデータを削除します... @interfaceをヘッダーにコピーし、@ implementationを.mファイルにコピーします。次に、プロジェクトで#import "UIKitCategories.h"を使用して、UIViewコード内で使用します。

// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];

47
そして、UIViewがそのUIViewControllerを認識できるようにする必要がある1つの理由は、モーダルビュー/ダイアログをプッシュする必要があるカスタムUIViewサブクラスがある場合です。
Phil M

すごい、サブコントローラによって作成されたカスタムポップアップを表示するためにViewControllerにアクセスする必要がありました
aryaxt

2
UIViewがモーダルビューをプッシュするのは、この悪い習慣ではありませんか?私は今これをやっているが、私はそれを行うには正しいことではないと感じ...
ヴァンデュトラン

6
Phil、カスタムビューは、ビューコントローラーがリッスンするデリゲートメソッドを呼び出し、そこからプッシュする必要があります。
Malhal 2013年

9
私は、いくつかのSOの質問が「賢い人」、学問的、受け入れられた答えをほんの数点と、実用的で汚い、ルールに反する、10倍の点で得た2番目の答えが
大好きです

114

UIViewのサブクラスですUIResponderUIResponderは、-nextResponderを返す実装でメソッドをレイアウトしますnilUIViewで説明されているようにUIResponder(何らかの理由でUIView)で説明されているように、このメソッドをオーバーライドします-nextResponder。ビューにビューコントローラがある場合、によって返されます。ビューコントローラがない場合、メソッドはスーパービューを返します。

これをプロジェクトに追加すると、ロールする準備が整います。

@interface UIView (APIFix)
- (UIViewController *)viewController;
@end

@implementation UIView (APIFix)

- (UIViewController *)viewController {
    if ([self.nextResponder isKindOfClass:UIViewController.class])
        return (UIViewController *)self.nextResponder;
    else
        return nil;
}
@end

これでUIView、ビューコントローラを返すための実用的なメソッドができました。


5
これは、受信側UIViewとの間のレスポンダーチェーンに何もない場合にのみ機能しUIViewControllerます。Phil Mの再帰をフィーチャーした答えは、進むべき道です。
Olivier

33

UIViewにカテゴリを追加する必要なく、レスポンダーチェーン全体をトラバースするためのより軽量なアプローチを提案します。

@implementation MyUIViewSubclass

- (UIViewController *)viewController {
    UIResponder *responder = self;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

@end

22

すでに与えられたいくつかの答えを組み合わせて、私はそれも私の実装に同梱しています:

@implementation UIView (AppNameAdditions)

- (UIViewController *)appName_viewController {
    /// Finds the view's view controller.

    // Take the view controller class object here and avoid sending the same message iteratively unnecessarily.
    Class vcc = [UIViewController class];

    // Traverse responder chain. Return first found view controller, which will be the view's view controller.
    UIResponder *responder = self;
    while ((responder = [responder nextResponder]))
        if ([responder isKindOfClass: vcc])
            return (UIViewController *)responder;

    // If the view controller isn't found, return nil.
    return nil;
}

@end

このカテゴリは、作成するすべてのアプリケーションに同梱されているARC対応の静的ライブラリの一部です。何度かテストされましたが、問題やリークは見つかりませんでした。

PS:関係するビューが自分のサブクラスである場合は、私が行ったようなカテゴリを使用する必要はありません。後者の場合、メソッドをサブクラスに配置するだけで問題ありません。


1
これが最良の答えです。再帰の必要はありません。このバージョンは
適切に

12

これはpgbが推奨するように技術的に解決できるとはいえ、私見では、これは設計上の欠陥です。ビューはコントローラーを認識する必要はありません。


ビューの1つが廃止され、viewXXXAppear / viewXXXDisappearメソッドの1つを呼び出す必要があることを、viewControllerにどのように通知できるかわかりません。
mahboudz

2
これがObserverパターンの背後にある考え方です。オブザーバー(この場合はビュー)は、オブザーバーを直接認識してはなりません。オブザーバーは、彼らだけが興味を持っていることをコールバックを受けるべきである。
Ushox

12

ビュー、ボタン、ラベルなどを渡して親を取得できるように、de answer を変更しましたUIViewController。これが私のコードです。

+(UIViewController *)viewController:(id)view {
    UIResponder *responder = view;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

Swift 3バージョンの編集

class func viewController(_ view: UIView) -> UIViewController {
        var responder: UIResponder? = view
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }

編集2:-スイフト拡張

extension UIView
{
    //Get Parent View Controller from any view
    func parentViewController() -> UIViewController {
        var responder: UIResponder? = self
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }
}

7

ビューがサブビューであるウィンドウのルートビューコントローラーにアクセスできることを忘れないでください。そこから、たとえばナビゲーションビューコントローラを使用していて、それに新しいビューをプッシュしたい場合は、次のようにします。

    [[[[self window] rootViewController] navigationController] pushViewController:newController animated:YES];

ただし、最初にウィンドウのrootViewControllerプロパティを適切に設定する必要があります。最初にコントローラーを作成するときにこれを行います(例:アプリのデリゲート):

-(void) applicationDidFinishLaunching:(UIApplication *)application {
    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    RootViewController *controller = [[YourRootViewController] alloc] init];
    [window setRootViewController: controller];
    navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
    [controller release];
    [window addSubview:[[self navigationController] view]];
    [window makeKeyAndVisible];
}

Appleのドキュメントによれば[[self navigationController] view]、ウィンドウの「メイン」(サブ)ビューなので、ウィンドウのrootViewControllerプロパティはnavigationController、「メイン」ビューをすぐに制御するように設定する必要があるようです。
adubr '14年

6

Ushoxを含め、これらの答えは技術的には正しいですが、承認された方法は、新しいプロトコルを実装するか、既存のプロトコルを再利用することだと思います。プロトコルは、オブザーバーを観察されたものから隔離します。それらの間にメールスロットを置くようなものです。実際には、GabrielがpushViewControllerメソッドの呼び出しを介して行うことです。viewControllerはnavigationControllerプロトコルに準拠しているため、viewは、navigationControllerにビューをプッシュするよう丁寧に要求することが適切なプロトコルであることを「認識」しています。独自のプロトコルを作成することもできますが、Gabrielの例を使用してUINavigationControllerプロトコルを再利用するだけで十分です。


6

再利用したい小さなコンポーネントがある状況に遭遇し、再利用可能なビュー自体にいくつかのコードを追加しました(実際にはを開くボタンにすぎませんPopoverController)。

これはiPadで正常に機能しUIPopoverControllerますが(存在するため、への参照は必要ありませんUIViewController)、同じコードが機能することは、presentViewControllerからを突然参照することを意味しますUIViewController。ちょっと矛盾していますよね?

前述のように、UIViewにロジックを含めることは最善の方法ではありません。しかし、必要な数行のコードを別のコントローラーでラップするのは本当に役に立たないと感じました。

いずれの場合も、UIViewに新しいプロパティを追加する迅速なソリューションを次に示します。

extension UIView {

    var viewController: UIViewController? {

        var responder: UIResponder? = self

        while responder != nil {

            if let responder = responder as? UIViewController {
                return responder
            }
            responder = responder?.nextResponder()
        }
        return nil
    }
}

5

場合によっては、ビューコントローラが誰であるかを見つけるのは「悪い」考えではないと思います。悪い考えかもしれないのは、スーパービューが変更されるのと同じように変更される可能性があるため、このコントローラーへの参照を保存することです。私の場合、レスポンダーチェーンを通過するゲッターがあります。

//.h

@property (nonatomic, readonly) UIViewController * viewController;

//.m

- (UIViewController *)viewController
{
    for (UIResponder * nextResponder = self.nextResponder;
         nextResponder;
         nextResponder = nextResponder.nextResponder)
    {
        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController *)nextResponder;
    }

    // Not found
    NSLog(@"%@ doesn't seem to have a viewController". self);
    return nil;
}

4

viewControllerを見つけるための最も単純なwhileループ。

-(UIViewController*)viewController
{
    UIResponder *nextResponder =  self;

    do
    {
        nextResponder = [nextResponder nextResponder];

        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController*)nextResponder;

    } while (nextResponder != nil);

    return nil;
}

4

スウィフト4

(他の回答よりも簡潔です)

fileprivate extension UIView {

  var firstViewController: UIViewController? {
    let firstViewController = sequence(first: self, next: { $0.next }).first(where: { $0 is UIViewController })
    return firstViewController as? UIViewController
  }

}

私は最初のビューにアクセスする必要のある私のユースケースUIViewController私は周りにラップするオブジェクトを持っている:AVPlayer/ AVPlayerViewControllerと私は、単純な提供したいshow(in view: UIView)埋め込みます方法AVPlayerViewControllerにしますview。そのために、私がアクセスする必要viewUIViewController


3

これは質問に直接回答するのではなく、質問の意図についての仮定を行います。

ビューがあり、そのビューでビューコントローラなどの別のオブジェクトのメソッドを呼び出す必要がある場合は、代わりにNSNotificationCenterを使用できます。

まず、ヘッダーファイルに通知文字列を作成します

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

ビュー呼び出しでpostNotificationName:

- (IBAction) copyString:(id)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:SLCopyStringNotification object:nil];
}

次に、ビューコントローラーでオブザーバーを追加します。私はviewDidLoadでこれを行います

- (void)viewDidLoad
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(copyString:)
                                                 name:SLCopyStringNotification
                                               object:nil];
}

これで(同じビューコントローラでも)、上記の@selectorで示したように、メソッドcopyString:を実装します。

- (IBAction) copyString:(id)sender
{
    CalculatorResult* result = (CalculatorResult*)[[PercentCalculator sharedInstance].arrayTableDS objectAtIndex:([self.viewTableResults indexPathForSelectedRow].row)];
    UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
    [gpBoard setString:result.stringResult];
}

これが正しい方法だと言っているわけではありません。最初のレスポンダーチェーンを実行するよりもクリーンなようです。このコードを使用して、UITableViewにUIMenuControllerを実装し、イベントをUIViewControllerに渡して、データで何かを実行できるようにしました。


3

それは確かに悪い考えであり、間違った設計ですが、@ Phil_Mによって提案された最良の回答のSwiftソリューションをすべて楽しむことができると確信しています。

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.nextResponder() {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder)
}

モーダルダイアログを表示したりデータを追跡したりするなど、単純なことを行うことが目的である場合、それはプロトコルの使用を正当化しません。私は個人的にこの関数をユーティリティオブジェクトに格納しています。UIResponderプロトコルを実装するあらゆるものから使用できます。

if let viewController = MyUtilityClass.firstAvailableUIViewController(self) {}

@Phil_Mへのすべてのクレジット


3

たぶんここで遅れました。しかし、この状況では、カテゴリ(汚染)は好きではありません。私はこの方法が大好きです:

#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})

3

Swiftierソリューション

extension UIView {
    var parentViewController: UIViewController? {
        for responder in sequence(first: self, next: { $0.next }) {
            if let viewController = responder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

私はこの答えが好きです。でもそれがもっと速いとは思いません。Swiftはまだ複数のパラダイムのうち、結婚したい言語を決定することはできません。この場合、より機能的なアプローチ(sequenceビット)の優れたデモンストレーションがあります。したがって、「迅速」が「より機能的」であることを意味する場合、私はそれがより迅速であることを推測します。
Travis Griggs、2018年

2

Swift 4の更新バージョン:@Phil_Mと@ paul-slmに感謝

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.next {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(responder: nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder: responder)
}

2

Swift 4バージョン

extension UIView {
var parentViewController: UIViewController? {
    var parentResponder: UIResponder? = self
    while parentResponder != nil {
        parentResponder = parentResponder!.next
        if let viewController = parentResponder as? UIViewController {
            return viewController
        }
    }
    return nil
}

使用例

 if let parent = self.view.parentViewController{

 }

2

Swift 5.2以降の 2つのソリューション:

  • 機能面の詳細
  • returnキーワードは不要になりました🤓

解決策1:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .first(where: { $0 is UIViewController })
            .flatMap { $0 as? UIViewController }
    }
}

解決策2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • このソリューションでは、最初に各レスポンダを反復処理する必要があるため、最もパフォーマンスが高いとは限りません。

1

フィルの答えへ:

インライン: id nextResponder = [self nextResponder]; self(UIView)がViewControllerのビューのサブビューでない場合、self(UIView)の階層がわかっている場合は、次も使用できます:id nextResponder = [[self superview] nextResponder];...


0

私のソリューションはおそらく一種の偽物と見なされますが、mayoneezと同様の状況があり(EAGLViewのジェスチャーに応答してビューを切り替えたい)、次のようにEAGLのビューコントローラーを取得しました。

EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;

2
次のようにコードを書き直してください:EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];
ジョナサンスターリング

1
問題はドット構文ではなく、型にあります。Objective C ClassName *objectでは、アスタリスクを使用して、作成するオブジェクトを宣言します。
adubr '14年

おっと...それは実際に私が持っていたものですが、StackOverflow HTMLウィジェットは、アスタリスクがイタリックを意味すると思ったように見えます...私はそれをコードブロックに変更しましたが、正しく表示されるようになりました。ありがとう!
gulchrider

0

観察者が観察者に知らせる必要がある場合があると思います。

UIViewControllerのUIViewが状況に応答しており、最初に親ビューコントローラーに戻るボタンを非表示にするように指示し、完了時に親ビューコントローラーにスタックからそれ自体をポップする必要があることを伝える必要がある同様の問題を確認しました。

私は成功を収めていない代表者たちとこれを試みてきました。

これが悪い考えである理由がわかりませんか?


0

別の簡単な方法は、独自のビュークラスを用意し、ビュークラスにビューコントローラーのプロパティを追加することです。通常、ビューコントローラーはビューを作成し、コントローラーがそれ自体をプロパティに設定できる場所です。基本的には、コントローラーを(少しハッキングして)探し回る代わりに、コントローラーをビューに設定します。これは簡単ですが、ビューを「制御」するコントローラーであるため、理にかなっています。


0

これをApp Storeにアップロードしない場合は、UIViewのプライベートメソッドを使用することもできます。

@interface UIView(Private)
- (UIViewController *)_viewControllerForAncestor;
@end

// Later in the code
UIViewController *vc = [myView _viewControllerForAncestor];

0
var parentViewController: UIViewController? {
    let s = sequence(first: self) { $0.next }
    return s.compactMap { $0 as? UIViewController }.first
}

このコードは質問に答えるかもしれませんが、良い答えは、コードが何をするか、どのように問題を解決するかも説明する必要があります。
BDL

0

特定のビューのコントローラーを取得するには、UIFirstResponderチェーンを使用できます。

customView.target(forAction: Selector("viewDidLoad"), withSender: nil)

-1

rootViewControllerが、AppDelegateクラスで設定されたUINavigationViewControllerである場合、

    + (UIViewController *) getNearestViewController:(Class) c {
NSArray *arrVc = [[[[UIApplication sharedApplication] keyWindow] rootViewController] childViewControllers];

for (UIViewController *v in arrVc)
{
    if ([v isKindOfClass:c])
    {
        return v;
    }
}

return nil;}

ここで、cにはビューコントローラクラスが必要です。

使用法:

     RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];

-5

道はない。

UIViewControllerポインターをUIView(または適切な継承)に渡します。私はIBを信じていないので、問題に対するIBのアプローチを手伝うことができないのは残念です。

最初のコメンターに回答するには:何ができるかが決まるため、だれがあなたに電話したかを知る必要がある場合があります。たとえば、データベースの場合、読み取りアクセスのみまたは読み取り/書き込み...


10
「私はIBを信じない」とはどういう意味ですか?私はそれを立ち上げました、それは確かに存在します。
marcc

5
あなたは、特に英語に関して、楽しさと抽象化をよりよく理解する必要があります。それは私がそれを好きではないことを意味します。
John Smith

4
私はアボカドが好きではありません。しかし、私は誰かがワカモレを作るのを手伝うことができるに違いない。それはIBで行うことができるので、「方法がない」というあなたの答えは間違いです。IBが好きかどうかは関係ありません。
Feloneous Cat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.