Wie kann ich Key Value Observing durchführen und einen KVO-Rückruf für einen UIView-Frame erhalten?

79

Ich möchte für Änderungen in einer sehen UIViewist frame, boundsoder centerEigentum. Wie kann ich Key-Value Observing verwenden, um dies zu erreichen?

hfossli
quelle
3
Das ist eigentlich keine Frage.
extreme Langeweile
7
Ich wollte nur meine Antwort auf die Lösung posten, da ich die Lösung nicht durch googeln und überlaufen finden konnte :-) ... seufz ! ... so viel zum Teilen ...
hfossli
12
Es ist vollkommen in Ordnung, Fragen zu stellen, die Sie interessant finden und für die Sie bereits Lösungen haben. Nehmen Sie sich jedoch mehr Mühe, die Frage so zu formulieren, dass sie wirklich wie eine Frage klingt, die man stellen würde.
Bozho
1
Fantastische Qualitätssicherung, danke hfossil !!!
Fattie

Antworten:

71

Es gibt normalerweise Benachrichtigungen oder andere beobachtbare Ereignisse, bei denen KVO nicht unterstützt wird. Obwohl die Dokumentation "Nein" sagt , ist es scheinbar sicher, den CALayer zu beobachten, der die UIView unterstützt. Das Beobachten des CALayer funktioniert in der Praxis aufgrund des umfassenden Einsatzes von KVO und geeigneten Accessoren (anstelle der Ivar-Manipulation). Es ist nicht garantiert, dass es in Zukunft funktioniert.

Auf jeden Fall ist der Rahmen der Ansicht nur das Produkt anderer Eigenschaften. Deshalb müssen wir diese beachten:

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

Das vollständige Beispiel finden Sie hier https://gist.github.com/hfossli/7234623

HINWEIS: Dies wird in den Dokumenten nicht unterstützt, funktioniert jedoch ab heute mit allen bisherigen iOS-Versionen (derzeit iOS 2 -> iOS 11).

HINWEIS: Beachten Sie, dass Sie mehrere Rückrufe erhalten, bevor der endgültige Wert erreicht ist. Wenn Sie beispielsweise den Rahmen einer Ansicht oder Ebene ändern, ändert sich die Ebene positionund bounds(in dieser Reihenfolge).


Mit ReactiveCocoa können Sie tun

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

Und wenn Sie nur wissen möchten, wann boundsSie Änderungen vornehmen können

@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");
}];

Und wenn Sie nur wissen möchten, wann frameSie Änderungen vornehmen können

@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
quelle
Wenn ein Rahmen Sicht war gefällig KVO, es würde ausreichen, nur den Rahmen zu beobachten. Andere Eigenschaften, die den Frame beeinflussen, würden auch eine Änderungsbenachrichtigung für den Frame auslösen (dies wäre ein abhängiger Schlüssel). Aber wie gesagt, all das ist einfach nicht der Fall und könnte nur zufällig funktionieren.
Nikolai Ruhe
4
Nun, CALayer.h sagt: "CALayer implementiert das Standardprotokoll NSKeyValueCoding für alle Objective C-Eigenschaften, die von der Klasse und ihren Unterklassen definiert werden ..." Also los geht's. :) Sie sind beobachtbar.
Hfossli
2
Sie haben Recht, dass Core Animation-Objekte als KVC-konform dokumentiert sind . Dies sagt jedoch nichts über die KVO-Konformität aus. KVC und KVO sind nur verschiedene Dinge (obwohl die Einhaltung von KVC eine Voraussetzung für die Einhaltung von KVO ist).
Nikolai Ruhe
3
Ich stimme ab, um auf die Probleme aufmerksam zu machen, die mit Ihrem Ansatz bei der Verwendung von KVO verbunden sind. Ich habe versucht zu erklären, dass ein funktionierendes Beispiel keine Empfehlung unterstützt, wie man etwas richtig im Code macht. Falls Sie nicht überzeugt sind, finden Sie hier einen weiteren Hinweis darauf, dass es nicht möglich ist, beliebige UIKit-Eigenschaften zu beobachten .
Nikolai Ruhe
5
Bitte übergeben Sie einen gültigen Kontextzeiger. Auf diese Weise können Sie zwischen Ihren Beobachtungen und denen eines anderen Objekts unterscheiden. Andernfalls kann es zu undefiniertem Verhalten kommen, insbesondere beim Entfernen eines Beobachters.
quellish
62

EDIT : Ich denke nicht, dass diese Lösung gründlich genug ist. Diese Antwort wird aus historischen Gründen aufbewahrt. Meine neueste Antwort finden Sie hier: https://stackoverflow.com/a/19687115/202451


Sie müssen KVO für die Frame-Eigenschaft ausführen. "self" ist in diesem Fall ein UIViewController.

Hinzufügen des Beobachters (normalerweise in viewDidLoad):

[self addObserver:self forKeyPath:@"view.frame" options:NSKeyValueObservingOptionOld context:NULL];

