web-dev-qa-db-ja.com

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

UIViewからUIViewControllerに到達する組み込みの方法はありますか? UIViewControllerからUIView[self view]経由でアクセスできることは知っていますが、逆参照があるかどうか疑問に思っていましたか?

179
bertrandom

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

必要性に関するコメント:

  • ビューがView Controllerに直接アクセスする必要はありません。
  • 代わりに、ビューはView Controllerから独立しており、さまざまなコンテキストで動作できる必要があります。
  • ビューがView Controllerとのインターフェースをとる必要がある場合、推奨される方法、およびCocoaでAppleが行うことは、デリゲートパターンを使用することです。

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

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

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

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

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

45
pgb

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];
200
Phil M

UIViewUIResponderのサブクラスです。 UIResponderは、nilを返す実装でメソッド-nextResponderをレイアウトします。 UIViewは、次のようにUIResponderUIViewではなく何らかの理由で)に記載されているように、このメソッドをオーバーライドします。ビューにView Controllerがある場合、-nextResponderによって返されます。 。 View Controllerがない場合、メソッドはスーパービューを返します。

これをプロジェクトに追加すると、ロールする準備ができました。

@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には、View Controllerを返すための作業メソッドがあります。

112
Brock

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

@implementation MyUIViewSubclass

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

@end
32
de.

既に与えられたいくつかの回答を組み合わせて、実装にも同梱しています。

@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対応静的ライブラリの一部です。何回かテストされましたが、問題やリークは見つかりませんでした。

追伸:関係するビューがあなたのサブクラスである場合、私がしたようにカテゴリを使用する必要はありません。後者の場合、メソッドをサブクラスに入れるだけで準備完了です。

22

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

12
Ushox

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

Edit Swift 3 Version

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:-Swift Extention

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)!
    }
}
11
Varun Naharia

ビューがサブビューであるウィンドウのルートビューコントローラーにアクセスできることを忘れないでください。そこから、例えばNavigation View Controllerを使用して、新しいビューをPushしたい場合:

    [[[[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];
}
7
Gabriel

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

これはiPadで正常に機能しますが(UIPopoverController自体が存在するため、UIViewControllerを参照する必要はありません)、同じコードを機能させると、presentViewControllerUIViewController。ちょっと矛盾する?

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

いずれにせよ、ここにSwiftソリューションがあり、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
    }
}
6
Kevin R

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

6
PapaSmurf

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

//.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;
}
5
Rivera

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

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

    do
    {
        nextResponder = [nextResponder nextResponder];

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

    } while (nextResponder != nil);

    return nil;
}

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

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

それは確かに悪い考えであり、間違った設計ですが、@ 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
Paul Slm

スイフト4

(他の回答よりも簡潔)

fileprivate extension UIView {

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

}

最初にビューにアクセスする必要がある私の使用例UIViewControllerAVPlayer/AVPlayerViewControllerを囲むオブジェクトがあり、簡単なshow(in view: UIView)メソッドを提供したいAVPlayerViewControllerviewに埋め込みます。そのためには、viewUIViewControllerにアクセスする必要があります。

3
frouo

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

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

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

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

ビューでpostNotificationNameを呼び出します。

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

次に、View Controllerでオブザーバーを追加します。私はviewDidLoadでこれを行います

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

(同じView Controllerでも)上記の@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
Shaolo

迅速なソリューション

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

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
levin varghese

Philの答え:

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

1
sag

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

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

UIViewControllerのUIViewが状況に応答し、最初に親View Controllerに戻るボタンを非表示にするように指示し、完了時に親View Controllerにスタックからポップする必要があることを伝える必要があるという同様の問題があります。

私はこれをデリゲートで試みましたが、成功しませんでした。

なぜこれが悪い考えなのか理解できませんか?

0
user7865437

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

0
jack

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

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

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

私の解決策はおそらく偽のものと考えられますが、マヨネーズと似たような状況があり(EAGLViewのジェスチャーに応じてビューを切り替えたい)、EAGLのビューコントローラーをこのように取得しました:

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