Jak mogę przeprowadzić obserwację wartości klucza i uzyskać wywołanie zwrotne KVO w ramce UIView?

79

Chcę obserwować zmiany w UIView„s frame, boundslub centermienia. Jak mogę wykorzystać obserwację klucz-wartość, aby to osiągnąć?

hfossli
źródło
3
To właściwie nie jest pytanie.
extremeboredom
7
Chciałem tylko opublikować swoją odpowiedź na rozwiązanie, ponieważ nie mogłem znaleźć rozwiązania przez
googlowanie i przepełnienie stosu
12
dobrze jest zadawać pytania, które uważasz za interesujące i na które masz już rozwiązania. Jednak - włóż więcej wysiłku w sformułowanie pytania tak, aby naprawdę brzmiało jak pytanie, które można by zadać.
Bozho
1
Fantastyczna kontrola jakości, dzięki hfossil !!!
Fattie

Odpowiedzi:

71

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 positioni 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 boundsmoż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 framemoż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");
}];
hfossli
źródło
Jeśli rama rzutem za był zgodny KVO, to byłoby wystarczające, aby obserwować tylko ramkę. Inne właściwości, które wpływają na ramkę, spowodowałyby również powiadomienie o zmianie w ramce (byłby to klucz zależny). Ale, jak powiedziałem, wszystko to po prostu nie ma miejsca i może zadziałać tylko przez przypadek.
Nikolai Ruhe
4
Cóż, CALayer.h mówi: „CALayer implementuje standardowy protokół NSKeyValueCoding dla wszystkich właściwości Objective C zdefiniowanych przez klasę i jej podklasy…”. :) Są widoczne.
hfossli
2
Masz rację, że udokumentowano, że obiekty Core Animation są zgodne z KVC. Nie mówi to jednak nic o zgodności z KVO. KVC i KVO to po prostu różne rzeczy (chociaż zgodność KVC jest warunkiem wstępnym zgodności z KVO).
Nikolai Ruhe
3
Głosuję w dół, aby zwrócić uwagę na problemy związane z Twoim podejściem do korzystania z KVO. Próbowałem wyjaśnić, że przykład roboczy nie wspiera rekomendacji, jak zrobić coś poprawnie w kodzie. Jeśli nie jesteś przekonany, oto kolejne odniesienie do faktu, że nie można zaobserwować dowolnych właściwości UIKit .
Nikolai Ruhe
5
Podaj prawidłowy wskaźnik kontekstu. Dzięki temu możesz odróżnić swoje obserwacje od obserwacji innego obiektu. Niezrobienie tego może skutkować niezdefiniowanym zachowaniem, w szczególności w przypadku usunięcia obserwatora.
quellish
62

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];
        }
    }
}

 
hfossli
źródło
Nie działa. Możesz dodać obserwatory dla większości właściwości w UIView, ale nie dla ramki. Otrzymuję ostrzeżenie kompilatora o „prawdopodobnie niezdefiniowanej ramce ścieżki klucza”. Ignorując to ostrzeżenie i robiąc to mimo wszystko, metoda observValueForKeyPath nigdy nie jest wywoływana.
n13
Cóż, na mnie działa. Opublikowałem tutaj również późniejszą i solidniejszą wersję.
hfossli
3
Potwierdzone, działa również dla mnie. UIView.frame jest właściwie obserwowalna. Co zabawne, UIView.bounds nie jest.
Do
1
@hfossli Masz rację, że nie możesz po prostu na ślepo zadzwonić do [super] - spowoduje to zgłoszenie wyjątku w rodzaju „… wiadomość została odebrana, ale nie została obsłużona”, co jest trochę wstydem - musisz tak naprawdę wiedzą, że nadklasa implementuje metodę przed jej wywołaniem.
Richard
3
-1: Ani nie UIViewControllerdeklaruje, viewani nie UIViewdeklaruje framezgodnoś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.
Nikolai Ruhe
7

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 operationswł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.