Entfernen des Beobachters (normalerweise in Dealloc oder viewDidDisappear :):

[self removeObserver:self forKeyPath:@"view.frame"];

Informationen über die Änderung erhalten

- (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
quelle
Funktioniert nicht Sie können Beobachter für die meisten Eigenschaften in UIView hinzufügen, jedoch nicht für Frames. Ich erhalte eine Compiler-Warnung über einen "möglicherweise undefinierten Schlüsselpfad 'Frame'". Wenn Sie diese Warnung ignorieren und trotzdem ausführen, wird die Methode compareValueForKeyPath niemals aufgerufen.
n13
Nun, funktioniert für mich. Ich habe jetzt auch hier eine spätere und robustere Version veröffentlicht.
Hfossli
3
Bestätigt, funktioniert auch bei mir. UIView.frame ist richtig beobachtbar. Lustigerweise ist UIView.bounds nicht.
Bis zum
1
@hfossli Du hast Recht, dass du nicht einfach blind [super] anrufen kannst - das wird eine Ausnahme in der Art von "... Nachricht wurde empfangen, aber nicht behandelt" auslösen, was ein bisschen schade ist - du musst wissen tatsächlich, dass die Oberklasse die Methode implementiert, bevor sie aufgerufen wird.
Richard
3
-1: Es werden weder KVO-kompatible Schlüssel UIViewControllerdeklariert viewnoch UIViewdeklariert frame. Kakao und Kakao-Touch erlauben keine willkürliche Beobachtung der Tasten. Alle beobachtbaren Schlüssel müssen ordnungsgemäß dokumentiert werden. Die Tatsache, dass es zu funktionieren scheint, macht dies nicht zu einer gültigen (produktionssicheren) Methode, um Rahmenänderungen in einer Ansicht zu beobachten.
Nikolai Ruhe
7

Derzeit ist es nicht möglich, KVO zum Beobachten des Rahmens einer Ansicht zu verwenden. Eigenschaften müssen KVO-konform sein , um beobachtbar zu sein. Leider sind die Eigenschaften des UIKit-Frameworks im Allgemeinen nicht wie bei jedem anderen System-Framework zu beobachten.

Aus der Dokumentation :

Hinweis: Obwohl die Klassen des UIKit-Frameworks KVO im Allgemeinen nicht unterstützen, können Sie es dennoch in den benutzerdefinierten Objekten Ihrer Anwendung implementieren, einschließlich benutzerdefinierter Ansichten.

Es gibt einige Ausnahmen von dieser Regel, wie die operationsEigenschaft von NSOperationQueue, die jedoch explizit dokumentiert werden müssen.

Selbst wenn die Verwendung von KVO für die Eigenschaften einer Ansicht derzeit möglicherweise funktioniert, würde ich nicht empfehlen, es im Versandcode zu verwenden. Es ist ein fragiler Ansatz und beruht auf undokumentiertem Verhalten.

Nikolai Ruhe
quelle
Ich stimme Ihnen in Bezug auf KVO in der Eigenschaft "frame" auf UIView zu. Die andere Antwort, die ich gegeben habe, scheint einfach perfekt zu funktionieren.
Hfossli
@hfossli ReactiveCocoa basiert auf KVO. Es hat die gleichen Einschränkungen und Probleme. Es ist keine richtige Möglichkeit, den Rahmen einer Ansicht zu beobachten.
Nikolai Ruhe
Ja, das weiß ich. Deshalb habe ich geschrieben, dass Sie gewöhnliche KVO machen können. Die Verwendung von ReactiveCocoa diente nur dazu, es einfach zu halten.
Hfossli
Hallo @NikolaiRuhe - es ist mir gerade eingefallen. Wenn Apple den Frame nicht KVO kann, wie zum Teufel implementieren sie stackoverflow.com/a/25727788/294884 moderne Einschränkungen für Ansichten?!
Fattie
@ JoeBlow Apple muss KVO nicht verwenden. Sie steuern die Implementierung von allem, UIViewdamit sie jeden Mechanismus verwenden können, den sie für richtig halten.
Nikolai Ruhe
4

Wenn ich zum Gespräch beitragen könnte: Wie andere betont haben, frameist nicht garantiert, dass der Schlüsselwert selbst beobachtbar ist, und die CALayerEigenschaften sind es auch nicht, obwohl sie zu sein scheinen.

Sie können stattdessen eine benutzerdefinierte UIViewUnterklasse erstellen , die setFrame:diese Quittung überschreibt und einem Delegaten mitteilt. Stellen Sie das autoresizingMaskso ein, dass die Ansicht alles flexibel hat. Konfigurieren Sie es so, dass es vollständig transparent und klein ist (um Kosten auf der CALayerRückseite zu sparen , nicht dass es sehr wichtig ist), und fügen Sie es als Unteransicht der Ansicht hinzu, in der Sie Größenänderungen beobachten möchten.

Dies funktionierte für mich bereits unter iOS 4 erfolgreich, als wir iOS 5 zum ersten Mal als API für den Code spezifizierten und daher eine vorübergehende Emulation von benötigten viewDidLayoutSubviews(obwohl das Überschreiben layoutSubviewsangemessener war, aber Sie verstehen, worum es geht).

Tommy
quelle
Sie müssten die Ebene auch unterklassifizieren, um transformetc
hfossli
Dies ist eine nette, wiederverwendbare Lösung (geben Sie Ihrer UIView-Unterklasse eine Init-Methode, mit der die Ansicht Einschränkungen erstellt und der Ansichtscontroller Änderungen zurückmeldet und die einfach überall dort bereitgestellt werden kann, wo Sie sie benötigen), die weiterhin funktioniert (überschreibende setBounds gefunden) : am effektivsten in meinem Fall). Besonders praktisch, wenn Sie den viewDidLayoutSubviews: -Ansatz nicht verwenden können, da Elemente weitergeleitet werden müssen.
bcl
0

