Як я можу здійснити спостереження за ключовими значеннями та отримати зворотний виклик KVO у кадрі UIView?


79

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


3
Це насправді не питання.
extremeboredom

7
я просто хотів опублікувати свою відповідь на рішення, оскільки я не міг знайти рішення,
гуглюючи та перебігаючи stackoverflowing

12
цілком нормально ставити запитання, які вам здаються цікавими і для яких у вас вже є рішення. Однак - докладіть більше зусиль, щоб сформулювати питання так, щоб воно справді звучало як питання, яке можна було б задати.
Божо

1
Фантастичний контроль якості, дякую 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");
}];

Якщо рама відкривається вид в був KVO поступливим, воно було б достатньо , щоб спостерігати тільки кадр. Інші властивості, які впливають на кадр, також ініціюватимуть повідомлення про зміну на кадрі (це був би залежний ключ). Але, як я вже сказав, все це просто не так, і може спрацювати лише випадково.
Nikolai Ruhe

4
Ну CALayer.h каже: "CALayer реалізує стандартний протокол NSKeyValueCoding для всіх властивостей Objective C, визначених класом та його підкласами ..." Ось і ви. :) Вони спостерігаються.
hfossli

2
Ви маєте рацію, що об’єкти Core Animation задокументовані як сумісні з KVC. Однак це нічого не говорить про відповідність KVO. KVC та KVO - це просто різні речі (хоча відповідність KVC є необхідною умовою дотримання KVO).
Микола Руе

3
Я голосую проти, щоб звернути увагу на проблеми, пов'язані з вашим підходом до використання KVO. Я намагався пояснити, що робочий приклад не підтримує рекомендації щодо того, як щось робити правильно в коді. Якщо ви не впевнені, ось ще одне посилання на той факт, що неможливо спостерігати довільні властивості UIKit .
Микола Руе

5
Будь ласка, передайте дійсний покажчик контексту. Це дозволяє розрізнити ваші спостереження та спостереження за якимсь іншим об’єктом. Якщо цього не зробити, це може призвести до невизначеної поведінки, зокрема до відсторонення спостерігача.
втамований

62

EDIT : Я не думаю, що це рішення є достатньо ґрунтовним. Ця відповідь зберігається з історичних причин. Дивіться мою найновішу відповідь тут: https://stackoverflow.com/a/19687115/202451


Вам потрібно зробити 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];
        }
    }
}

 

Не працює. Ви можете додати спостерігачів для більшості властивостей в UIView, але не для кадру. Я отримую попередження компілятора про "можливо невизначений шлях ключа" фрейм "". Ігноруючи це попередження і роблячи його в будь-якому випадку, метод obserValueForKeyPath ніколи не викликається.
n13

Ну, працює у мене. Зараз я також розмістив тут пізнішу та надійнішу версію.
hfossli 02.03.12

3
Підтверджено, працює і для мене. UIView.frame можна спостерігати належним чином. Досить смішно, UIView.bounds ні.
До

1
@hfossli Ви маєте рацію, що ви не можете просто наосліп зателефонувати [супер] - це викличе виняток на зразок "... повідомлення отримано, але не оброблено", що трохи шкода - ви повинні насправді знаємо, що суперклас реалізує метод до його виклику.
Річард

3
-1: Ні UIViewControllerзаявляє, viewні UIViewзаявляє, frameщо відповідає ключам KVO. Какао та какао-дотик не дозволяють довільно спостерігати за клавішами. Усі спостережувані ключі повинні бути належним чином задокументовані. Той факт, що це, здається, працює, не вважає цей дійсний (безпечний для виробництва) спосіб спостереження за змінами кадрів у поданні.
Микола Руе

7

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

З документації :

Примітка: Хоча класи фреймворку UIKit, як правило, не підтримують KVO, ви все одно можете реалізувати його у спеціальних об'єктах вашої програми, включаючи власні подання.

Є кілька винятків із цього правила, наприклад властивість NSOperationQueue, operationsале вони повинні бути чітко задокументовані.

Навіть якщо використання KVO у властивостях представлення в даний час може працювати, я б не рекомендував використовувати його в коді доставки. Це тендітний підхід і спирається на поведінку без документів.


Я погоджуюся з вами щодо KVO щодо властивості "фрейм" на UIView. Інша відповідь, яку я надав, здається, працює абсолютно ідеально.
hfossli

@hfossli ReactiveCocoa побудований на KVO. Він має ті самі обмеження та проблеми. Це не належний спосіб спостерігати за кадром виду.
Nikolai Ruhe

Так, я це знаю. Ось чому я писав, що ти можеш робити звичайні KVO. Використання реактивного какао було лише для того, щоб підтримувати його простотою.
hfossli

Привіт @NikolaiRuhe - мені це просто спало на думку. Якщо Apple не може KVO кадру, як, чорт візьми, вони реалізують stackoverflow.com/a/25727788/294884 сучасні обмеження для переглядів ?!
Fattie

@JoeBlow Apple не повинна використовувати KVO. Вони контролюють виконання всіх, UIViewщоб вони могли використовувати будь-який механізм, який вважають за потрібне.
Nikolai Ruhe

4

Якби я міг взяти участь у розмові: як зазначали інші, frameне гарантується, що сам по собі можна буде спостерігати ключ-значення, як і CALayerвластивості, навіть якщо вони видаються.

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

Це успішно спрацювало для мене ще під iOS 4, коли ми вперше вказували iOS 5 як API для кодування, і, як результат, потрібна була тимчасова емуляція viewDidLayoutSubviews(хоча, перевизначення layoutSubviewsбуло більш доречним, але ви розумієте).


Вам також потрібно буде підкласирувати шар, щоб отримати transformтощо
hfossli

Це приємне багаторазове використання (надайте своєму підкласу UIView метод init, який використовує подання для побудови обмежень, а контролер подання повідомляє про зміни назад, і його легко розгорнути в будь-якому місці, де вам це потрібно), рішення, яке все ще працює (знайдено переважні 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...
      }
   });
}

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. Що не так із KVO? :)
hfossli

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

ОП запитав спосіб отримати конкретні значення для подання під час його анімації. Вони також запитали, чи можна 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,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.