Как я могу сделать Наблюдение значения ключа и получить обратный вызов KVO для фрейма UIView?
Я хочу следить за изменениями в UIView
"s frame
, bounds
или же center
имущество. Как я могу использовать Key-Value Observing для достижения этой цели?
8 ответов
Обычно есть уведомления или другие наблюдаемые события, где 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");
}];
РЕДАКТИРОВАТЬ: Я не думаю, что это решение достаточно тщательно. Этот ответ хранится по историческим причинам. Смотрите мой новый ответ здесь: /questions/5188046/kak-ya-mogu-sdelat-nablyudenie-znacheniya-klyucha-i-poluchit-obratnyij-vyizov-kvo-dlya-frejma-uiview/5188067#5188067
Вы должны сделать 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];
}
}
}
В настоящее время невозможно использовать KVO для наблюдения за рамкой вида. Свойства должны быть KVO-совместимыми, чтобы их можно было наблюдать. К сожалению, свойства фреймворка UIKit, как правило, не наблюдаемы, как с любой другой системной фреймворк.
Из документации:
Примечание. Хотя классы инфраструктуры UIKit обычно не поддерживают KVO, вы все равно можете реализовать ее в пользовательских объектах вашего приложения, в том числе в пользовательских представлениях.
Есть несколько исключений из этого правила, например, NSOperationQueue operations
собственности, но они должны быть четко задокументированы.
Даже если в настоящее время может использоваться KVO для свойств представления, я бы не рекомендовал использовать его в коде доставки. Это хрупкий подход, основанный на недокументированном поведении.
Если бы я мог внести свой вклад в разговор: как уже отмечали другие, frame
не гарантируется, что сам ключ-значение можно наблюдать, и CALayer
свойства, даже если они кажутся.
Вместо этого вы можете создать кастом UIView
подкласс, который переопределяет setFrame:
и объявляет эту квитанцию делегату. Установить autoresizingMask
так что представление имеет гибкое все. Настройте его как полностью прозрачный и небольшой (чтобы сэкономить на CALayer
бэк, не то, чтобы это имело большое значение) и добавьте его в качестве подпредставления представления, на котором вы хотите наблюдать изменения размера.
Это успешно работало для меня еще под iOS 4, когда мы впервые указали iOS 5 в качестве API для кода и, как следствие, нуждались во временной эмуляции viewDidLayoutSubviews
(хотя это переопределение layoutSubviews
было более уместно, но вы поняли).
Обновлен ответ @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)
Как уже упоминалось, если 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...
}
});
}
Чтобы не полагаться на наблюдение за КВО, вы можете выполнить метод 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));
}
Есть способ добиться этого без использования KVO вообще, и ради других, которые найдут этот пост, я добавлю его сюда.
http://www.objc.io/issue-12/animating-custom-layer-properties.html
Этот превосходный урок Ника Локвуда описывает, как использовать основные функции синхронизации анимации, чтобы управлять чем угодно. Это намного лучше, чем использование таймера или слоя CADisplay, потому что вы можете использовать встроенные функции синхронизации или довольно легко создать свою собственную функцию кубического Безье (см. Сопутствующую статью ( http://www.objc.io/issue-12/animations-explained.html).
Не безопасно использовать KVO в некоторых свойствах UIKit, таких как frame
, Или, по крайней мере, так говорит Apple.
Я бы порекомендовал использовать ReactiveCocoa, это поможет вам прослушивать изменения в любом свойстве без использования KVO, очень легко начать наблюдать что-то с помощью сигналов:
[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
//do whatever you want with the new frame
}];