Chcę obserwować zmiany w UIView
„s frame
, bounds
lub center
mienia. Jak mogę wykorzystać obserwację klucz-wartość, aby to osiągnąć?
iphone
ios
objective-c
uiview
key-value-observing
hfossli
źródło
źródło
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
ibounds
(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"); }];
źródło
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]; } } }
źródło
UIViewController
deklaruje,view
ani nieUIView
deklarujeframe
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 :
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.
źródło
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 jakCALayer
właściwości, nawet jeśli wydają się takie.Zamiast tego możesz utworzyć niestandardową
UIView
podklasę, która zastępujesetFrame:
i ogłasza to przyjęcie delegatowi. UstawautoresizingMask
tak, aby wszystko było elastyczne. Skonfiguruj go tak, aby był całkowicie przezroczysty i mały (aby zaoszczędzić koszty naCALayer
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ępowanielayoutSubviews
było bardziej odpowiednie, ale o co chodzi).źródło
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... } }); }
źródło
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)); }
źródło
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ć zmianObservable.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ć zmianObservable.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)
źródło
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 ).
źródło
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 }];
źródło