Làm cách nào tôi có thể thực hiện Quan sát giá trị chính và nhận lệnh gọi lại KVO trên khung của UIView?


79

Tôi muốn xem các thay đổi trong một UIView's frame, boundshoặc centerbất động sản. Làm cách nào để sử dụng tính năng Quan sát giá trị chính để đạt được điều này?


3
Đây thực sự không phải là một câu hỏi.
Extremeboredom

7
tôi chỉ muốn gửi câu trả lời của tôi với các giải pháp kể từ khi tôi không thể tìm ra giải pháp bằng cách googling và stackoverflowing :-) ... thở dài ...! rất nhiều vì đã chia sẻ ...
hfossli

12
hoàn toàn ổn nếu bạn đặt những câu hỏi mà bạn thấy thú vị và bạn đã có giải pháp. Tuy nhiên - hãy nỗ lực nhiều hơn để diễn đạt câu hỏi sao cho nó thực sự giống như một câu hỏi mà người ta sẽ hỏi.
Bozho

1
QA tuyệt vời, cảm ơn hfossil !!!
Fattie,

Câu trả lời:


71

Thường có các thông báo hoặc các sự kiện có thể quan sát khác mà KVO không được hỗ trợ. Mặc dù các tài liệu nói "không" , nhưng có vẻ an toàn khi quan sát CALayer ủng hộ UIView. Việc quan sát CALayer hoạt động trong thực tế vì nó được sử dụng rộng rãi KVO và các trình truy cập thích hợp (thay vì thao tác ivar). Nó không được đảm bảo sẽ hoạt động trong tương lai.

Dù sao, khung của khung nhìn cũng chỉ là sản phẩm của các thuộc tính khác. Do đó chúng ta cần quan sát những điều sau:

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

Xem ví dụ đầy đủ tại đây https://gist.github.com/hfossli/7234623

LƯU Ý: Tính năng này không được hỗ trợ trong tài liệu, nhưng nó hoạt động cho đến hôm nay với tất cả các phiên bản iOS cho đến nay (hiện tại là iOS 2 -> iOS 11)

LƯU Ý: Hãy lưu ý rằng bạn sẽ nhận được nhiều cuộc gọi lại trước khi nó giải quyết ở giá trị cuối cùng. Ví dụ, thay đổi khung của một khung nhìn hoặc lớp sẽ làm cho lớp thay đổi positionbounds(theo thứ tự đó).


Với ReactiveCocoa, bạn có thể làm

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

Và nếu bạn chỉ muốn biết khi boundsnào bạn có thể thực hiện những thay đổi

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

Và nếu bạn chỉ muốn biết khi framenào bạn có thể thực hiện những thay đổi

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

Nếu khung của một quan điểm KVO tuân thủ, nó sẽ là đủ để quan sát chỉ là khung. Các thuộc tính khác ảnh hưởng đến khung cũng sẽ kích hoạt thông báo thay đổi trên khung (nó sẽ là một khóa phụ thuộc). Nhưng, như tôi đã nói, tất cả những điều đó không phải như vậy và có thể chỉ hoạt động một cách tình cờ.
Nikolai Ruhe,

4
CALayer.h nói rằng "CALayer triển khai giao thức NSKeyValueCoding tiêu chuẩn cho tất cả các thuộc tính Objective C được xác định bởi lớp và các lớp con của nó ..." Vậy là xong. :) Chúng có thể quan sát được.
hfossli

2
Bạn nói đúng rằng các đối tượng Core Animation được ghi nhận là tuân thủ KVC. Tuy nhiên, điều này không nói gì về việc tuân thủ KVO. KVC và KVO chỉ là những thứ khác nhau (mặc dù tuân thủ KVC là điều kiện tiên quyết để tuân thủ KVO).
Nikolai Ruhe,

3
Tôi cố gắng thu hút sự chú ý đến các vấn đề liên quan đến cách tiếp cận của bạn khi sử dụng KVO. Tôi đã cố gắng giải thích rằng một ví dụ làm việc không hỗ trợ khuyến nghị về cách thực hiện điều gì đó đúng cách trong mã. Trong trường hợp bạn không bị thuyết phục, đây là một tham chiếu khác về thực tế là không thể quan sát các thuộc tính UIKit tùy ý .
Nikolai Ruhe

5
Vui lòng chuyển một con trỏ ngữ cảnh hợp lệ. Làm như vậy cho phép bạn phân biệt giữa quan sát của mình và quan sát của một số đối tượng khác. Không làm như vậy có thể dẫn đến hành vi không xác định, cụ thể là loại bỏ một người quan sát.
dập tắt

62

CHỈNH SỬA : Tôi không nghĩ rằng giải pháp này là đủ triệt để. Câu trả lời này được giữ vì lý do lịch sử. Xem câu trả lời mới nhất của tôi tại đây: https://stackoverflow.com/a/19687115/202451


Bạn phải thực hiện KVO trên thuộc tính khung. "self" trong trường hợp này là UIViewController.