Wie bereits erwähnt, können Sie eine benutzerdefinierte Ansicht erstellen, die entweder setFrame oder setBounds überschreibt, wenn KVO nicht funktioniert und Sie nur Ihre eigenen Ansichten beobachten möchten, über die Sie die Kontrolle haben. Eine Einschränkung besteht darin, dass der endgültige, gewünschte Frame-Wert zum Zeitpunkt des Aufrufs möglicherweise nicht verfügbar ist. Daher habe ich der nächsten Haupt-Thread-Schleife einen GCD-Aufruf hinzugefügt, um den Wert erneut zu überprüfen.

-(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...
      }
   });
}
Loungerdork
quelle
0

Um sich nicht auf die Beobachtung von KVO zu verlassen, können Sie das Methoden-Swizzling wie folgt durchführen:

@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

So beobachten Sie nun die Rahmenänderung für eine UIView in Ihrem Anwendungscode:

- (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
quelle
Außer dass Sie nicht nur setFrame, sondern auch layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ und layer.frame swizzeln müssen. Was ist los mit KVO? :)
Hfossli
0

Aktualisierte @ hfossli-Antwort für RxSwift und Swift 5 .

Mit RxSwift können Sie tun

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)

Und wenn Sie nur wissen möchten, wann boundsSie Änderungen vornehmen können

Observable.of(rx.observe(CGRect.self, #keyPath(UIView.layer.bounds))).subscribe(onNext: { _ in
                print("View bounds changed its geometry")
            }).disposed(by: rx.disposeBag)

Und wenn Sie nur wissen möchten, wann frameSie Änderungen vornehmen können

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)
schwarze Perle
quelle
-1

Es gibt eine Möglichkeit, dies zu erreichen, ohne KVO überhaupt zu verwenden, und damit andere diesen Beitrag finden, werde ich ihn hier hinzufügen.

http://www.objc.io/issue-12/animating-custom-layer-properties.html

Dieses hervorragende Tutorial von Nick Lockwood beschreibt, wie Sie mithilfe der Timing-Funktionen für Kernanimationen alles steuern können. Es ist der Verwendung eines Timers oder einer CADisplay-Ebene weit überlegen, da Sie die integrierten Timing-Funktionen verwenden oder ganz einfach Ihre eigene kubische Bezier-Funktion erstellen können (siehe den zugehörigen Artikel ( http://www.objc.io/issue-12/). Animationen erklärt.html ).

Sam Clewlow
quelle
"Es gibt eine Möglichkeit, dies ohne KVO zu erreichen". Was ist "das" in diesem Zusammenhang? Können Sie etwas genauer sein?
Hfossli
Das OP fragte nach einer Möglichkeit, während der Animation bestimmte Werte für eine Ansicht abzurufen. Sie fragten auch, ob es möglich ist, diese Eigenschaften zu KVO, was es ist, aber es wird technisch nicht unterstützt. Ich habe vorgeschlagen, den Artikel zu lesen, der eine robuste Lösung für das Problem bietet.
Sam Clewlow
Kannst du genauer sein? Welchen Teil des Artikels fanden Sie relevant?
Hfossli
@hfossli Wenn ich es mir anschaue, denke ich, dass ich diese Antwort auf die falsche Frage gestellt habe, da ich jede Erwähnung von Animationen sehen kann! Es tut uns leid!
Sam Clewlow
:-) kein Problem. Ich war nur hungrig nach Wissen.
Hfossli
-4

Es ist nicht sicher, KVO in einigen UIKit-Eigenschaften wie zu verwenden frame. Zumindest sagt das Apple.

Ich würde empfehlen, ReactiveCocoa zu verwenden . Dies hilft Ihnen dabei, Änderungen in einer Eigenschaft zu hören, ohne KVO zu verwenden. Es ist sehr einfach, etwas mithilfe von Signalen zu beobachten :

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];
saky
quelle
4
@NikolaiRuhe sagt jedoch: "ReactiveCocoa basiert auf KVO. Es hat die gleichen Einschränkungen und Probleme. Es ist keine richtige Möglichkeit, den Rahmen einer Ansicht zu beobachten"
Fattie