Как я могу выполнить наблюдение за ключевыми значениями и получить обратный вызов KVO на кадре UIView?

Я хочу посмотреть изменения в свойствах UIView frame, bounds или center. Как я могу использовать Key-Value Observing для достижения этого?

Ответ 1

Обычно есть уведомления или другие наблюдаемые события, когда KVO не поддерживается. Несмотря на то, что в документах указано "нет", якобы безопасно наблюдать за CALayer, поддерживающим UIView. Наблюдение за CALayer работает на практике из-за его широкого использования 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/7234623

ПРИМЕЧАНИЕ. Это не поддерживается в документах, но работает на сегодняшний день со всеми версиями 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");
}];

Ответ 2

EDIT. Я не думаю, что это решение достаточно полно. Этот ответ сохраняется по историческим причинам. См. Мой новейший ответ здесь: fooobar.com/questions/81342/...


Вы должны сделать KVO в кадровой собственности. "self" в этом случае является 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];
        }
    }
}

 

Ответ 3

В настоящее время невозможно использовать KVO для наблюдения за рамкой представления. Свойства должны быть совместимыми с KVO. К сожалению, свойства структуры UIKit обычно не наблюдаются, как и в любой другой системной структуре.

Из documentation:

Примечание. Хотя классы структуры UIKit обычно не поддерживают KVO, вы все равно можете реализовать его в пользовательских объектах вашего приложения, включая пользовательские представления.

Есть несколько исключений из этого правила, например NSOperationQueue operations, но они должны быть явно задокументированы.

Даже если использование KVO в свойствах представления в настоящее время может работать, я бы не рекомендовал использовать его в транспортном коде. Это хрупкий подход и полагается на недокументированное поведение.

Ответ 4

Если бы я мог внести свой вклад в разговор: как указывали другие, frame не гарантируется, что он сам по себе является наблюдаемым ключом, и не являются свойствами CALayer, даже если они выглядят.

Вместо этого вы можете создать собственный подкласс UIView, который переопределяет setFrame: и объявляет о получении к делегату. Установите autoresizingMask, чтобы представление имело гибкое все. Настройте его полностью прозрачным и небольшим (чтобы сэкономить затраты на поддержку CALayer, а не на то, что это имеет большое значение) и добавьте его в качестве подсмотра представления, на которое вы хотите посмотреть изменения размера.

Это сработало для меня снова в iOS 4, когда мы сначала указали iOS 5 как API для кодирования и, как результат, потребовали временную эмуляцию viewDidLayoutSubviews (хотя переопределение layoutSubviews было более уместным, но вы получите точку).

Ответ 5

Как уже упоминалось, если 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...
      }
   });
}

Ответ 6

Существует способ достичь этого, не используя KVO вообще, и ради других, кто нашел этот пост, я добавлю его здесь.

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

Этот отличный учебник от Nick Lockwood описывает, как использовать основные функции синхронизации анимации для вождения чего-либо. Это намного превосходит использование таймера или уровня CADisplay, потому что вы можете использовать встроенные функции синхронизации или довольно легко создать свою собственную функцию кубического безье (см. Сопроводительную статью (http://www.objc.io/issue-12/animations-explained.html).

Ответ 7

Чтобы не полагаться на наблюдение KVO, вы можете выполнять метод swizzling следующим образом:

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

Ответ 8

Нельзя использовать KVO в некоторых свойствах UIKit, таких как frame. Или, по крайней мере, то, что говорит Apple.

Я бы рекомендовал использовать ReactiveCocoa, это поможет вам слушать изменения в любом свойстве без использования KVO, очень легко начать наблюдение что-то с помощью сигналов:

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];