在 MsRoomMessageMainView 和 XPRoomMessageContainerView 中添加了主线程调度以确保 UI 更新的安全性,同时实现了安全插入逻辑以处理数据与 UI 不一致的情况,优化了本地化字符串以提升用户体验。

This commit is contained in:
edwinQQQ
2025-09-22 16:18:07 +08:00
parent ae0f1993e3
commit 3a817bb947
3 changed files with 137 additions and 74 deletions

View File

@@ -112,15 +112,21 @@
#pragma mark - RoomGuestDelegate
- (void)handleNIMCustomMessage:(NIMMessage *)message {
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView handleNIMCustomMessage:message];
});
}
- (void)handleNIMNotificationMessage:(NIMMessage *)message {
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView handleNIMNotificationMessage:message];
});
}
- (void)handleNIMTextMessage:(NIMMessage *)message {
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView handleNIMTextMessage:message];
});
}
- (void)handleNIMImageMessage:(NIMMessage *)message {
@@ -129,16 +135,22 @@
- (void)onRoomMiniEntered {
self.hidden = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView onRoomMiniEntered];
});
}
- (void)onRoomEntered {
self.hidden = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView onRoomEntered];
});
}
- (void)onRoomUpdate {
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView onRoomUpdate];
});
}
#pragma mark - XPRoomMessageContainerViewDelegate
- (void)xPRoomMessageContainerViewlDidTapEmpty:(XPRoomMessageContainerView *)view{
@@ -165,7 +177,9 @@
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageListView handleBroadcastMessage:broadcastMessage];
});
}
#pragma mark -

View File

