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

79

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

Hfossli
источник
3
На самом деле это не вопрос.
extremeboredom 02
7
я просто хотел опубликовать свой ответ на решение, так как я не мог найти решение с помощью googling и stackoverflowing :-) ... вздох! ... так много для обмена ...
hfossli
12
совершенно нормально задавать вопросы, которые кажутся вам интересными и для которых у вас уже есть решения. Однако - приложите больше усилий, чтобы сформулировать вопрос так, чтобы он действительно походил на вопрос, который можно задать.
Bozho
1
Великолепный QA, спасибо hfossil !!!
Fattie

Ответы:

71

Обычно есть уведомления или другие наблюдаемые события, когда 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");
}];
Hfossli
источник
Если бы кадр вида был KVO-совместимым, было бы достаточно наблюдать только кадр. Другие свойства, влияющие на фрейм, также вызовут уведомление об изменении фрейма (это будет зависимый ключ). Но, как я уже сказал, все это совсем не так и может сработать только случайно.
Николай Рухе
4
Что ж, CALayer.h говорит: «CALayer реализует стандартный протокол NSKeyValueCoding для всех свойств Objective C, определенных классом и его подклассами ...» Итак, готово. :) Они заметны.
hfossli
2
Вы правы, что объекты Core Animation документированы как совместимые с KVC. Однако это ничего не говорит о соблюдении KVO. KVC и KVO - это разные вещи (хотя соответствие KVC является предпосылкой для соответствия KVO).
Николай Рухе
3
Я голосую против, чтобы привлечь внимание к проблемам, связанным с вашим подходом к использованию KVO. Я пытался объяснить, что рабочий пример не поддерживает рекомендации, как правильно что-то делать в коде. Если вы не уверены, вот еще одна ссылка на тот факт, что невозможно наблюдать произвольные свойства UIKit .
Николай Рухе
5
Передайте действительный указатель контекста. Это позволит вам отличить ваши наблюдения от наблюдений за каким-либо другим объектом. Невыполнение этого может привести к неопределенному поведению, особенно при удалении наблюдателя.
quellish
62

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


Вы должны сделать KVO на свойстве frame. «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];
        }
    }
}

 
Hfossli
источник
Не работает. Вы можете добавить наблюдателей для большинства свойств в UIView, но не для фрейма. Я получаю предупреждение компилятора о «возможно, неопределенном ключевом пути 'frame'». Если вы проигнорируете это предупреждение и все равно продолжите его выполнение, метод «ObservValueForKeyPath» никогда не будет вызван.
n13,
Что ж, у меня работает. Я также разместил здесь более позднюю и более надежную версию.
hfossli 02
3
Подтверждено, у меня тоже работает. UIView.frame правильно наблюдаем. Как ни странно, UIView.bounds - нет.
до
1
@hfossli Вы правы, вы не можете просто слепо вызвать [super] - это вызовет исключение в строке «... сообщение было получено, но не обработано», что немного обидно - вам нужно на самом деле знать, что суперкласс реализует метод до его вызова.
Ричард
3
-1: Не UIViewControllerобъявляет viewи не UIViewобъявляет frameключи, совместимые с KVO. Cocoa и Cocoa-touch не допускают произвольного наблюдения за клавишами. Все наблюдаемые ключи должны быть должным образом задокументированы. Тот факт, что он, кажется, работает, не делает его действительным (безопасным для производства) способом наблюдения за изменениями кадров в представлении.
Николай Рухе
7

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

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

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

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

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

Николай Рухе
источник
Я согласен с вами насчет KVO по свойству "frame" на UIView. Другой ответ, который я предоставил, похоже, работает отлично.
hfossli
@hfossli ReactiveCocoa построен на KVO. У него те же ограничения и проблемы. Это неправильный способ наблюдать за рамкой вида.
Николай Рухе
Да, я знаю это. Вот почему я написал, что вы можете делать обычные КВО. ReactiveCocoa использовался только для упрощения работы.
hfossli
Привет, @NikolaiRuhe - мне это пришло в голову. Если Apple не может KVO фрейм, как, черт возьми, они реализуют stackoverflow.com/a/25727788/294884 современные ограничения для представлений ?!
Fattie
@JoeBlow Apple не обязательно использовать KVO. Они контролируют выполнение всего, UIViewпоэтому могут использовать любой механизм, который они считают нужным.
Николай Рухе
4

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

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

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

Томми
источник
Вам также необходимо transform
создать
Это хорошее, многоразовое решение (дайте своему подклассу UIView метод инициализации, который принимает представление для построения ограничений, а контроллер представления сообщает об изменениях обратно, и его легко развернуть в любом месте, где вам это нужно) решение, которое все еще работает (обнаружено переопределение setBounds : наиболее эффективен в моем случае). Особенно удобно, когда вы не можете использовать подход viewDidLayoutSubviews: из-за необходимости ретрансляции элементов.
bcl
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...
      }
   });
}
Loungerdork
источник
0

Чтобы не полагаться на наблюдение 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));
}
Вернер Альтевишер
источник
За исключением того, что вам нужно будет изменить не только setFrame, но также layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ и layer.frame. Что не так с КВО? :)
hfossli 06
0

Обновлен ответ @hfossli для RxSwift и Swift 5 .

С RxSwift вы можете

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.frame)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.bounds)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.transform)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.position)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.zPosition)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.anchorPoint)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.anchorPointZ)),
              rx.observe(CGRect.self, #keyPath(UIView.layer.frame))
        ).merge().subscribe(onNext: { _ in
                 print("View probably changed its geometry")
            }).disposed(by: rx.disposeBag)

И если вы хотите знать, когда boundsвы можете внести изменения

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)

И если вы хотите знать, когда frameвы можете внести изменения

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.frame)),
              rx.observe(CGRect.self, #keyPath(UIView.frame))).merge().subscribe(onNext: { _ in
                 print("View frame changed its geometry")
            }).disposed(by: rx.disposeBag)
черная жемчужина
источник
-1

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

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

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

Сэм Клюлоу
источник
«Есть способ добиться этого без использования KVO». Что в этом контексте означает «это»? Не могли бы вы быть более конкретными.
hfossli
OP попросил способ получить определенные значения для представления во время его анимации. Они также спросили, возможно ли KVO этих свойств, что есть, но это технически не поддерживается. Я посоветовал почитать статью, в которой дано надежное решение проблемы.
Sam Clewlow
Можете быть более конкретными? Какую часть статьи вы сочли актуальной?
hfossli
@hfossli Глядя на это, я думаю, что, возможно, сформулировал этот ответ не на тот вопрос, поскольку я вижу любое упоминание об анимации! Сожалею!
Sam Clewlow
:-) без проблем. Я просто жаждал знаний.
hfossli
-4

Небезопасно использовать KVO в некоторых свойствах UIKit, например frame. По крайней мере, так утверждает Apple.

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

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];
саки
источник
4
однако @NikolaiRuhe говорит: «ReactiveCocoa построен на KVO. У него те же ограничения и проблемы. Это неправильный способ наблюдать за рамкой представления»
Fattie