Nikolai Ruhe
źródło
Zgadzam się z Tobą co do KVO na posesji "Rama" na UIView. Druga odpowiedź, którą podałem, wydaje się działać idealnie.
hfossli
@hfossli ReactiveCocoa jest zbudowany na KVO. Ma te same ograniczenia i problemy. To nie jest właściwy sposób obserwowania kadru widoku.
Nikolai Ruhe
Tak, wiem to. Dlatego napisałem, że możesz zrobić zwykłe KVO. Używanie ReactiveCocoa służyło tylko do prostoty.
hfossli
Cześć @NikolaiRuhe - właśnie przyszło mi do głowy. Jeśli Apple nie może KVO ramki, jak u licha wdrażają stackoverflow.com/a/25727788/294884 modern Constraints for views ?!
Fattie
@JoeBlow Apple nie musi używać KVO. Kontrolują wdrażanie wszystkich, UIViewwięc mogą używać dowolnego mechanizmu, który uznają za odpowiedni.
Nikolai Ruhe
4

Jeśli mógłbym wnieść swój wkład w rozmowę: jak zauważyli inni, framenie ma gwarancji, że sam będzie obserwowalny klucz-wartość, podobnie jak CALayerwłaściwości, nawet jeśli wydają się takie.

Zamiast tego możesz utworzyć niestandardową UIViewpodklasę, która zastępuje setFrame:i ogłasza to przyjęcie delegatowi. Ustaw autoresizingMasktak, aby wszystko było elastyczne. Skonfiguruj go tak, aby był całkowicie przezroczysty i mały (aby zaoszczędzić koszty na CALayerpodkł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 layoutSubviewsbyło bardziej odpowiednie, ale o co chodzi).

Tommy
źródło
Musisz również transform
podklasować
Jest to ładne, nadające się do ponownego użycia (daj swojej podklasie UIView metodę init, która bierze pod uwagę budowanie ograniczeń i kontroler widoku do raportowania zmian z powrotem i jest łatwe do wdrożenia wszędzie tam, gdzie jest to potrzebne) rozwiązanie, które nadal działa (znaleziono nadpisanie setBounds : najbardziej skuteczny w moim przypadku). Jest to szczególnie przydatne, gdy nie można użyć podejścia viewDidLayoutSubviews: z powodu konieczności przekazywania elementów.
bcl
0

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...
      }
   });
}
leżak
źródło
0

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));
}
Werner Altewischer
źródło
Z wyjątkiem tego, że musiałbyś nie tylko zamienić setFrame, ale także layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ i layer.frame. Co jest nie tak z KVO? :)
hfossli
0

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 boundsmoż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 framemoż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)
czarna Perła
źródło
-1

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 ).

Sam Clewlow
źródło
„Jest sposób, aby to osiągnąć bez użycia KVO”. Co to jest „to” w tym kontekście? Czy możesz być trochę bardziej szczegółowy.
hfossli
OP poprosił o sposób uzyskania określonych wartości dla widoku podczas jego animacji. Zapytali również, czy jest możliwe, aby KVO te właściwości, co jest, ale nie jest to technicznie obsługiwane. Zasugerowałem zapoznanie się z artykułem, który zapewnia solidne rozwiązanie problemu.
Sam Clewlow
Czy mógłbyś to sprecyzować? Która część artykułu była dla Ciebie istotna?
hfossli
@hfossli Patrząc na to, myślę, że mogłem postawić tę odpowiedź na niewłaściwe pytanie, ponieważ widzę jakąkolwiek wzmiankę o animacjach! Przepraszam!
Sam Clewlow
:-) nie ma problemu. Byłem po prostu głodny wiedzy.
hfossli
-4

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
}];
saky
źródło
4
jednak @NikolaiRuhe mówi: „ReactiveCocoa jest zbudowane na KVO. Ma te same ograniczenia i problemy. To nie jest właściwy sposób obserwowania ramy widoku”
Fattie