@@ -93,6 +93,13 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
/// reload
@property (nonatomic, assign) BOOL forceReloadNextAppend;
/// UI
@property (atomic, assign) BOOL viewReady;
///
@property (atomic, assign) BOOL isFlushing;
/// UI viewReady
@property (nonatomic, strong) NSMutableArray<dispatch_block_t> *pendingOps;
@end
@@ -115,6 +122,54 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
return self;
}
/// UI 退 reload
- (void)safeApplyInsertsFromStartIndex:(NSInteger)startIndex
newItemsCount:(NSInteger)newItemsCount
needReload:(BOOL)needReload {
if (needReload || newItemsCount <= 0) {
[self.messageTableView reloadData];
return;
}
if (self.isFlushing) {
//
dispatch_async(dispatch_get_main_queue(), ^{
[self safeApplyInsertsFromStartIndex:startIndex newItemsCount:newItemsCount needReload:NO];
});
return;
}
self.isFlushing = YES;
//
NSInteger beforeRows = [self.messageTableView numberOfRowsInSection:0];
NSInteger expectedRows = [self getCurrentDataSourceCount];
BOOL indexValid = (startIndex >= 0 && startIndex <= beforeRows);
BOOL countValid = (beforeRows + newItemsCount == expectedRows);
if (!indexValid || !countValid) {
[self.messageTableView reloadData];
self.isFlushing = NO;
return;
}
// indexPaths
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
for (NSInteger i = 0; i < newItemsCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]];
}
//
[self.messageTableView beginUpdates];
@try {
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
} @catch (__unused NSException *e) {
[self.messageTableView reloadData];
} @finally {
[self.messageTableView endUpdates];
self.isFlushing = NO;
}
}
- (void)changeType:(NSInteger)type {
if (self.displayType == type) {
return;
@@ -182,6 +237,19 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
///
- (void)appendAndScrollToAtUser {
//
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self appendAndScrollToAtUser];
});
return;
}
if (!self.viewReady) {
if (!self.pendingOps) self.pendingOps = [NSMutableArray array];
__weak typeof(self) weakSelf = self;
[self.pendingOps addObject:^{ __strong typeof(weakSelf) self = weakSelf; [self appendAndScrollToAtUser]; }];
return;
}
// 1. incomingMessages
if (self.incomingMessages.count < 1) {
// 2. locationArray
@@ -245,32 +313,8 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
[self.incomingMessages removeAllObjects];
// 7. UITableView
if (needReloadData || self.forceReloadNextAppend) {
[self.messageTableView reloadData];
[self safeApplyInsertsFromStartIndex:currentRows newItemsCount:tempNewDatas.count needReload:(needReloadData || self.forceReloadNextAppend)];
self.forceReloadNextAppend = NO;
} else if (tempNewDatas.count > 0) {
//
NSInteger expectedRows = [self getCurrentDataSourceCount];
if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) {
[self.messageTableView reloadData];
} else {
// indexPath使
NSMutableArray *indexPaths = @[].mutableCopy;
NSInteger startIndex = currentRows;
if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) {
for (NSInteger i = 0; i < tempNewDatas.count; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]];
}
// 使
[UIView animateWithDuration:0.2 animations:^{
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}];
} else {
[self.messageTableView reloadData];
}
}
}
// 6.
[self scrollToFirstLocationOrBottom];
@@ -523,6 +567,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
[self addSubview:self.messageTipsBtn];
[self addSubview:self.atTipBtn];
self.messageTableView.tableHeaderView = self.headerView;
// flush
self.viewReady = NO;
self.isFlushing = NO;
}
- (void)initSubViewConstraints {
@@ -546,6 +594,18 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
make.bottom.mas_equalTo(self.mas_bottom).offset(-5);
make.leading.mas_equalTo(self);
}];
// flush
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.viewReady) {
self.viewReady = YES;
if (self.pendingOps.count > 0) {
NSArray<dispatch_block_t> *ops = [self.pendingOps copy];
[self.pendingOps removeAllObjects];
for (void (^op)(void) in ops) { op(); }
}
}
});
}
///
@@ -609,6 +669,19 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
///
- (void)appendAndScrollToBottom {
//
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self appendAndScrollToBottom];
});
return;
}
if (!self.viewReady) {
if (!self.pendingOps) self.pendingOps = [NSMutableArray array];
__weak typeof(self) weakSelf = self;
[self.pendingOps addObject:^{ __strong typeof(weakSelf) self = weakSelf; [self appendAndScrollToBottom]; }];
return;
}
if (self.incomingMessages.count < 1) {
///atincomeatatat
[self scrollToBottom:YES];
@@ -646,32 +719,8 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
[self updateAllDataSource:tempArray];
// 使 reloadData使
if (needReloadData || self.forceReloadNextAppend) {
[self.messageTableView reloadData];
[self safeApplyInsertsFromStartIndex:currentRows newItemsCount:tempArray.count needReload:(needReloadData || self.forceReloadNextAppend)];
self.forceReloadNextAppend = NO;
} else if (tempArray.count > 0) {
//
NSInteger expectedRows = [self getCurrentDataSourceCount];
if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) {
[self.messageTableView reloadData];
} else {
// indexPath使
NSMutableArray *indexPaths = @[].mutableCopy;
NSInteger startIndex = currentRows;
if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) {
for (NSInteger i = 0; i < tempArray.count; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]];
}
// 使
[UIView animateWithDuration:0.2 animations:^{
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}];
} else {
[self.messageTableView reloadData];
}
}
}
//
//

View File

