注冊鍵-值觀察
為了接收某個屬性的鍵-值觀察通知,以下三個要素是必須的:
- 被觀察的類當中你關心的屬性必須是遵循鍵-值觀察的,這一細節在 中有所討論。
- 你必須使用以下方法,將觀察方對象與被觀察方對象注冊:
:forKeyPath:options:context:
. - 觀察方的對象必須實現以下方法:
observeValueForKeyPath:ofObject:change:context:
.
內容導航:
注冊為觀察者
為了正確接收屬性的變更通知,觀察對象必須首先發送一個addObserver:forKeyPath:options:context:
消息至被觀察對象,用以傳送觀察對象和需要觀察的屬性的關鍵路徑,以便與其注冊。選項參數指定了發送變更通知時提供給觀察者的信息。使用NSKeyValueObservingOptionOld
選項可以將初始對象值以變更字典中的一個項的形式提供給觀察者。指定NSKeyValueObservingOptionNew
選項可以將新的值以一個項的形式添加至變更字典。你可以使用逐位OR
這兩個常量來指定接收上述兩種類型的值。
列表1中的例子演示了為屬性openingBalance
注冊一個檢查器對象的方法。
表1 將檢查器注冊為openingBalance屬性的觀察者
- (void)registerAsObserver |
{ |
// register "inspector" to receive change notifications |
// for the "openingBalance" property of the "account" object |
// and that both the old and new values of "openingBalance" |
// should be provided to the observer |
[account addObserver:inspector |
forKeyPath:@"openingBalance" |
options:(NSKeyValueObservingOptionNew | |
NSKeyValueObservingOptionOld) |
context:NULL]; |
} |
當你注冊一個對象為觀察者時,還可以同時指定一個內容指針。當observeValueForKeyPath:ofObject:change:context:
被調用時,內容指針會被提交至觀察者。該指針可為C語言指針或對象引用。該指針可作為指定被觀察變更的唯一標示符,或是為觀察者提供其他數據。該指針不會被保留,應用程序本身應保證該指針在觀察方象被取消觀察者身份之前被釋放。
接收變更通知
當對象的一個被觀察屬性發生變動時,觀察者收到一個observeValueForKeyPath:ofObject:change:context:
消息。所有觀察者都必須實現這一方法。觸發觀察通知的對象和鍵路徑、包含變更細節的字典,以及觀察者注冊時提交的上下文指針均被提交給觀察者。
變更字典項NSKeyValueChangeKindKey
供發生變更的類型信息。如果被觀察對象的值改變了,NSKeyValueChangeKindKey
項將返回一個NSKeyValueChangeSetting
。依賴觀察者注冊時指定的選項,字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
項分別包含了被觀察屬性變更前后的值。
如果被觀察的屬性是一個對多的關系,NSKeyValueChangeKindKey
項同樣可以通過返回三個不同的項NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
來分別指明關系中的對象被執行的插入、刪除和替換操作。
變更字典項NSKeyValueChangeIndexesKey
是一個指定關系表變更索引的NSIndexSet
對象。注冊觀察者時,如果NSKeyValueObservingOptionNew
或NSKeyValueObservingOptionOld
被指定為選項,變更字典中NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
兩個項將以數組形式包含相關對象在變更前后的值。
表2中的例子演示了observeValueForKeyPath:ofObject:change:context:
方法實現了在檢查器中反映openingBalance
屬性的舊值和新值,該屬性在表1中已被注冊。
表2 observeValueForKeyPath:ofObject:change:context:的實現
- (void)observeValueForKeyPath:(NSString *)keyPath |
ofObject:(id)object |
change:(NSDictionary *)change |
context:(void *)context |
{ |
if ([keyPath isEqual:@"openingBalance"]) { |
[openingBalanceInspectorField setObjectValue: |
[change objectForKey:NSKeyValueChangeNewKey]]; |
} |
// be sure to call the super implementation |
// if the superclass implements it |
[super observeValueForKeyPath:keyPath |
ofObject:object |
change:change |
context:context]; |
} |
移除對象的觀察者身份
你可以發送一條指定觀察方對象和鍵路徑的removeObserver:forKeyPath:
消息至被觀察的對象,來移除一個鍵-值觀察者。表3中的例子將檢查器移除了其針對openingBalance
的觀察者身份。
表3 移除檢查器針對openingBalance的觀察者身份
- (void)unregisterForChangeNotification |
{ |
[observedObject removeObserver:inspector |
forKeyPath:@"openingBalance"]; |
} |
如果在觀察者被注冊時,內容被指定為一個對象,那么在觀察者被移除后它可以被安全地釋放。當接收到removeObserver:forKeyPath:
消息后,觀察方對象不會再收到關於指定的關鍵值路徑和對象的任何 observeValueForKeyPath:ofObject:change:context:
消息。
自動支持和手動支持的對比
使用鍵-值觀察,有兩種方式可以實現類屬性的可觀察性。NSObject提供自動觀察功能,該功能對遵循鍵-值編程的類所有屬性可用。手工觀察在觀察行為被通知時提供額外的控制,但需要編寫額外的代碼。
內容導航:
自動的鍵-值觀察
NSObject提供了基本的自動進行鍵-值觀察的實現方法。使用自動觀察通知可在通過鍵-值編程和遵循鍵-值編程方法實現功能時免去對變更的歸類,進而無須對willChangeValueForKey:
和didChangeValueForKey:
進行請求調用。自動觀察者通知受類方法automaticallyNotifiesObserversForKey:
控制。默認的實現方法對所有的鍵值都返回YES
。
和鍵-值編碼方法一樣,自動的鍵-值觀察將遵循鍵-值的訪問器作出的變更通知給觀察者。表1中的例子可實現當屬性name
發生變更時,其所有觀察者都收到變更通知。
// calling the accessor method |
[self setName:@"Savings"]; |
// using setValue:forKey: |
[self setValue:@"Savings" forKey:@"name"]; |
// using a key path, where account is a kvc-compliant property |
// of "document" |
[document setValue:@"Savings" forKeyPath:@"account.name"] |
自動通知還支持mutableArrayValueForKey:
和mutableSetValueForKey:
返回集合代理對象。這個功能可用於支持insertObject:in<Key>AtIndex:
,replaceObjectIn<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
等索引存取方法的對多關系。
你可以通過實現類方法automaticallyNotifiesObserversForKey:
來控制你的子類的自動觀察通知 。子類可以檢測參數檢測的鍵值,並在自動通知可用時返回YES
,禁用時則返回NO
。
手動觀察者通知
手動鍵-值觀察通知針對通知發送至觀察者的時間和方式提供更為精確的控制。這可以有效減少無用的觸發通知,或是將一組變更集成至一個單獨的通知中。
實現手動觀察通知的類必須重新實現NSObject類中的automaticallyNotifiesObserversForKey:
方法。在同一個類中同時使用自動和手動觀察者通知是可行的。對於執行手動觀察者通知的屬性來說,子類中automaticallyNotifiesObserversForKey:
實現應當返回NO
。子類實現中對於未在子類中不能識別的鍵值,必須調用Super
。表2中的例子對 openingBalance
屬性啟用了手動通知,允許父類確定其它所有鍵的通知。
表2 automaticallyNotifiesObserversForKey:的實現方法例
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { |
BOOL automatic = NO; |
if ([theKey isEqualToString:@"openingBalance"]) { |
automatic=NO; |
} else { |
automatic=[super automaticallyNotifiesObserversForKey:theKey]; |
} |
return automatic; |
} |
為了實現手動觀察通知,你必須在改變值之前調用willChangeValueForKey:
並在更改它之后調用didChangeValueForKey:
。表3中的例子為實現了屬性openingBalance
的手動觀察者通知。
- (void)setOpeningBalance:(double)theBalance { |
[self willChangeValueForKey:@"openingBalance"]; |
openingBalance=theBalance; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
你可以先確認值是否發生了變更,以盡量減小發送無用通知的數量。表4中的例子測試了openingBalance
的值,並且只在該值改變時發送通知。
- (void)setOpeningBalance:(double)theBalance { |
if (theBalance != openingBalance) { |
[self willChangeValueForKey:@"openingBalance"]; |
openingBalance=theBalance; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
} |
如果某個單一的操作造成了多個鍵變動,你必須將所有變更通知整合到一起,如表5所示。
- (void)setOpeningBalance:(double)theBalance { |
[self willChangeValueForKey:@"openingBalance"]; |
[self willChangeValueForKey:@"itemChanged"]; |
openingBalance=theBalance; |
itemChanged=itemChanged+1; |
[self didChangeValueForKey:@"itemChanged"]; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
在對多關系情況下,你不僅需要指定被改動的key,同樣地還需要指定變更類型和涉及到的對象索引。變更類型是指定NSKeyValueChangeInsertion
,SKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
的NSKeyValueChange
。受影響對象的索引將以NSIndexSet形式傳遞。
表6中的代碼段描述了在對多關系transactions
中內嵌對象刪除的方法。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { |
[self willChange:NSKeyValueChangeRemoval |
valuesAtIndexes:indexes forKey:@"transactions"]; |
// remove the transaction objects at the specified indexes here |
[self didChange:NSKeyValueChangeRemoval |
valuesAtIndexes:indexes forKey:@"transactions"]; |
} |
注冊依賴鍵
在許多情形下,一個屬性的值依賴於另一實體中的一個或多個屬性。如果其中一個屬性的值發生變更,被依賴的屬性值也應當為其改動進行標記。保證各個依賴屬性的鍵-值觀察通知被正確通報的辦法基於你使用的Mac OS X版本和對一或對多關系。
內容導航:
Mac OS X v10.5及更新版本中的對一關系
Mac OS X v10.4和Mac OS X v10.5上的對多關系情況
Mac OS X v10.5及更新版本中的對一關系
如果你針對Mac OS X v10.5及更新版本的操作系統,並且需要處理擁有對一關系的相關實體,那么你既可以重寫keyPathsForValuesAffectingValueForKey:
也可以實現一個適當的方法來注冊依賴的鍵。
例如,完整的人名依賴於名和姓兩者。一個返回全名的方法可以寫作如下形式:
- (NSString *)fullName { |
return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; |
} |
在firstName
或lastName
其中任一屬性變動時,觀察fullName
屬性的程序必須被通知,因為它們影響了該屬性的值。
一個解決辦法是重載keyPathsForValuesAffectingValueForKey:
指明人的fullName
屬性是依賴於lastName
和firstName
這兩個屬性的。表1演示了實現這種依賴關系的方法:
表1 keyPathsForValuesAffectingValueForKey:
的示例實現方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key |
{ |
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; |
if ([key isEqualToString:@"fullName"]) |
{ |
NSSet *affectingKeys = [NSSet setWithObjects:@"lastName", @"firstName",nil]; |
keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys]; |
} |
return keyPaths; |
} |
你的重載通常應該調用父類並且返回包換父類返回的所有成員的集合(以便讓你的重載不影響父類中這個方法)。
你也可以通過實現一個遵循命名規則的類方法keyPathsForvaluesAffecting<Key>
以達到同樣的效果,其中
<Key>
是依賴於值的屬性名(首字母大寫)。使用這種規則,表1中的代碼可被重載為一個名為
keyPathsForValuesAffectingFullName
的類方法,如
表2所示。
表2 keyPathsForValuesAffecting<Key>
命名規則實例實現方法
+ (NSSet *)keyPathsForValuesAffectingFullName |
{ |
return [NSSet setWithObjects:@"lastName", @"firstName", nil |
} |
當你為一個已經存在的使用范疇的類增加一個合成屬性是,你不能重載keyPathsForValuesAffectingValueForKey:
方法,原因是你不能重載范疇中的方法。在這種情況下,實現一個匹配keyPathsForValuesAffecting<Key>
的類方法來實現這種機制。
注釋:你不能依靠實現keyPathsForValuesAffectingValueForKey:
來為對多關系設置依賴性,而必須觀察對多關系集合中每個對象的適當屬性,並自己通過更新依賴鍵來相應值的變更。下一小節說明了處理這一情況的方法。
Mac OS X v10.4和Mac OS X v10.5上的對多關系情況
如果你針對的是Mac OS X v10.4,setKeys:triggerChangeNotificationsForDependentKey:
並不允許任何鍵路徑,因此你不能使用上述的方法。
如果你針對Mac OS X v10.5,keyPathsForValuesAffectingValueForKey:
不允許包含對多關系的鍵路徑。舉例來說,假如你有一個Department實體(
employees
) , 和Employees之間是對多關系,並且Employee擁有一個salary屬性。你可能希望Department實體擁有一個
totalSalary
屬性,依賴於所有雇員的薪金水平而變更。此時你無法使用
keyPathsForValuesAffectingTotalSalary
來實現方法並返回鍵
employees.salary
。
針對以上兩種情況,有以下兩種可行的解決辦法:
- 可以使用鍵-值觀察來注冊上級(此例中為Department)為所有下屬對象(此例中為Employees)的相關屬性的觀察者。你必須在添加或移除這個關系中的下屬對象的同時添加或移除這個上級對象的觀察者的身份。在
方法中你需要響應改變來更新依賴的值,如下面的代碼片段所示:observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary
{
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary
{
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
[totalSalary release];
totalSalary = [newTotalSalary retain];
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary
{
return totalSalary;
}
- 如果你使用Core Data,可以使用應用的通知中心來注冊上級對象為其所管理對象內容的觀察者。上級對象應該針對相關的變變更知作出響應,這些變更通知由下級對象以類似於鍵-值觀察的方法發出。