Tôi muốn xem các thay đổi trong một UIView
's frame
, bounds
hoặc center
bấ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?
Tôi muốn xem các thay đổi trong một UIView
's frame
, bounds
hoặc center
bấ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?
Câu trả lời:
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 position
và bounds
(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 bounds
nà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 frame
nà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");
}];
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];
}
}
}
UIViewController
khai báo view
hoặc không UIView
tuyên bố frame
là 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.
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 operations
tí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ờ.
UIView
để họ có thể sử dụng bất kỳ cơ chế nào họ thấy phù hợp.
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, frame
không được đảm bảo là bản thân giá trị khóa có thể quan sát được và các CALayer
thuộ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 UIView
lớ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í CALayer
hỗ 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 đè layoutSubviews
phù hợp hơn, nhưng bạn hiểu được).
transform
vv
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...
}
});
}
Để 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));
}
Đã cập nhật câu trả lời @hfossli cho RxSwift và Swift 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 bounds
nà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 frame
nà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)
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 ).
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
}];