@@ -713,7 +713,7 @@
"AnchorLevelTimeView0" = "час(ов)";
"AnchorLevelTimeView1" = "минут(ы)";
"AnchorLevelTimeView2" = "секунд(ы)";
"AnchorLevelTimeView2" = "сек.(ы)";
"AnchorLevelProgressView0" = "Полученные монеты";
"AnchorLevelProgressView1" = "Завершено";
@@ -1237,12 +1237,12 @@
"XPRoomPKTimePickerView0" = "%d минут";
"XPRoomPKTimePickerView1" = "0 минут";
"XPRoomPKTimePickerView2" = "30 минут";
"XPRoomPKTimePickerView3" = "%d минут %d секунд";
"XPRoomPKTimePickerView4" = "%d секунд";
"XPRoomPKTimePickerView3" = "%d минут %d сек.";
"XPRoomPKTimePickerView4" = "%d сек.";
"XPRoomPKTimePickerView5" = "Отмена";
"XPRoomPKTimePickerView7" = "0 секунд";
"XPRoomPKTimePickerView8" = "30 секунд";
"XPRoomPKTimePickerView7" = "0 сек.";
"XPRoomPKTimePickerView8" = "30 сек.";
"XPRoomPKSelectUserView0" = "Номер 0";
@@ -1252,14 +1252,14 @@
"XPRoomPKUserView1" = "Ожидание подключения к микрофону";
"XPRoomPKTimeTableViewCell0" = "Время PK";
"XPRoomPKTimeTableViewCell1" = "30 секунд";
"XPRoomPKTimeTableViewCell1" = "30 сек.";
"XPRoomPKTypeTableViewCell0" = "Режим PK";
"XPRoomPKTypeTableViewCell1" = "Командное PK";
"XPRoomPKVoteTableViewCell0" = "Тип голосования";
"XPRoomPKVoteTableViewCell1" = "Стоим. подар.";
"XPRoomPKVoteTableViewCell2" = "По количеству отправителей подарков";
"XPRoomPKVoteTableViewCell2" = "Дар. чел.";
"XPRoomPKRecordTableViewCell0" = "Ничья";
"XPRoomPKRecordTableViewCell1" = "Ничья";
@@ -1349,7 +1349,7 @@
"XPAcrossRoomPKRuleView0" = "Как начать PK между комнатами";
"XPAcrossRoomPKRuleView1" = "1) Только владелец комнаты и супер-администратор лицензированной комнаты могут инициировать PK между комнатами, и за один раз можно выбрать только одну лицензированную комнату для инициирования;\n2) Только владелец комнаты и супер-администратор могут принимать или отклонять запросы на PK между комнатами. Если в течение 10 секунд не предпринято действий с всплывающим окном приглашения на PK, оно исчезнет, и это будет считаться автоматическим отказом;\n3) При инициировании PK необходимо выбрать время PK и цель PK. Настраиваемый диапазон времени составляет от 5 до 180, и можно вводить только целые числа;\n4) После инициирования PK его нельзя завершить раньше времени PK. В случае особых обстоятельств, требующих досрочного прекращения, обратитесь в службу поддержки, но результат этого PK не будет учитываться.";
"XPAcrossRoomPKRuleView1" = "1) Только владелец комнаты и супер-администратор лицензированной комнаты могут инициировать PK между комнатами, и за один раз можно выбрать только одну лицензированную комнату для инициирования;\n2) Только владелец комнаты и супер-администратор могут принимать или отклонять запросы на PK между комнатами. Если в течение 10 сек. не предпринято действий с всплывающим окном приглашения на PK, оно исчезнет, и это будет считаться автоматическим отказом;\n3) При инициировании PK необходимо выбрать время PK и цель PK. Настраиваемый диапазон времени составляет от 5 до 180, и можно вводить только целые числа;\n4) После инициирования PK его нельзя завершить раньше времени PK. В случае особых обстоятельств, требующих досрочного прекращения, обратитесь в службу поддержки, но результат этого PK не будет учитываться.";
"XPAcrossRoomPKEmptyTableViewCell0" = "Нет результатов поиска";
@@ -1750,11 +1750,11 @@
"XPRoomMessageParser36" = "Результат PK в этом раунде: %@ победил! \nЗначение PK: %@ \n %@ Значение стража: %@";
"XPRoomMessageParser37" = "Побеждает команда с большим количеством полученных подарков";
"XPRoomMessageParser38" = "Побеждает команда с большим количеством отправителей";
"XPRoomMessageParser39" = "Администратор запустил PK комнаты, этот PK длится %.0f секунд";
"XPRoomMessageParser40" = "PK начался, этот PK длится %.0f секунд, быстро проголосуйте за своего любимого участника";
"XPRoomMessageParser39" = "Администратор запустил PK комнаты, этот PK длится %.0f сек.";
"XPRoomMessageParser40" = "PK начался, этот PK длится %.0f сек., быстро проголосуйте за своего любимого участника";
"XPRoomMessageParser41" = "Побеждает команда с большим количеством полученных подарков";
"XPRoomMessageParser42" = "Побеждает команда с большим количеством отправителей";
"XPRoomMessageParser43" = "Администратор перезапустил PK, этот PK длится %.0f секунд, %@";
"XPRoomMessageParser43" = "Администратор перезапустил PK, этот PK длится %.0f сек., %@";
"XPRoomMessageParser44" = "Добавлена комната в закладки";
"XPRoomMessageParser45" = "Сообщение:";
"XPRoomMessageParser46" = "Чат публичного экрана закрыт администратором";
@@ -1823,7 +1823,7 @@
"XPRoomMessageParser114" = "Управление инициировало PK комнаты, этот PK длится %.0f секунд, %@";
"XPRoomMessageParser114" = "Управление инициировало PK комнаты, этот PK длится %.0f сек., %@";
"XPRoomMessageParser115" = "Впечатляюще! ";
"XPRoomMessageParser116" = "Дух сокровища получен ";
@@ -3433,7 +3433,7 @@
"XPFreeGiftsObtainView0"="Понятно";
"XPFreeGiftsObtainView1"="Сегодня смотрели прямой эфир в течение %@, даём вам подарок";
"XPFreeGiftsObtainView2"="Каждый день, просмотр прямого эфира в течение определенного времени приносит вам \"%@\", дневной лимит %@, подарок действителен в течение дня";
"XPFreeGiftsObtainView3"="Секунд";
"XPFreeGiftsObtainView3"="сек.";
"XPFreeGiftsObtainView4"="Минут";
"XPFreeGiftsObtainView5"="Часов";
///XPGiftFreeItemCell
@@ -3558,7 +3558,7 @@
"App_Common_Or" = "Или";
"App_Common_Yuan" = "Юань";
"App_Common_Wan" = "Тысяча";
"App_Common_Zero_Second" = "0 секунд";
"App_Common_Zero_Second" = "0 сек.";
"App_Common_zai" = "В";
"App_Common_bei" = "От";
"App_Common_gei" = "Для";
@@ -3930,7 +3930,7 @@
"20.20.51_text_27" = "1 Каждый раз, когда вы загружаете динамический аватар, система списывает %@ монет с вашего аккаунта. Пожалуйста, убедитесь, что баланс вашего аккаунта достаточен.\n2 Процесс проверки: После завершения загрузки система автоматически переходит в процесс проверки. В это время не загружайте новый аватар, чтобы не повлиять на прогресс проверки.\n3 Результаты проверки:\nПроверка пройдена: Ваш новый динамический аватар вступит в силу немедленно и будет отображаться другим пользователям.\nПроверка не пройдена: После завершения проверки система автоматически вернет вам потраченные %@ монеты. В то же время вы получите системное уведомление.\n4 Нереверсивная операция: Пожалуйста, обратите внимание, что после проверки и вступления в силу динамический аватар вы не сможете восстановить предыдущий аватар.\n";
"20.20.51_text_28" = "Время просмотра: %@";
"20.20.51_text_29" = "Количество алмазов должно быть кратно 1000";
"20.20.51_text_30" = "Продолжительность видео не должна превышать 10 секунд.";
"20.20.51_text_30" = "Продолжительность видео не должна превышать 10 сек..";
"20.20.56_text_1" = "Текущее количество матчей недостаточно, матч не удался. Сыграть снова?";
"20.20.56_text_2" = "Матч не удался, потраченные вами монеты возвращены в Кошелёк";