UIView
からUIViewController
に到達する組み込みの方法はありますか? UIViewController
からUIView
に[self view]
経由でアクセスできることは知っていますが、逆参照があるかどうか疑問に思っていましたか?
これは長い間受け入れられてきた回答であるため、より良い回答で修正する必要があると感じています。
必要性に関するコメント:
実装方法の例を次に示します。
@protocol MyViewDelegate < NSObject >
- (void)viewActionHappened;
@end
@interface MyView : UIView
@property (nonatomic, assign) MyViewDelegate delegate;
@end
@interface MyViewController < MyViewDelegate >
@end
ビューはそのデリゲートとインターフェースし(UITableView
がそうするように)、View Controllerに実装されているか、最終的に使用する他のクラスに実装されているかは関係ありません。
私の元の答えは次のとおりです:これはお勧めしません、View Controllerへの直接アクセスが達成される残りの答えも
組み込みの方法はありません。 IBOutlet
にUIView
を追加してInterface Builderで接続することで回避できますが、これはお勧めしません。ビューは、View Controllerについて認識してはなりません。代わりに、@ Phil Mが提案するようにして、デリゲートとして使用するプロトコルを作成する必要があります。
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];
UIView
はUIResponder
のサブクラスです。 UIResponder
は、nil
を返す実装でメソッド-nextResponder
をレイアウトします。 UIView
は、次のようにUIResponder
(UIView
ではなく何らかの理由で)に記載されているように、このメソッドをオーバーライドします。ビューに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を返すための作業メソッドがあります。
UIViewにカテゴリを追加することなく、レスポンダーチェーン全体を移動するためのより軽量なアプローチを提案します。
@implementation MyUIViewSubclass
- (UIViewController *)viewController {
UIResponder *responder = self;
while (![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
if (nil == responder) {
break;
}
}
return (UIViewController *)responder;
}
@end
既に与えられたいくつかの回答を組み合わせて、実装にも同梱しています。
@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対応静的ライブラリの一部です。何回かテストされましたが、問題やリークは見つかりませんでした。
追伸:関係するビューがあなたのサブクラスである場合、私がしたようにカテゴリを使用する必要はありません。後者の場合、メソッドをサブクラスに入れるだけで準備完了です。
pgbが推奨しているように、これは技術的に解決できますが、これは設計上の欠陥です。ビューはコントローラーを認識する必要はありません。
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)!
}
}
ビューがサブビューであるウィンドウのルートビューコントローラーにアクセスできることを忘れないでください。そこから、例えば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];
}
再利用したい小さなコンポーネントがある状況に出くわし、再利用可能なビュー自体にいくつかのコードを追加しました(PopoverController
を開くボタンにすぎません)。
これはiPadで正常に機能しますが(UIPopoverController
自体が存在するため、UIViewController
を参照する必要はありません)、同じコードを機能させると、presentViewController
をUIViewController
。ちょっと矛盾する?
前述のように、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
}
}
Ushoxを含むこれらの答えは技術的には正しいですが、承認済みの方法は、新しいプロトコルを実装するか、既存のプロトコルを再利用することです。プロトコルは、オブザーバーを監視対象から隔離し、メールスロットをその間に配置するようなものです。実際、それはGabrielがpushViewControllerメソッドの呼び出しを介して行うことです。 viewControllerはnavigationControllerプロトコルに準拠しているため、ビューは、navigationControllerにビューをプッシュするよう丁寧に要求することが適切なプロトコルであることを「認識」します。独自のプロトコルを作成できますが、Gabrielの例を使用してUINavigationControllerプロトコルを再利用するだけで十分です。
場合によっては誰が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;
}
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; \
})
それは確かに悪い考えであり、間違った設計ですが、@ 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へのすべてのクレジット
(他の回答よりも簡潔)
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
に埋め込みます。そのためには、view
のUIViewController
にアクセスする必要があります。
これは質問に直接答えるのではなく、質問の意図について仮定を立てます。
ビューがあり、そのビューで別のオブジェクト(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に戻して、データで何かを実行できるようにしました。
迅速なソリューション
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 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{
}
Philの答え:
行:id nextResponder = [self nextResponder];
self(UIView)がViewControllerのビューのサブビューでない場合、self(UIView)の階層がわかっている場合は、id nextResponder = [[self superview] nextResponder];
...も使用できます。
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)
}
観察者が観察者に知らせる必要がある場合があると思います。
UIViewControllerのUIViewが状況に応答し、最初に親View Controllerに戻るボタンを非表示にするように指示し、完了時に親View Controllerにスタックからポップする必要があることを伝える必要があるという同様の問題があります。
私はこれをデリゲートで試みましたが、成功しませんでした。
なぜこれが悪い考えなのか理解できませんか?
別の簡単な方法は、独自のビュークラスを作成し、View Controllerのプロパティをビュークラスに追加することです。通常、View Controllerはビューを作成します。これは、コントローラが自身をプロパティに設定できる場所です。基本的には、コントローラを(少しハッキングして)検索する代わりに、コントローラをビューに設定します。これは簡単ですが、ビューを「制御する」コントローラであるため、理にかなっています。
これをApp Storeにアップロードしない場合は、UIViewのプライベートメソッドを使用することもできます。
@interface UIView(Private)
- (UIViewController *)_viewControllerForAncestor;
@end
// Later in the code
UIViewController *vc = [myView _viewControllerForAncestor];
私の解決策はおそらく偽のものと考えられますが、マヨネーズと似たような状況があり(EAGLViewのジェスチャーに応じてビューを切り替えたい)、EAGLのビューコントローラーをこのように取得しました:
EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;