web-dev-qa-db-ja.com

キー値の監視を行い、UIViewのフレームでKVOコールバックを取得するにはどうすればよいですか?

UIViewframebounds、またはcenterプロパティの変更を監視したい。 Key-Value Observingを使用してこれを実現するにはどうすればよいですか?

75
hfossli

通常、KVOがサポートされていない通知またはその他の監視可能なイベントがあります。ドキュメントには 'no'と書かれていますが、UIViewを支えるCALayerを観察することは、表面上は安全です。 CAVOを観察することは、KVOと適切なアクセサ(ivar操作の代わりに)を広範囲に使用するため、実際に機能します。今後の動作を保証するものではありません。

とにかく、ビューのフレームは他のプロパティの単なる産物です。したがって、それらを観察する必要があります。

[self.view addObserver:self forKeyPath:@"frame" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"bounds" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"transform" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"position" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"zPosition" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"anchorPoint" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"anchorPointZ" options:0 context:NULL];
[self.view.layer addObserver:self forKeyPath:@"frame" options:0 context:NULL];

完全な例をご覧ください https://Gist.github.com/hfossli/723462

注:これはドキュメントでサポートされているとは言われていませんが、現在のところ、これまでのすべてのiOSバージョン(現在はiOS 2-> iOS 11)で動作します

注:最終値に落ち着く前に、複数のコールバックを受け取ることに注意してください。たとえば、ビューまたはレイヤーのフレームを変更すると、レイヤーはpositionおよびboundsを(この順序で)変更します。


ReactiveCocoaでできること

RACSignal *signal = [RACSignal merge:@[
  RACObserve(view, frame),
  RACObserve(view, layer.bounds),
  RACObserve(view, layer.transform),
  RACObserve(view, layer.position),
  RACObserve(view, layer.zPosition),
  RACObserve(view, layer.anchorPoint),
  RACObserve(view, layer.anchorPointZ),
  RACObserve(view, layer.frame),
  ]];

[signal subscribeNext:^(id x) {
    NSLog(@"View probably changed its geometry");
}];

boundsがいつ変更されたかを知りたいだけなら

@weakify(view);
RACSignal *boundsChanged = [[signal map:^id(id value) {
    @strongify(view);
    return [NSValue valueWithCGRect:view.bounds];
}] distinctUntilChanged];

[boundsChanged subscribeNext:^(id ignore) {
    NSLog(@"View bounds changed its geometry");
}];

frameがいつ変更されたかを知りたいだけなら

@weakify(view);
RACSignal *frameChanged = [[signal map:^id(id value) {
    @strongify(view);
    return [NSValue valueWithCGRect:view.frame];
}] distinctUntilChanged];

[frameChanged subscribeNext:^(id ignore) {
    NSLog(@"View frame changed its geometry");
}];
64
hfossli

[〜#〜] edit [〜#〜]:この解決策が十分に徹底しているとは思わない。この答えは歴史的な理由で保持されています。ここで私の最新の回答を参照してください: https://stackoverflow.com/a/19687115/202451


フレームプロパティでKVOを実行する必要があります。この場合、「自己」はUIViewControllerです。

オブザーバーの追加(通常はviewDidLoadで行われます):

[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];

オブザーバーの削除(通常はdeallocまたはviewDidDisappear:で行われます):

[self removeObserver:self forKeyPath:@"view.frame"];

変更に関する情報を取得する

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"view.frame"]) {
        CGRect oldFrame = CGRectNull;
        CGRect newFrame = CGRectNull;
        if([change objectForKey:@"old"] != [NSNull null]) {
            oldFrame = [[change objectForKey:@"old"] CGRectValue];
        }
        if([object valueForKeyPath:keyPath] != [NSNull null]) {
            newFrame = [[object valueForKeyPath:keyPath] CGRectValue];
        }
    }
}

 
62
hfossli

現在、ビューのフレームを観察するためにKVOを使用することはできません。プロパティを確認するには、KVO準拠である必要があります。悲しいことに、UIKitフレームワークのプロパティは、他のシステムフレームワークのように、通常は観察できません。

ドキュメント から:

注:UIKitフレームワークのクラスは一般にKVOをサポートしていませんが、カスタムビューを含むアプリケーションのカスタムオブジェクトにKVOを実装できます。

