電子の海をたゆたう

iOS初心者がXcodeでアプリ開発を学んでいく日記です。

NSUserDefaultsがobserveValueForKeyPathで2回呼ばれてる問題

iOS9.3がリリースされ、アプリのテストをした際に発見したバグ?について投稿しておこうと思います。

KVOでNSUserDefaultsのキー値の変化を監視するシステムを作ったのですが、NSUserDefaultが2回、observeValueForKeyPathに呼び出されているようです。

試しにNSUserDefaultsではなく、NSMutableDictionaryで値の監視を行ったところ2回呼び出されず、1回だけログに出力がされました。

なお、この現象はこちらで確認した限りではiOS9.3でのみ起こっており、9.2以下では発生しませんでした。


NSUserDefaultsのコードと出力結果

#import "ViewController.h"

@interface ViewController (){
}
@end

@implementation ViewController
- (void)dealloc{
    [super dealloc];
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    [ud removeObserver:self forKeyPath:TESTIOS93_FIRST];
    [ud removeObserver:self forKeyPath:TESTIOS93_SECOND];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    dict = [NSMutableDictionary dictionaryWithDictionary:@{TESTIOS93_FIRST:@"This is key1.", TESTIOS93_SECOND:@"This is key2."}];
    [ud addObserver:self forKeyPath:TESTIOS93_FIRST options:0 context:nil];
    [ud addObserver:self forKeyPath:TESTIOS93_SECOND options:0 context:nil];
    [self changeKeyValue];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    NSLog(@"-------------------------");
    NSLog(@"call observeValueForKeyPath");
    if([keyPath isEqualToString:TESTIOS93_FIRST] == YES){
        NSLog(@"TESTIOS93_FIRST CALLING = %@",[ud objectForKey:TESTIOS93_FIRST]);
        
    }else if([keyPath isEqualToString:TESTIOS93_SECOND] == YES){
        NSLog(@"TESTIOS93_SECOND CALLING = %@",[ud objectForKey:TESTIOS93_SECOND]);
    }
}
- (void)changeKeyValue{
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    [ud setValue:@"change key1 KVO Test" forKey:TESTIOS93_FIRST];
    [ud setValue:@"change key2 KVO Test" forKey:TESTIOS93_SECOND];
    [ud synchronize];
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end

出力結果

2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] -------------------------
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] call observeValueForKeyPath
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] TESTIOS93_FIRST CALLING = change key1 KVO Test
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] -------------------------
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] call observeValueForKeyPath
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] TESTIOS93_FIRST CALLING = change key1 KVO Test
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] -------------------------
2016-03-25 12:07:15.442 iOS9.3_KVOTest[1711:469489] call observeValueForKeyPath
2016-03-25 12:07:15.443 iOS9.3_KVOTest[1711:469489] TESTIOS93_SECOND CALLING = change key2 KVO Test
2016-03-25 12:07:15.443 iOS9.3_KVOTest[1711:469489] -------------------------
2016-03-25 12:07:15.443 iOS9.3_KVOTest[1711:469489] call observeValueForKeyPath
2016-03-25 12:07:15.443 iOS9.3_KVOTest[1711:469489] TESTIOS93_SECOND CALLING = change key2 KVO Test

******************************************************************************************

NSMutableDictionaryのコードと出力結果

#import "ViewController.h"

@interface ViewController (){
    NSMutableDictionary *dict;
}
@end

@implementation ViewController
- (void)dealloc{
    [super dealloc];
    [dict removeObserver:self forKeyPath:TESTIOS93_FIRST];
    [dict removeObserver:self forKeyPath:TESTIOS93_SECOND];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    dict = [NSMutableDictionary dictionaryWithDictionary:@{TESTIOS93_FIRST:@"This is key1.", TESTIOS93_SECOND:@"This is key2."}];
    [dict addObserver:self forKeyPath:TESTIOS93_FIRST options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [dict addObserver:self forKeyPath:TESTIOS93_SECOND options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [self changeKeyValue];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"-------------------------");
    NSLog(@"call observeValueForKeyPath");
    
    if([keyPath isEqualToString:TESTIOS93_FIRST] == YES){
        NSLog(@"TESTIOS93_FIRST CALLING = %@",[dict objectForKey:TESTIOS93_FIRST]);
    }else if([keyPath isEqualToString:TESTIOS93_SECOND] == YES){
        NSLog(@"TESTIOS93_SECOND CALLING = %@",[dict objectForKey:TESTIOS93_SECOND]);
    }
}
- (void)changeKeyValue{
    [dict setObject:@"change key1 KVO Test" forKey:TESTIOS93_FIRST];
    [dict setObject:@"change key2 KVO Test" forKey:TESTIOS93_SECOND];
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end

出力結果

2016-03-25 12:01:26.001 iOS9.3_KVOTest[1676:466919] -------------------------
2016-03-25 12:01:26.002 iOS9.3_KVOTest[1676:466919] call observeValueForKeyPath
2016-03-25 12:01:26.002 iOS9.3_KVOTest[1676:466919] TESTIOS93_FIRST CALLING = change key1 KVO Test
2016-03-25 12:01:26.002 iOS9.3_KVOTest[1676:466919] -------------------------
2016-03-25 12:01:26.002 iOS9.3_KVOTest[1676:466919] call observeValueForKeyPath
2016-03-25 12:01:26.002 iOS9.3_KVOTest[1676:466919] TESTIOS93_SECOND CALLING = change key2 KVO Test

ちらほらと報告が上がってるみたいですが、このようなことは初めて経験したので正直焦りました...