thêm trình quan sát (thường được thực hiện trong viewDidLoad):

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

loại bỏ trình quan sát (thường được thực hiện trong dealloc hoặc viewDidDisappear :):

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

Nhận thông tin về thay đổi

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

 

Không hoạt động. Bạn có thể thêm người quan sát cho hầu hết các thuộc tính trong UIView, nhưng không cho khung. Tôi nhận được cảnh báo trình biên dịch về "khung 'đường dẫn khóa có thể không xác định'". Bỏ qua cảnh báo này và vẫn thực hiện nó, phương thức ObserValueForKeyPath sẽ không bao giờ được gọi.
n13, 11/02/12

Tốt, làm việc cho tôi. Bây giờ tôi cũng đã đăng một phiên bản mới hơn và mạnh mẽ hơn ở đây.
hfossli

3
Đã xác nhận, cũng có tác dụng với tôi. UIView.frame có thể quan sát được. Thật buồn cười, UIView.bounds thì không.
Đến

1
@hfossli Bạn nói đúng rằng bạn không thể chỉ gọi một cách mù quáng [super] - điều đó sẽ tạo ra một ngoại lệ dọc theo dòng "... tin nhắn đã được nhận nhưng chưa được xử lý", điều này hơi đáng tiếc - bạn phải thực sự biết rằng lớp cha thực thi phương thức trước khi gọi nó.
Richard

3
-1: Không UIViewControllerkhai báo viewhoặc không UIViewtuyên bố framelà khóa tuân thủ KVO. Cocoa và Cocoa-touch không cho phép tùy ý quan sát các phím. Tất cả các khóa có thể quan sát phải được ghi lại đúng cách. Thực tế là nó có vẻ hoạt động không giúp đây là một cách hợp lệ (an toàn cho sản xuất) để quan sát những thay đổi của khung hình trên một chế độ xem.
Nikolai Ruhe

7

Hiện tại, không thể sử dụng KVO để quan sát khung của một chế độ xem. Các thuộc tính phải tuân thủ KVO để có thể quan sát được. Đáng buồn thay, các thuộc tính của khung UIKit nói chung không thể quan sát được, như với bất kỳ khung hệ thống nào khác.

Từ tài liệu :

Lưu ý: Mặc dù các lớp của khuôn khổ UIKit thường không hỗ trợ KVO, bạn vẫn có thể triển khai nó trong các đối tượng tùy chỉnh của ứng dụng của mình, bao gồm cả dạng xem tùy chỉnh.

Có một vài ngoại lệ đối với quy tắc này, chẳng hạn như thuộc operationstính của NSOperationQueue nhưng chúng phải được ghi lại một cách rõ ràng.

Ngay cả khi việc sử dụng KVO trên các thuộc tính của chế độ xem hiện có thể hoạt động, tôi không khuyên bạn nên sử dụng nó trong mã vận chuyển. Đó là một cách tiếp cận mong manh và dựa trên hành vi không có giấy tờ.


Tôi đồng ý với bạn về KVO trên thuộc tính "khung" trên UIView. Câu trả lời khác mà tôi đã cung cấp dường như hoạt động hoàn hảo.
hfossli

@hfossli ReactiveCocoa được xây dựng trên KVO. Nó có những hạn chế và vấn đề giống nhau. Đó không phải là cách thích hợp để quan sát khung của một chế độ xem.
Nikolai Ruhe,

Vâng, tôi biết điều đó. Đó là lý do tại sao tôi đã viết bạn có thể làm KVO bình thường. Sử dụng ReactiveCocoa chỉ để giữ cho nó đơn giản.
hfossli,

Xin chào @NikolaiRuhe - điều đó vừa xảy ra với tôi. Nếu Apple không thể KVO khung, làm thế quái nào họ triển khai stackoverflow.com/a/25727788/294884 Các ràng buộc hiện đại cho lượt xem ?!
Fattie

@JoeBlow Apple không phải sử dụng KVO. Họ kiểm soát việc thực hiện tất cả UIViewđể họ có thể sử dụng bất kỳ cơ chế nào họ thấy phù hợp.
Nikolai Ruhe

4

Nếu tôi có thể đóng góp vào cuộc trò chuyện: như những người khác đã chỉ ra, framekhông được đảm bảo là bản thân giá trị khóa có thể quan sát được và các CALayerthuộc tính cũng không mặc dù chúng có vẻ như vậy.

Thay vào đó, những gì bạn có thể làm là tạo một UIViewlớp con tùy chỉnh ghi đè setFrame:và thông báo biên nhận đó cho một người được ủy quyền. Đặt autoresizingMaskđể chế độ xem có mọi thứ linh hoạt. Định cấu hình nó để hoàn toàn trong suốt và nhỏ (để tiết kiệm chi phí CALayerhỗ trợ, không phải là vấn đề quan trọng) và thêm nó làm chế độ xem phụ của chế độ xem mà bạn muốn xem các thay đổi về kích thước.

