Chcę obserwować zmiany w UIView
„s frame
, bounds
lub center
mienia. Jak mogę wykorzystać obserwację klucz-wartość, aby to osiągnąć?
Chcę obserwować zmiany w UIView
„s frame
, bounds
lub center
mienia. Jak mogę wykorzystać obserwację klucz-wartość, aby to osiągnąć?
Odpowiedzi:
Zwykle pojawiają się powiadomienia lub inne obserwowalne zdarzenia, w których KVO nie jest obsługiwane. Mimo że doktorzy mówią „nie” , pozornie bezpieczne jest obserwowanie CALayera wspierającego UIView. Obserwowanie CALayera działa w praktyce ze względu na jego szerokie wykorzystanie KVO i odpowiednich akcesorów (zamiast manipulacji ivar). Nie ma gwarancji, że będzie działać dalej.
Zresztą rama widoku jest po prostu produktem innych właściwości. Dlatego musimy przestrzegać tych:
[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];
Zobacz pełny przykład tutaj https://gist.github.com/hfossli/7234623
UWAGA: Mówi się, że nie jest to obsługiwane w dokumentach, ale działa od dzisiaj ze wszystkimi wersjami iOS do tej pory (obecnie iOS 2 -> iOS 11)
UWAGA: pamiętaj, że otrzymasz wiele wywołań zwrotnych, zanim osiągnie ostateczną wartość. Na przykład zmiana klatki widoku lub warstwy spowoduje zmianę warstwy position
i bounds
(w tej kolejności).
Z ReactiveCocoa możesz to zrobić
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");
}];
A jeśli chcesz tylko wiedzieć, kiedy bounds
możesz dokonać zmian
@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");
}];
A jeśli chcesz tylko wiedzieć, kiedy frame
możesz dokonać zmian
@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");
}];
EDYCJA : Myślę, że to rozwiązanie nie jest wystarczająco dokładne. Ta odpowiedź jest zachowana ze względów historycznych. Zobacz moją najnowszą odpowiedź tutaj: https://stackoverflow.com/a/19687115/202451
Musisz zrobić KVO na właściwości ramki. „self” jest w tym przypadku kontrolerem UIViewController.
dodanie obserwatora (zwykle robione w viewDidLoad):
[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];
usuwanie obserwatora (zwykle wykonywane w dealloc lub viewDidDisappear :):
[self removeObserver:self forKeyPath:@"view.frame"];
Uzyskanie informacji o zmianie
- (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
deklaruje, view
ani nie UIView
deklaruje frame
zgodności kluczy z KVO. Cocoa i Cocoa-touch nie pozwalają na dowolne obserwowanie klawiszy. Wszystkie obserwowalne klucze muszą być odpowiednio udokumentowane. Fakt, że wygląda na to, że działa, nie oznacza, że jest to ważny (bezpieczny w produkcji) sposób obserwowania zmian ramek w widoku.
Obecnie nie jest możliwe użycie KVO do obserwacji ramy widoku. Właściwości muszą być zgodne z KVO, aby były widoczne. Niestety, właściwości frameworka UIKit na ogół nie są obserwowalne, tak jak w przypadku każdego innego frameworka systemowego.
Z dokumentacji :
Uwaga: Chociaż klasy frameworka UIKit generalnie nie obsługują KVO, nadal można go zaimplementować w niestandardowych obiektach aplikacji, w tym w niestandardowych widokach.
Istnieje kilka wyjątków od tej reguły, takich jak operations
właściwość NSOperationQueue, ale muszą one być jawnie udokumentowane.
Nawet jeśli używanie KVO we właściwościach widoku może obecnie działać, nie polecałbym używania go w kodzie wysyłki. To delikatne podejście i opiera się na nieudokumentowanym zachowaniu.
UIView
więc mogą używać dowolnego mechanizmu, który uznają za odpowiedni.
Jeśli mógłbym wnieść swój wkład w rozmowę: jak zauważyli inni, frame
nie ma gwarancji, że sam będzie obserwowalny klucz-wartość, podobnie jak CALayer
właściwości, nawet jeśli wydają się takie.
Zamiast tego możesz utworzyć niestandardową UIView
podklasę, która zastępuje setFrame:
i ogłasza to przyjęcie delegatowi. Ustaw autoresizingMask
tak, aby wszystko było elastyczne. Skonfiguruj go tak, aby był całkowicie przezroczysty i mały (aby zaoszczędzić koszty na CALayer
podkładzie, ale nie ma to dużego znaczenia) i dodaj go jako podwidok widoku, w którym chcesz obserwować zmiany rozmiaru.
Udało mi się to z powodzeniem w iOS 4, kiedy po raz pierwszy określaliśmy iOS 5 jako interfejs API do kodowania i, w rezultacie, potrzebowaliśmy tymczasowej emulacji viewDidLayoutSubviews
(choć to zastępowanie layoutSubviews
było bardziej odpowiednie, ale o co chodzi).
transform
Jak wspomniano, jeśli KVO nie działa i chcesz po prostu obserwować własne widoki, nad którymi masz kontrolę, możesz utworzyć niestandardowy widok, który nadpisuje setFrame lub setBounds. Zastrzeżenie polega na tym, że ostateczna, pożądana wartość ramki może nie być dostępna w momencie wywołania. Dlatego dodałem wywołanie GCD do następnej pętli głównego wątku, aby ponownie sprawdzić wartość.
-(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...
}
});
}
Aby nie polegać na obserwacji KVO, możesz wykonać swizzling metod w następujący sposób:
@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
Teraz, aby obserwować zmianę ramki dla UIView w kodzie aplikacji:
- (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));
}
Zaktualizowana odpowiedź @hfossli dla RxSwift i Swift 5 .
Z RxSwift możesz to zrobić
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)
A jeśli chcesz tylko wiedzieć, kiedy bounds
możesz dokonać zmian
Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
print("View bounds changed its geometry")
}).disposed(by: rx.disposeBag)
A jeśli chcesz tylko wiedzieć, kiedy frame
możesz dokonać zmian
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)
Jest sposób, aby to osiągnąć bez użycia KVO w ogóle i aby inni znaleźli ten post, dodam go tutaj.
http://www.objc.io/issue-12/animating-custom-layer-properties.html
Ten doskonały samouczek Nicka Lockwooda opisuje, jak używać podstawowych funkcji synchronizacji animacji, aby sterować czymkolwiek. Jest to o wiele lepsze niż użycie timera lub warstwy CADisplay, ponieważ możesz użyć wbudowanych funkcji czasowych lub dość łatwo stworzyć własną sześcienną funkcję beziera (patrz dołączony artykuł ( http://www.objc.io/issue-12/ animations-wyjaśnione.html ).
Używanie KVO w niektórych właściwościach UIKit, takich jak frame
. A przynajmniej tak twierdzi Apple.
Polecam użycie ReactiveCocoa , pomoże to wysłuchać zmian w dowolnej nieruchomości bez użycia KVO, bardzo łatwo jest zacząć obserwować coś za pomocą sygnałów:
[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
//do whatever you want with the new frame
}];