Я хочу , чтобы следить за изменениями в UIView
русской frame
, bounds
или center
собственности. Как я могу использовать для этого наблюдение за ключом?
Я хочу , чтобы следить за изменениями в UIView
русской frame
, bounds
или center
собственности. Как я могу использовать для этого наблюдение за ключом?
Ответы:
Обычно есть уведомления или другие наблюдаемые события, когда 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");
}];
РЕДАКТИРОВАТЬ : Я не думаю, что это решение достаточно тщательное. Этот ответ сохранен по историческим причинам. Смотрите мой последний ответ здесь: 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];
}
}
}
UIViewController
объявляет view
и не UIView
объявляет frame
ключи, совместимые с KVO. Cocoa и Cocoa-touch не допускают произвольного наблюдения за клавишами. Все наблюдаемые ключи должны быть должным образом задокументированы. Тот факт, что он, кажется, работает, не делает его действительным (безопасным для производства) способом наблюдения за изменениями кадров в представлении.
В настоящее время невозможно использовать KVO для наблюдения за рамкой вида. Чтобы недвижимость была заметной, она должна соответствовать требованиям KVO. К сожалению, свойства фреймворка UIKit обычно не наблюдаются, как и любого другого фреймворка системы.
Из документации :
Примечание. Хотя классы инфраструктуры UIKit обычно не поддерживают KVO, вы все равно можете реализовать его в настраиваемых объектах вашего приложения, включая настраиваемые представления.
Из этого правила есть несколько исключений, например operations
свойство NSOperationQueue, но они должны быть явно задокументированы.
Даже если использование KVO для свойств представления в настоящее время может работать, я бы не рекомендовал использовать его в коде доставки. Это хрупкий подход, основанный на недокументированном поведении.
UIView
поэтому могут использовать любой механизм, который они считают нужным.
Если бы я мог внести свой вклад в беседу: как указывали другие, frame
не гарантируется, что само значение ключа-значение является наблюдаемым, как и CALayer
свойства, даже если они кажутся.
Вместо этого вы можете создать собственный UIView
подкласс, который переопределяет setFrame:
и объявляет это уведомление делегату. Установите autoresizingMask
так, чтобы в представлении было все гибкое. Настройте его так, чтобы он был полностью прозрачным и маленьким (чтобы сэкономить на CALayer
поддержке, а не на том , что это имеет большое значение) и добавьте его как часть представления, в котором вы хотите отслеживать изменения размера.
Это сработало для меня еще в iOS 4, когда мы впервые указали iOS 5 в качестве API для кодирования и, как следствие, нуждались во временной эмуляции viewDidLayoutSubviews
(хотя переопределение layoutSubviews
было более подходящим, но суть вы поняли).
transform
Как уже упоминалось, если 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...
}
});
}
Чтобы не полагаться на наблюдение 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));
}
Обновлен ответ @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, и чтобы другие нашли этот пост, я добавлю его сюда.
http://www.objc.io/issue-12/animating-custom-layer-properties.html
В этом отличном руководстве Ника Локвуда описывается, как использовать основные функции синхронизации анимации для управления чем угодно. Это намного лучше, чем использование таймера или слоя CADisplay, потому что вы можете использовать встроенные функции синхронизации или довольно легко создать свою собственную кубическую функцию Безье (см. Сопроводительную статью ( http://www.objc.io/issue-12/ анимации-объяснил.html ).
Небезопасно использовать KVO в некоторых свойствах UIKit, например frame
. По крайней мере, так утверждает Apple.
Я бы порекомендовал использовать ReactiveCocoa , это поможет вам прослушивать изменения в любом свойстве без использования KVO, очень легко начать наблюдать что-то с помощью сигналов:
[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
//do whatever you want with the new frame
}];