Điều này đã hoạt động thành công đối với tôi theo cách trở lại iOS 4 khi chúng tôi lần đầu tiên chỉ định iOS 5 làm API để viết mã và do đó, cần một mô phỏng tạm thời viewDidLayoutSubviews(mặc dù ghi đè layoutSubviewsphù hợp hơn, nhưng bạn hiểu được).


Bạn sẽ cần phải phân lớp lớp cũng như để có được transformvv
hfossli

Đây là một giải pháp hay, có thể sử dụng lại (cung cấp cho lớp con UIView của bạn một phương thức init có chế độ xem để xây dựng các ràng buộc và bộ điều khiển chế độ xem để báo cáo lại các thay đổi và dễ dàng triển khai ở bất kỳ đâu bạn cần) giải pháp vẫn hoạt động (tìm thấy ghi đè setBounds : hiệu quả nhất trong trường hợp của tôi). Đặc biệt hữu ích khi bạn không thể sử dụng cách tiếp cận viewDidLayoutSubviews: do cần chuyển tiếp các phần tử.
bcl

0

Như đã đề cập, nếu KVO không hoạt động và bạn chỉ muốn quan sát các chế độ xem của riêng mình mà bạn có quyền kiểm soát, bạn có thể tạo chế độ xem tùy chỉnh ghi đè setFrame hoặc setBounds. Lưu ý là giá trị khung hình cuối cùng, mong muốn có thể không có sẵn tại thời điểm gọi. Vì vậy, tôi đã thêm một lệnh gọi GCD vào vòng lặp chính tiếp theo để kiểm tra lại giá trị.

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

0

Để không dựa vào KVO quan sát, bạn có thể thực hiện phương pháp swizzling như sau:

@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

Bây giờ để quan sát sự thay đổi khung cho một UIView trong mã ứng dụng của bạn:

- (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));
}

Ngoại trừ việc bạn không chỉ cần sử dụng setFrame mà còn cả layer.bounds, layer.transform, layer.position, layer.zPosition, layer.anchorPoint, layer.anchorPointZ và layer.frame. Có gì sai với KVO? :)
hfossli

0

Đã cập nhật câu trả lời @hfossli cho RxSwiftSwift 5 .

Với RxSwift bạn có thể làm

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)

Và nếu bạn chỉ muốn biết khi boundsnào bạn có thể thực hiện những thay đổi

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

Và nếu bạn chỉ muốn biết khi framenào bạn có thể thực hiện những thay đổi

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)

-1

Có một cách để đạt được điều này mà không cần sử dụng KVO, và vì lợi ích của những người khác tìm thấy bài đăng này, tôi sẽ thêm nó vào đây.

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

Hướng dẫn tuyệt vời này của Nick Lockwood mô tả cách sử dụng các chức năng định thời hoạt ảnh cốt lõi để thúc đẩy mọi thứ. Nó tốt hơn nhiều so với việc sử dụng bộ đếm thời gian hoặc lớp CADisplay, vì bạn có thể sử dụng các chức năng định thời tích hợp sẵn hoặc khá dễ dàng tạo hàm bezier khối của riêng bạn (xem bài viết kèm theo ( http://www.objc.io/issue-12/ động-giải thích.html ).


"Có một cách để đạt được điều này mà không cần sử dụng KVO". "Cái này" trong ngữ cảnh này là gì? Bạn có thể cụ thể hơn một chút.
hfossli

OP đã yêu cầu một cách để nhận các giá trị cụ thể cho một chế độ xem trong khi nó đang hoạt hình. Họ cũng hỏi liệu có thể KVO các thuộc tính này, nhưng nó không được hỗ trợ về mặt kỹ thuật. Tôi đã đề nghị xem bài viết, bài viết này cung cấp giải pháp mạnh mẽ cho vấn đề.
Sam Clewlow

Bạn có thể cụ thể hơn không? Bạn thấy phần nào của bài viết có liên quan?
hfossli

@hfossli Nhìn vào nó, tôi nghĩ rằng tôi có thể đã đặt câu trả lời này cho câu hỏi sai, vì tôi có thể thấy bất kỳ đề cập nào về hình ảnh động! Lấy làm tiếc!
Sam Clewlow

:-) không vấn đề gì. Tôi chỉ đói kiến ​​thức.
hfossli

-4

Không an toàn khi sử dụng KVO trong một số thuộc tính UIKit như frame. Hoặc ít nhất đó là những gì Apple nói.

Tôi khuyên bạn nên sử dụng ReactiveCocoa , điều này sẽ giúp bạn lắng nghe các thay đổi trong bất kỳ thuộc tính nào mà không cần sử dụng KVO, rất dễ dàng để bắt đầu quan sát điều gì đó bằng Signals:

[RACObserve(self, frame) subscribeNext:^(CGRect frame) {
    //do whatever you want with the new frame
}];

4
Tuy nhiên, @NikolaiRuhe nói "ReactiveCocoa được xây dựng trên KVO Nó có những hạn chế và những vấn đề cùng Nó không phải là một cách thích hợp để quan sát khung của một cái nhìn.."
Fattie
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.