Фон
В моем приложении у меня есть класс FavoritesController, который управляет объектами, которые пользователь пометил как избранные, и этот статус избранного затем используется во всем приложении.FavoritesController разработан как одноэлементный класс, так как в приложении имеется ряд элементов пользовательского интерфейса, которым необходимо знать «статус избранного» для объектов в разных местах, а также сетевые запросы должны иметь возможность сигнализировать о том, что избранное должно быть признано недействительным.если сервер так говорит.
Эта часть аннулирования происходит, когда сервер отвечает ошибкой 404, указывающей, что избранный объект должен быть удален из избранного пользователя.Функция сетевого извлечения выдает ошибку, которая запускает FavoritesController для удаления объекта, а затем отправляет заинтересованным сторонам уведомление о необходимости обновления.
Проблема
При использовании модульного теста дляпроверьте качество реализации 404, все методы запускаются как задумано - ошибка выдается и перехватывается, FavoritesController удаляет объект и отправляет уведомление.В некоторых случаях, однако, удаленный фаворит все еще там - но это зависит от того, где запрос сделан!
Если я запрашиваю внутри одиночного, удаление прошло нормально, но если я запрашиваю из класса, который используетдля синглтона удаление не произошло.
Сведения о проекте
- Свойство FavoritesController
favorites
использует ивар со всеми доступами @synchronized()
и значением ивара.поддерживается свойством NSUserDefaults. - Любимым объектом является NSDictionary с двумя ключами:
id
и name
.
Другая информация
Одна странная вещь, которую я не понимаю, почему это происходит: при некоторых попытках удаления значение name
для избранного объекта устанавливается равным ""
, но ключ id
сохраняет свое значение.
Я написал модульные тесты, которые добавляют недопустимое избранное и проверяют, удаляется ли оно при первом запросе к серверу.Этот тест проходит при запуске с пустым набором избранного, но не проходит, когда существует экземпляр «полуудаленного» объекта, как указано выше (который сохраняет свое значение id
)
- ЕдиницаТестирование теперь последовательно проходит, но в режиме реального времени не удаляется удаление.Я подозреваю, что это связано с тем, что NSUserDefaults не сохраняет данные на диск сразу.
Шаги, которые я пробовал
- Убедившись, что реализация синглтона является 'истинным' синглтоном,т. е.
sharedController
всегда возвращает один и тот же экземпляр. - Я думал, что существует какая-то проблема 'захвата', когда закрытие будет сохранять свою собственную копию с устаревшими фаворитами, но я думаю, что нет.При NSLogging ID объекта он возвращает то же самое.
Код
Основные методы FavoritesController
- (void) serverCanNotFindFavorite:(NSInteger)siteID {
NSLog(@"Server can't find favorite");
NSDictionary * removedFavorite = [NSDictionary dictionaryWithDictionary:[self favoriteWithID:siteID]];
NSUInteger index = [self indexOfFavoriteWithID:siteID];
[self debugLogFavorites];
dispatch_async(dispatch_get_main_queue(), ^{
[self removeFromFavorites:siteID completion:^(BOOL success) {
if (success) {
NSNotification * note = [NSNotification notificationWithName:didRemoveFavoriteNotification object:nil userInfo:@{@"site" : removedFavorite, @"index" : [NSNumber numberWithUnsignedInteger:index]}];
NSLog(@"Will post notification");
[self debugLogFavorites];
[self debugLogUserDefaultsFavorites];
[[NSNotificationCenter defaultCenter] postNotification:note];
NSLog(@"Posted notification with name: %@", didRemoveFavoriteNotification);
}
}];
});
}
- (void) removeFromFavorites:(NSInteger)siteID completion:(completionBlock) completion {
if ([self isFavorite:siteID]) {
NSMutableArray * newFavorites = [NSMutableArray arrayWithArray:self.favorites];
NSIndexSet * indices = [newFavorites indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber * value = (NSNumber *)[entryUnderTest objectForKey:@"id"];
if ([value isEqualToNumber:[NSNumber numberWithInteger:siteID]]) {
return YES;
}
return NO;
}];
__block NSDictionary* objectToRemove = [[newFavorites objectAtIndex:indices.firstIndex] copy];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Will remove %@", objectToRemove);
[newFavorites removeObject:objectToRemove];
[self setFavorites:[NSArray arrayWithArray:newFavorites]];
if ([self isFavorite:siteID]) {
NSLog(@"Failed to remove!");
if (completion) {
completion(NO);
}
} else {
NSLog(@"Removed OK");
if (completion) {
completion(YES);
}
}
});
} else {
NSLog(@"Tried removing site %li which is not a favorite", (long)siteID);
if (completion) {
completion(NO);
}
}
}
- (NSArray *) favorites
{
@synchronized(self) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
return internalFavorites;
}
}
- (void) setFavorites:(NSArray *)someFavorites {
@synchronized(self) {
internalFavorites = someFavorites;
[self.defaults setObject:internalFavorites forKey:k_key_favorites];
}
}
- (void) addToFavorites:(NSInteger)siteID withName:(NSString *)siteName {
if (![self isFavorite:siteID]) {
NSDictionary * newFavorite = @{
@"name" : siteName,
@"id" : [NSNumber numberWithInteger:siteID]
};
dispatch_async(dispatch_get_main_queue(), ^{
NSArray * newFavorites = [self.favorites arrayByAddingObject:newFavorite];
[self setFavorites:newFavorites];
});
NSLog(@"Added site %@ with id %ld to favorites", siteName, (long)siteID);
} else {
NSLog(@"Tried adding site as favorite a second time");
}
}
- (BOOL) isFavorite:(NSInteger)siteID
{
@synchronized(self) {
NSNumber * siteNumber = [NSNumber numberWithInteger:siteID];
NSArray * favs = [NSArray arrayWithArray:self.favorites];
if (favs.count == 0) {
NSLog(@"No favorites");
return NO;
}
NSIndexSet * indices = [favs indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
if ([[entryUnderTest objectForKey:@"id"] isEqualToNumber:siteNumber]) {
return YES;
}
return NO;
}];
if (indices.count > 0) {
return YES;
}
}
return NO;
}
Синглтонная реализация FavoritesController
- (instancetype) init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype) sharedController
{
return [self new];
}
Код модульного тестирования
func testObsoleteFavoriteRemoval() {
let addToFavorites = self.expectation(description: "addToFavorites")
let networkRequest = self.expectation(description: "network request")
unowned let favs = PKEFavoritesController.shared()
favs.clearFavorites()
XCTAssertFalse(favs.isFavorite(313), "Should not be favorite initially")
if !favs.isFavorite(313) {
NSLog("Adding 313 to favorites")
favs.add(toFavorites: 313, withName: "Skatås")
}
let notification = self.expectation(forNotification: NSNotification.Name("didRemoveFavoriteNotification"), object: nil) { (notification) -> Bool in
NSLog("Received notification: \(notification.name.rawValue)")
return true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
NSLog("Verifying 313 is favorite")
XCTAssertTrue(favs.isFavorite(313))
addToFavorites.fulfill()
}
self.wait(for: [addToFavorites], timeout: 5)
NSLog("Will trigger removal for 313")
let _ = SkidsparAPI.fetchRecentReports(forSite: 313, session: SkidsparAPI.session()) { (reports) in
NSLog("Network request completed")
networkRequest.fulfill()
}
self.wait(for: [networkRequest, notification], timeout: 10)
XCTAssertFalse(favs.isFavorite(313), "Favorite should be removed after a 404 error from server")
}