NSOperationQueueのoperationsプロパティなど、このルールにはいくつかの例外がありますが、明示的に文書化する必要があります。

ビューのプロパティでKVOを使用しても現在は機能する場合でも、出荷コードで使用することはお勧めしません。これは脆弱なアプローチであり、文書化されていない動作に依存しています。

7
Nikolai Ruhe

私が会話に貢献する可能性がある場合:他の人が指摘しているように、frameはそれ自体がキー値で観測可能であることは保証されず、CALayerプロパティーであるようにも見えません。

代わりにできることは、setFrame:をオーバーライドし、その受信をデリゲートに通知するカスタムUIViewサブクラスを作成することです。 autoresizingMaskを設定して、ビューにすべての柔軟性を持たせます。完全に透明で小さく(CALayerバッキングのコストを節約するために、それほど重要ではないように)構成し、サイズの変更を監視するビューのサブビューとして追加します。

これは、iOS 5でコード化するAPIとして最初にiOS 5を指定したときに戻ってきたため、viewDidLayoutSubviewsの一時的なエミュレーションが必要でした(layoutSubviewsより適切でしたが、あなたはポイントを得る)。

4
Tommy

KVOをまったく使用せずにこれを達成する方法があり、他の人がこの投稿を見つけるために、ここに追加します。

http://www.objc.io/issue-12/animating-custom-layer-properties.html

Nick Lockwoodによるこの優れたチュートリアルでは、コアアニメーションのタイミング関数を使用して何かを駆動する方法について説明します。組み込みのタイミング関数を使用するか、独自のキュービックベジェ関数を簡単に作成できるため、タイマーまたはCADisplayレイヤーを使用するよりもはるかに優れています(添付の記事( http://www.objc.io/ issue-12/animations-explained.html )。

0
Sam Clewlow

KVOの監視に依存しないようにするには、次のようにメソッドのスウィズリングを実行できます。

@interface UIView(SetFrameNotification)

extern NSString * const UIViewDidChangeFrameNotification;

@end

@implementation UIView(SetFrameNotification)

#pragma mark - Method swizzling setFrame

static IMP originalSetFrameImp = NULL;
NSString * const UIViewDidChangeFrameNotification = @"UIViewDidChangeFrameNotification";

static void __UIViewSetFrame(id self, SEL _cmd, CGRect frame) {
    ((void(*)(id,SEL, CGRect))originalSetFrameImp)(self, _cmd, frame);
    [[NSNotificationCenter defaultCenter] postNotificationName:UIViewDidChangeFrameNotification object:self];
}

+ (void)load {
    [self swizzleSetFrameMethod];
}

+ (void)swizzleSetFrameMethod {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        IMP swizzleImp = (IMP)__UIViewSetFrame;
        Method method = class_getInstanceMethod([UIView class],
                @selector(setFrame:));
        originalSetFrameImp = method_setImplementation(method, swizzleImp);
    });
}

@end

次に、アプリケーションコードでUIViewのフレームの変更を確認します。

- (void)observeFrameChangeForView:(UIView *)view {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidChangeFrameNotification:) name:UIViewDidChangeFrameNotification object:view];
}

- (void)viewDidChangeFrameNotification:(NSNotification *)notification {
    UIView *v = (UIView *)notification.object;
    NSLog(@"View '%@' did change frame to %@", v, NSStringFromCGRect(v.frame));
}
0

前述のように、KVOが機能せず、自分で制御できる独自のビューを観察したい場合は、setFrameまたはsetBoundsをオーバーライドするカスタムビューを作成できます。注意点は、呼び出しの時点で、最終的な目的のフレーム値が利用できない場合があることです。したがって、次のメインスレッドループにGCD呼び出しを追加して、値を再度チェックしました。

-(void)setFrame:(CGRect)frame
{
   NSLog(@"setFrame: %@", NSStringFromCGRect(frame));
   [super setFrame:frame];
   // final value is available in the next main thread cycle
   __weak PositionLabel *ws = self;
   dispatch_async(dispatch_get_main_queue(), ^(void) {
      if (ws && ws.superview)
      {
         NSLog(@"setFrame2: %@", NSStringFromCGRect(ws.frame));
         // do whatever you need to...
      }
   });
}
0
loungerdork