Я хочу посмотреть изменения в свойствах UIView
frame
, bounds
или center
. Как я могу использовать Key-Value Observing для достижения этого?
Как я могу выполнить наблюдение за ключевыми значениями и получить обратный вызов KVO на кадре UIView?
Ответ 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
}];