Как я могу сделать наблюдение значения ключа и получить обратный вызов KVO для фрейма UIView? - PullRequest
73 голосов
/ 02 февраля 2011

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

Ответы [ 8 ]

63 голосов
/ 30 октября 2013

Обычно есть уведомления или другие наблюдаемые события, где 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");
}];
61 голосов
/ 02 февраля 2011

РЕДАКТИРОВАТЬ : я не думаю, что это решение является достаточно тщательным. Этот ответ хранится по историческим причинам. Смотрите мой самый новый ответ здесь: https://stackoverflow.com/a/19687115/202451


Вы должны выполнить KVO для свойства frame. «Я» в этом случае 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];
        }
    }
}

 
7 голосов
/ 31 октября 2013

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

Из документации :

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

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

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

4 голосов
/ 02 апреля 2014

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

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

0 голосов
/ 06 сентября 2016

Чтобы не полагаться на наблюдение 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));
}
0 голосов
/ 04 ноября 2014

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

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

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

).
0 голосов
/ 30 октября 2014

Как уже упоминалось, если 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 голосов
/ 21 сентября 2014

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

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

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