生命周期中的self.view.bounds的值

本文探讨了iOS应用中视图控制器的生命周期问题,特别是在`viewDidLoad`与`viewWillAppear`方法中`self.view.bounds`值的变化,并给出了实际案例分析。


在方法`- (void)viewDidLoad`中添加`_parentScrollView`,将`_parentScrollView `拖拽到底部,和预期效果有细微差别,马上意识到可能是控制器的不同生命周期方法中的self.view.bounds的不同数值引起的。
![实现效果.png](https://upload-images.jianshu.io/upload_images/792843-cfe730375abcf9fe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![预期效果.png](https://upload-images.jianshu.io/upload_images/792843-eddcf7b140e5d5f2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

运行机型:6P手机
在方法`- (void)viewDidLoad`中`self.view.bounds`的值为:
`(origin = (x = 0, y = 0), size = (width = 414, height = 736))`

在方法`- (void)viewWillAppear:(BOOL)animated`中`self.view.bounds`的值为:
`(origin = (x = 0, y = 0), size = (width = 414, height = 672))`

总结:高度相差64(导航栏+状态栏高度),控制器vc的视图view在方法`- (void)viewDidLoad`中没有加载完成,需要使用正确的`self.view.bounds`的值,需要在方法`- (void)viewWillAppear:(BOOL)animated`中使用
 

#import "TPNavigationController.h" #import "TPContainerViewController.h" #import "TPContainerNavigationController.h" #import "UIViewController+TPNavigationExtension.h" @interface TPContainerViewController () @property (nonatomic, strong) __kindof UIViewController *rootViewController; @property (nonatomic, strong) TPContainerNavigationController *containerNavigationController; @end @implementation TPContainerViewController // 包装过程 + (instancetype)wrapViewController:(UIViewController *)viewController { return [[self alloc] initWithViewController:viewController]; } // 包装过程 - (instancetype)initWithViewController:(UIViewController *)viewController { if (self = [super init]) { // 先将viewController包装在TPContainerNavigationController下面 self.containerNavigationController = [TPContainerNavigationController wrapNavigationControllerWithViewController:viewController]; // 再将TPContainerNavigationController包装在TPContainerViewController下面 [self addChildViewController:self.containerNavigationController]; [self.containerNavigationController didMoveToParentViewController:self]; // 记录控制器 self.rootViewController = viewController; self.containerNavigationController.containerViewContorller = self; } return self; } - (void)dealloc { [self.containerNavigationController removeFromParentViewController]; self.containerNavigationController = nil; } - (void)viewDidLoad { [super viewDidLoad]; // 添加视图 [self.view addSubview:self.containerNavigationController.view]; self.containerNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.containerNavigationController.view.frame = self.view.bounds; } - (void)didMoveToParentViewController:(UIViewController *)parent { if (parent == nil) { [self.containerNavigationController removeFromParentViewController]; self.containerNavigationController = nil; } } - (BOOL)shouldAutorotate { return self.rootViewController.shouldAutorotate; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return self.rootViewController.preferredInterfaceOrientationForPresentation; } - (BOOL)becomeFirstResponder { return [self.rootViewController becomeFirstResponder]; } - (BOOL)canBecomeFirstResponder { return [self.rootViewController canBecomeFirstResponder]; } - (UIStatusBarStyle)preferredStatusBarStyle { return self.rootViewController.preferredStatusBarStyle; } - (BOOL)prefersStatusBarHidden { return self.rootViewController.prefersStatusBarHidden; } - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation { return self.rootViewController.preferredStatusBarUpdateAnimation; } - (BOOL)hidesBottomBarWhenPushed { return self.rootViewController.hidesBottomBarWhenPushed; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return self.rootViewController.supportedInterfaceOrientations; } - (UITabBarItem *)tabBarItem { return self.rootViewController.tabBarItem; } - (NSString *)title { return self.rootViewController.title; } - (UIViewController *)childViewControllerForStatusBarStyle { return self.rootViewController; } - (UIViewController *)childViewControllerForStatusBarHidden { return self.rootViewController; } - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { return self.rootViewController.preferredScreenEdgesDeferringSystemGestures; } - (UIViewController *)rootViewController { TPContainerNavigationController *containerNavController = self.childViewControllers.firstObject; return containerNavController.viewControllers.firstObject; } @end
09-27
这里干了什么:override func viewDidLoad() { super.viewDidLoad() let height: CGFloat = 70 let rect = UIScreen.main.bounds let screenWidth = rect.size.width let screenHeight = rect.size.height let frameWidth = screenHeight > screenWidth ? screenWidth : screenHeight var width: CGFloat width = (frameWidth - 2) / CGFloat(3) width = floor(width) thumnailSize = CGSize.init(width: width, height: height) initView() setGraduallyColorForBottomView() // bottomView.layer.addSublayer(gradient) bottomView.backgroundColor = UIColor.clear setGraduallyColorForTabBarView() self.view.sendSubviewToBack(bottomView) _collectionView?.contentInsetAdjustmentBehavior = .never self.initTimeLable() screenShotImageView.layer.borderColor = UIColor.tpbCard.cgColor flowWindowView.delegate = self let albumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(jumpToAlbum)) screenShotImageView.addGestureRecognizer(albumGestureRecognizer) screenShotImageView.isUserInteractionEnabled = true self.view.bringSubviewToFront(screenShotImageView) addCustomNavigationLeftBarButtonItem(title: nil, image: TPImageLiteral("common_light_back_nor")) mediaPlayerWindowViewController.setVideoDisplayViewIsHidden(isHidden: true) NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) addNotification() setupCollectionview() setUpToolBarView() //增加TouchView判断屏幕点击事件 setupTouchView() self.view.backgroundColor = .tpbBackground flowWindowView.isHidden = true self.landscapeDeviceTitle.isHidden = true if isCloudVMS || isLocalVms { // siteMessageArray = appContext.getSiteMessagesList() self.changeMessagePlayingIndex(from: IndexPath.init(row: currentMessageIndex, section: 0)) } else { shareInfoId = self.device.isSharedDevice ? -1 : 0 if playFromCloudStorage { // mediaPlayerWindowViewController.setPlaybackType(Int(TPSS_MP_PLAYBACK_TYPE_CLOUD_STORAGE_MSG)) // mediaPlayerWindowViewController.addCloudStorageDevice(windowConfig!, Int64(devTime)) } else { mediaPlayerWindowViewController.setPlaybackType(Int(TPSS_MP_PLAYBACK_TYPE_MSG_PLAYBACK)) mediaPlayerWindowViewController.setSelectedPlayerPlaybackDate(calendar.date(from: playbackDate)! as NSDate) self.windowConfig?.playbackDate = calendar.date(from:self.startTime as DateComponents) mediaPlayerWindowViewController.addSingleDevice(windowConfig!) mediaPlayerWindowViewController.upgradeForegroundWindows(false, false, calendar.date(from: startTime as DateComponents)!.timeIntervalSince1970) } if let message = TPSSMessageManager.message(at: currentMessageIndex) as? TPSSMessage { if message.messageType != .event || message.deviceType == .solar{ mediaPlayerWindowViewController.seekSelectedPlayerForChangeMessage(0) self.setIsNoRecordingmessage(isNoRecording: true) return } else { self.setIsNoRecordingmessage(isNoRecording: false) } } } // 获取倍速回放能力集 if mediaPlayerWindowViewController.isSelectedWindowDeviceSupportPlaybackScale() { speedArray = appContext.playbackScaleCapability(for: TPSSDeviceIdentifier(mediaPlayerWindowViewController.getSelectedWindowDeviceId().deviceId), of: TPSSDeviceListType(rawValue: mediaPlayerWindowViewController.getSelectedWindowDeviceId().listType)!) selectedSpeedIndex = self.getDefaultSpeedIndex(speedArray: speedArray) } loadModuleSpec() }
10-15
好好检查一下,另外我在发给你父类的m文件参考 @interface TPBCommonTableController () < TPBTableCommonInputDelegate, TPBTableListInputDelegate > @property (nonatomic, assign) UITableViewStyle tableViewStyle; @property (nonatomic, strong) UIView *tpbSearchBarPlaceholderView; @property (nonatomic, strong) TPBTableSearchHeader *tpbInnerSearchBar; @property (nonatomic, strong) TPBEmptyView *emptyView; @property (nonatomic, strong) TPKeyboardAvoidingTableView *tableView; @property (nonatomic, strong) MASConstraint *emptyCenterYConstraint; @property (nonatomic, strong) MASConstraint *searchBarTopConstraint; @property (nonatomic, strong) MASConstraint *searchBarHeightConstraint; @end @implementation TPBCommonTableController - (instancetype)init { if (@available(iOS 13.0, *)) { return [self initWithTableViewStyle:UITableViewStyleInsetGrouped]; } else { return [self initWithTableViewStyle:UITableViewStyleGrouped]; } } - (instancetype)initWithTableViewStyle:(UITableViewStyle)style { if (self = [super initWithNibName:nil bundle:nil]) { _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { UITableViewStyle style = UITableViewStyleGrouped; if (@available(iOS 13.0, *)) { style = UITableViewStyleInsetGrouped; } _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { UITableViewStyle style = UITableViewStyleGrouped; if (@available(iOS 13.0, *)) { style = UITableViewStyleInsetGrouped; } _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (void)dealloc { [self.view tpbRemoveForKeyboardEvent]; } - (void)tpbSetupInitialData { [super tpbSetupInitialData]; self.keyboardBehavior = TPBCommonTableKeyboardBehaviorDismissOnly; } - (void)tpbSetupSubviews { [super tpbSetupSubviews]; [self.tableView insertSubview:self.emptyView atIndex:0]; [self.view addSubview:self.tableView]; [self.view addSubview:self.tpbInnerSearchBar]; } - (void)tpbMakeConstraint { [super tpbMakeConstraint]; [self.emptyView mas_remakeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); self.emptyCenterYConstraint = make.centerY.equalTo(self.mas_tpSafeAreaLayoutGuide); }]; [self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_tpSafeAreaLayoutGuide); make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); make.bottom.equalTo(self.view); }]; CGFloat searchHeight = [self.tpbInnerSearchBar preferredHeight]; [self.tpbInnerSearchBar mas_remakeConstraints:^(MASConstraintMaker *make) { self.searchBarTopConstraint = make.top.equalTo(self.mas_tpSafeAreaLayoutGuide).offset(0); make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); self.searchBarHeightConstraint = make.height.equalTo(@(searchHeight)); }]; } - (void)tpbBindActions { [super tpbBindActions]; TPBWeakSelf [self tpbAddContentSizeDidChangeConfig:^(id _Nonnull object, TPBDynamicContentManager * _Nonnull manager) { TPBStrongSelf dispatch_async(dispatch_get_main_queue(), ^{ [_self updateTableHeader]; [_self.tableView reloadData]; }); } notifyWhenRegister:NO]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self.view bringSubviewToFront:self.tpbInnerSearchBar]; [self updateSearchBarPosition]; } #pragma mark - Public - (void)setSectionArray:(NSArray<TPBTableSectionModel *> *)sectionArray { _sectionArray = sectionArray; [self.tableView reloadData]; } - (void)setSearchBarShow:(BOOL)searchBarShow { _searchBarShow = searchBarShow; self.tpbInnerSearchBar.hidden = !searchBarShow; [self updateTableHeader]; } - (NSString *)searchPlaceholder { return self.tpbInnerSearchBar.searchPlaceholder; } - (void)setSearchPlaceholder:(NSString *)searchPlaceholder { self.tpbInnerSearchBar.searchPlaceholder = searchPlaceholder; } - (void)updateSearchKey:(NSString *)searchKey { [self.tpbInnerSearchBar updateSearchKey:searchKey]; } - (void)setCustomTableHeaderView:(UIView *)customTableHeaderView { _customTableHeaderView = customTableHeaderView; [self updateTableHeader]; } - (void)setKeyboardBehavior:(TPBCommonTableKeyboardBehavior)keyboardBehavior { _keyboardBehavior = keyboardBehavior; switch (keyboardBehavior) { case TPBCommonTableKeyboardBehaviorDismissOnly: [self.view tpbRegisterForKeyboardEvent]; break; case TPBCommonTableKeyboardBehaviorDismissAndRespondClick: [self.view tpbRemoveForKeyboardEvent]; break; default: break; } } - (void)focusToFirstEligibleElement { NSIndexPath *indexPath = nil; for (NSUInteger section = 0; section < self.sectionArray.count; section++) { TPBTableSectionModel *sectionModel = self.sectionArray[section]; for (NSUInteger row = 0; row < sectionModel.cellModelArray.count; row++) { TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[row]; if (cellModel.isAutoFocusEnabled) { indexPath = [NSIndexPath indexPathForRow:row inSection:section]; break; } } if (indexPath != nil) break; } if (indexPath == nil) return; UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; if (cell == nil) return; if ([TPBA11yHelper isVoiceOverOn]) { UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, cell); } else { [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; } } #pragma mark - UITableViewDataSource & UITableViewDelegate // 注册自定义Cell - (void)registerCustomCellWithCellModel:(TPBCustomTableCellBaseModel *)customCellModel tableView:(UITableView *)tableView cellIdentifier:(NSString *)cellIdentifier { if (customCellModel.cellClass != nil) { BOOL isCellClassValid = [customCellModel.cellClass isSubclassOfClass:[UITableViewCell class]]; BOOL isValid = isCellClassValid; NSAssert(isCellClassValid, @"TPBCustomBaseTableCellModel's cellClass「%@」is not Subclass of UITableViewCell!", NSStringFromClass(customCellModel.cellClass)); if (!isValid) { return; } // 注册Cell Class [tableView registerClass:customCellModel.cellClass forCellReuseIdentifier:cellIdentifier]; } else if (customCellModel.cellNib != nil) { // 注册Cell Nib [tableView registerNib:customCellModel.cellNib forCellReuseIdentifier:cellIdentifier]; } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.sectionArray.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return 0; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; return sectionModel.cellModelArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return [[UITableViewCell alloc] init]; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return [[UITableViewCell alloc] init]; } BOOL isRTL = self.view.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; BOOL isLast = indexPath.row == sectionModel.cellModelArray.count - 1; TPBBaseTableCellModel *model = sectionModel.cellModelArray[indexPath.row]; switch (model.cellType) { case TPBTableCellTypeCustomCell: { if ([model isKindOfClass:[TPBCustomTableCellBaseModel class]]) { TPBCustomTableCellBaseModel *cellModel = (TPBCustomTableCellBaseModel *)model; NSString *cellIdentifier = [cellModel effectiveCellIdentifier:isRTL]; UITableViewCell *cell; if (TPBA11yHelper.isVoiceOverOn && cellModel.cellClass != nil) { cell = [[cellModel.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } if (cell == nil) { // 若无法获取Cell,说明可能是未注册自定义Cell,则注册自定义Cell之后尝试重新取 [self registerCustomCellWithCellModel:cellModel tableView:tableView cellIdentifier:cellIdentifier]; cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } if (cellModel.cellConfigCallback) { cellModel.cellConfigCallback(tableView, indexPath, cell, cellModel); } if ([cell isKindOfClass:[TPBBaseTableViewCell class]]) { TPBBaseTableViewCell *baseCell = (TPBBaseTableViewCell *)cell; [baseCell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; } return cell; } } break; case TPBTableCellTypeCustomView: { if ([model isKindOfClass:[TPBCustomViewTableCellModel class]]) { TPBCustomViewTableCellModel *cellModel = (TPBCustomViewTableCellModel *)model; NSString *cellIdentifier = [TPBCustomViewTableCell cellIdentifier:isRTL]; TPBCustomViewTableCell *cell; cell = [[TPBCustomViewTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeTitleSubtitle: { if ([model isKindOfClass:[TPBTitleSubtitleTableCellModel class]]) { TPBTitleSubtitleTableCellModel *cellModel = (TPBTitleSubtitleTableCellModel *)model; NSString *cellIdentifier = [TPBTitleSubtitleTableCell cellIdentifier:isRTL]; TPBTitleSubtitleTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBTitleSubtitleTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeSwitch: { if ([model isKindOfClass:[TPBSwitchTableCellModel class]]) { TPBSwitchTableCellModel *cellModel = (TPBSwitchTableCellModel *)model; NSString *cellIdentifier = [TPBSwitchTableCell cellIdentifier:isRTL]; TPBSwitchTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBSwitchTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeListButton: { if ([model isKindOfClass:[TPBListButtonTableCellModel class]]) { TPBListButtonTableCellModel *cellModel = (TPBListButtonTableCellModel *)model; NSString *cellIdentifier = [TPBListButtonTableCell cellIdentifier:isRTL]; TPBListButtonTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBListButtonTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeProgress: { if ([model isKindOfClass:[TPBProgressTableCellModel class]]) { TPBProgressTableCellModel *cellModel = (TPBProgressTableCellModel *)model; NSString *cellIdentifier = [TPBProgressTableCell cellIdentifier:isRTL]; TPBProgressTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBProgressTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeCommonInput: { if ([model isKindOfClass:[TPBCommonInputTableCellModel class]]) { TPBCommonInputTableCellModel *cellModel = (TPBCommonInputTableCellModel *)model; NSString *cellIdentifier = [TPBCommonInputTableCell cellIdentifier:isRTL]; TPBCommonInputTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBCommonInputTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } cell.inputDelegate = self; [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeCheck: { if ([model isKindOfClass:[TPBCheckTableCellModel class]]) { TPBCheckTableCellModel *cellModel = (TPBCheckTableCellModel *)model; BOOL isCustomizedCellHeight = cellModel.height.isCustomHeight; NSString *cellIdentifier = [TPBCheckTableCell cellIdentifier:isRTL]; TPBCheckTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBCheckTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel isCustomizedCellHeight:isCustomizedCellHeight]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeJumpSelect: { if ([model isKindOfClass:[TPBJumpSelectTableCellModel class]]) { TPBJumpSelectTableCellModel *cellModel = (TPBJumpSelectTableCellModel *)model; NSString *cellIdentifier = [TPBJumpSelectTableCell cellIdentifier:isRTL]; TPBJumpSelectTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBJumpSelectTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && TPBIsEmptyString(cellModel.title) && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeMenuSelect: { if ([model isKindOfClass:[TPBMenuSelectTableCellModel class]]) { TPBMenuSelectTableCellModel *cellModel = (TPBMenuSelectTableCellModel *)model; NSString *cellIdentifier = [TPBMenuSelectTableCell cellIdentifier:isRTL]; TPBMenuSelectTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBMenuSelectTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeListInput: { if ([model isKindOfClass:[TPBListInputTableCellModel class]]) { TPBListInputTableCellModel *cellModel = (TPBListInputTableCellModel *)model; NSString *cellIdentifier = [TPBListInputTableCell cellIdentifier:isRTL]; TPBListInputTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBListInputTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } cell.inputDelegate = self; [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeTitleAction: { if ([model isKindOfClass:[TPBTitleActionTableCellModel class]]) { TPBTitleActionTableCellModel *cellModel = (TPBTitleActionTableCellModel *)model; NSString *cellIdentifier = [TPBTitleActionTableCell cellIdentifier:isRTL]; TPBTitleActionTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBTitleActionTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; } return [[UITableViewCell alloc] init]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return UITableViewAutomaticDimension; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return UITableViewAutomaticDimension; } TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[indexPath.row]; if (cellModel.height.isCustomHeight) { return cellModel.height.customHeight; } return UITableViewAutomaticDimension; } // Header - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return [UIView new]; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.customHeaderView != nil) { return sectionModel.customHeaderView; } if (TPBIsEmptyString(sectionModel.headerTitle) && sectionModel.headerAction == nil) { return [UIView new]; } TPBTableTextSectionHeader *headerView = [TPBTableTextSectionHeader new]; [headerView updateSectionModel:sectionModel]; return headerView; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return TPBDesign.list.sectionHeaderHeight; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.sectionHeaderHeight.isCustomHeight) { return sectionModel.sectionHeaderHeight.customHeight; } if (sectionModel.customHeaderView != nil) { return UITableViewAutomaticDimension; } if (TPBIsEmptyString(sectionModel.headerTitle) && sectionModel.headerAction == nil) { if (self.searchBarShow && section == 0) { return 10; } return TPBDesign.list.sectionHeaderHeight; } return UITableViewAutomaticDimension; } // Footer - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return [UIView new]; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.customFooterView != nil) { return sectionModel.customFooterView; } if (TPBIsEmptyString(sectionModel.footerTitle) && sectionModel.footerAction == nil) { return [UIView new]; } TPBTableTextSectionFooter *footerView = [TPBTableTextSectionFooter new]; [footerView updateSectionModel:sectionModel]; return footerView; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return TPBDesign.list.sectionFooterHeight; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.sectionFooterHeight.isCustomHeight) { return sectionModel.sectionFooterHeight.customHeight; } if (sectionModel.customFooterView != nil) { return UITableViewAutomaticDimension; } if (TPBIsEmptyString(sectionModel.footerTitle) && sectionModel.footerAction == nil) { return TPBDesign.list.sectionFooterHeight; } return UITableViewAutomaticDimension; } // TableViewCell点击 - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return NO; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return NO; } TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[indexPath.row]; switch (cellModel.cellType) { case TPBTableCellTypeCustomCell: case TPBTableCellTypeCustomView: case TPBTableCellTypeTitleSubtitle: case TPBTableCellTypeSwitch: case TPBTableCellTypeCheck: case TPBTableCellTypeListButton: case TPBTableCellTypeJumpSelect: case TPBTableCellTypeMenuSelect: case TPBTableCellTypeProgress: case TPBTableCellTypeTitleAction: return YES; case TPBTableCellTypeCommonInput: case TPBTableCellTypeListInput: return NO; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return; } if (self.keyboardBehavior == TPBCommonTableKeyboardBehaviorDismissAndRespondClick) { [self tpbHideKeyboard]; } TPBBaseTableCellModel *model = sectionModel.cellModelArray[indexPath.row]; switch (model.cellType) { case TPBTableCellTypeCustomCell: { if ([model isKindOfClass:[TPBCustomTableCellBaseModel class]]) { TPBCustomTableCellBaseModel *cellModel = (TPBCustomTableCellBaseModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeCustomView: { if ([model isKindOfClass:[TPBCustomViewTableCellModel class]]) { TPBCustomViewTableCellModel *cellModel = (TPBCustomViewTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeTitleSubtitle: { if ([model isKindOfClass:[TPBTitleSubtitleTableCellModel class]]) { TPBTitleSubtitleTableCellModel *cellModel = (TPBTitleSubtitleTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeSwitch: { if ([model isKindOfClass:[TPBSwitchTableCellModel class]]) { TPBSwitchTableCellModel *cellModel = (TPBSwitchTableCellModel *)model; if (cellModel.switchDidClickHotZoneCallback) { cellModel.switchDidClickHotZoneCallback(); } } } break; case TPBTableCellTypeListButton: { if ([model isKindOfClass:[TPBListButtonTableCellModel class]]) { TPBListButtonTableCellModel *cellModel = (TPBListButtonTableCellModel *)model; if (cellModel.actionEnabled && cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeCheck: { if ([model isKindOfClass:[TPBCheckTableCellModel class]]) { TPBCheckTableCellModel *cellModel = (TPBCheckTableCellModel *)model; if (cellModel.checkEnabled && cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeJumpSelect: { if ([model isKindOfClass:[TPBJumpSelectTableCellModel class]]) { TPBJumpSelectTableCellModel *cellModel = (TPBJumpSelectTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } case TPBTableCellTypeTitleAction: { if ([model isKindOfClass:[TPBTitleActionTableCellModel class]]) { TPBTitleActionTableCellModel *cellModel = (TPBTitleActionTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeMenuSelect: break; case TPBTableCellTypeProgress: case TPBTableCellTypeCommonInput: case TPBTableCellTypeListInput: break; } } //- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{ // if (indexPath.row < 0 || indexPath.row >= self.cloudDeviceList.count) { // return NO; // } // TPBDMECDevice *ecDevice = self.cloudDeviceList[indexPath.row]; // return [self canForgetWithDevice:ecDevice]; //} //- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath { // if (indexPath.row < 0 || indexPath.row >= self.cloudDeviceList.count) { // return @[]; // } // TPBDMECDevice *ecDevice = self.cloudDeviceList[indexPath.row]; // TPBWeakSelf; // UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:gControllerCloudAccess.controllerCloudAccessForget handler:^(UITableViewRowAction *action, NSIndexPath *indexPath){ // TPBStrongSelf; // [_self popAlertControlForUnbindActionWithECDevice:ecDevice]; // }]; // deleteAction.backgroundColor = [UIColor tpbRed]; // return @[deleteAction]; //} #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateSearchBarPosition]; } #pragma mark - TPBTableSearchHeaderDelegate - (void)tpbTableSearchHeaderHideKeyboard { [self tpbHideKeyboard]; } #pragma mark - TPBTableCommonInputDelegate - (void)tableCommonInputFieldDidClickReturn:(TPBCommonInputView *)inputView textField:(UITextField *)textField { [self handleUserClickReturnForTextField:textField]; } #pragma mark - TPBTableListInputDelegate - (void)tableListInputFieldDidClickReturn:(TPBListInputTableCell *)inputCell textField:(UITextField *)textField { [self handleUserClickReturnForTextField:textField]; } #pragma mark - Private - (void)updateSearchBarPosition { CGFloat tableY = self.tableView.frame.origin.y; CGRect convertedRect = [self.tpbSearchBarPlaceholderView convertRect:self.tpbSearchBarPlaceholderView.bounds toView:self.view]; CGFloat diffY = convertedRect.origin.y - tableY; CGFloat targetConstant = MAX(0, diffY); if (self.searchBarTopConstraint.tpbConstant != targetConstant) { self.searchBarTopConstraint.tpbConstant = targetConstant; } } // 用户点击UITextField键盘Return - (void)handleUserClickReturnForTextField:(UITextField *)textField { if (textField.returnKeyType != UIReturnKeyNext) { [textField resignFirstResponder]; return; } if ([self.tableView focusNextTextField]) { } else { [textField resignFirstResponder]; } } - (void)updateTableHeader { if (self.searchBarShow) { CGFloat searchBarHeight = [self.tpbInnerSearchBar preferredHeight]; self.tpbSearchBarPlaceholderView.frame = CGRectMake(0, 0, self.view.bounds.size.width, searchBarHeight); self.tableView.tableHeaderView = self.tpbSearchBarPlaceholderView; self.searchBarHeightConstraint.tpbConstant = searchBarHeight; } else if (self.customTableHeaderView) { self.tableView.tableHeaderView = self.customTableHeaderView; } else { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, CGFLOAT_MIN)]; self.tableView.tableHeaderView = view; } } #pragma mark - Property - (TPBEmptyView *)emptyView { if (!_emptyView) { _emptyView = [TPBEmptyView new]; _emptyView.hidden = YES; } return _emptyView; } - (UIView *)tpbSearchBarPlaceholderView { if (!_tpbSearchBarPlaceholderView) { _tpbSearchBarPlaceholderView = [UIView new]; } return _tpbSearchBarPlaceholderView; } - (TPBTableSearchHeader *)tpbInnerSearchBar { if (!_tpbInnerSearchBar) { CGRect frame = CGRectMake(0, 0, self.view.bounds.size.width, 62); _tpbInnerSearchBar = [[TPBTableSearchHeader alloc] initWithFrame:frame]; _tpbInnerSearchBar.delegate = self; _tpbInnerSearchBar.hidden = YES; _tpbInnerSearchBar.backgroundColor = [UIColor tpbBackground]; } return _tpbInnerSearchBar; } - (UITableView *)tableView { if (!_tableView) { _tableView = [[TPKeyboardAvoidingTableView alloc] initWithFrame:CGRectZero style:self.tableViewStyle]; if (@available(iOS 13.0, *)) { _tableView.automaticallyAdjustsScrollIndicatorInsets = YES; } if (@available(iOS 15.0, *)) { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, CGFLOAT_MIN)]; _tableView.tableHeaderView = view; _tableView.sectionHeaderTopPadding = 0; } _tableView.backgroundColor = [UIColor clearColor]; _tableView.estimatedRowHeight = 72; _tableView.estimatedSectionHeaderHeight = 68; _tableView.showsHorizontalScrollIndicator = NO; _tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; _tableView.sectionIndexColor = [UIColor tpbTableSectionIndexColor]; _tableView.delegate = self; _tableView.dataSource = self; NSArray<Class> *cellClassArray = @[ [TPBCustomViewTableCell class], [TPBTitleSubtitleTableCell class], [TPBSwitchTableCell class], [TPBProgressTableCell class], [TPBCommonInputTableCell class], [TPBCheckTableCell class], [TPBJumpSelectTableCell class], [TPBMenuSelectTableCell class], [TPBListInputTableCell class], [TPBListButtonTableCell class], [TPBTitleActionTableCell class] ]; for (Class cls in cellClassArray) { if ([cls isSubclassOfClass:[TPBBaseTableViewCell class]]) { NSString *cellIdentifier = [cls cellIdentifier]; [_tableView registerClass:cls forCellReuseIdentifier:cellIdentifier]; NSString *rtlCellIdentifier = [cls cellIdentifier:YES]; [_tableView registerClass:cls forCellReuseIdentifier:rtlCellIdentifier]; NSString *ltrCellIdentifier = [cls cellIdentifier:NO]; [_tableView registerClass:cls forCellReuseIdentifier:ltrCellIdentifier]; } } } return _tableView; } @end
12-03
import Foundation import AVFoundation import Accelerate @objc protocol MicrophoneSpectrumDelegate: AnyObject { func microphone(_ microphone: MicrophoneSpectrum, didGenerateSpectrum spectra: [[Float]]) } struct FrequencyBand { let lower: Float let upper: Float } @objcMembers class MicrophoneSpectrum: NSObject { weak var delegate: MicrophoneSpectrumDelegate? let engine = AVAudioEngine() let inputNode: AVAudioInputNode let mixerNode = AVAudioMixerNode() let playerNode = AVAudioPlayerNode() // 中间桥梁节点,隔离格式 var bufferSize: Int = 1024 { didSet { bufferSize = nearestPowerOfTwo(bufferSize) setupAudioTap() analyzer = RealtimeAnalyzer(fftSize: bufferSize) } } var analyzer: RealtimeAnalyzer! private var hardwareFormat: AVAudioFormat! private var targetFormat: AVAudioFormat! override init() { inputNode = engine.inputNode bufferSize = 1024 analyzer = RealtimeAnalyzer(fftSize: bufferSize) super.init() setupAudioEngine() } func start() { AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in guard let self = self, granted else { return } DispatchQueue.global().async { do { try self.engine.start() self.playerNode.play() // 启动桥梁节点 print("引擎启动成功") } catch { print("引擎启动失败:\(error)") } } } } func stop() { engine.stop() playerNode.stop() mixerNode.removeTap(onBus: 0) } } extension MicrophoneSpectrum { private func setupAudioEngine() { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .measurement) try session.setActive(true) // 1. 获取硬件格式(不可修改) hardwareFormat = inputNode.outputFormat(forBus: 0) logFormat("硬件原始格式", format: hardwareFormat) // 2. 定义目标格式(与硬件采样率一致,仅修改其他参数) targetFormat = AVAudioFormat( commonFormat: .pcmFormatFloat32, sampleRate: hardwareFormat.sampleRate, // 严格匹配硬件采样率 channels: 1, interleaved: false )! logFormat("目标处理格式", format: targetFormat) // 3. 配置节点链:input → mixer(硬件格式)→ player(桥梁)→ output(目标格式) engine.attach(mixerNode) engine.attach(playerNode) // 输入链:用硬件格式连接 input → mixer engine.connect(inputNode, to: mixerNode, format: hardwareFormat) // 处理链:用目标格式连接 player → output let outputNode = engine.outputNode engine.connect(playerNode, to: outputNode, format: targetFormat) // 4. 从 mixer 采集硬件格式数据,通过 player 转换为目标格式播放 mixerNode.installTap( onBus: 0, bufferSize: AVAudioFrameCount(bufferSize), format: hardwareFormat ) { [weak self] buffer, _ in guard let self = self else { return } self.playerNode.scheduleBuffer(buffer) // 用player转换格式 } engine.prepare() setupAudioTap() // 在player输出端设置Tap(目标格式) } catch { print("引擎配置失败:\(error)") } } // 关键:在playerNode的输出端设置Tap(已转换为目标格式) private func setupAudioTap() { guard let format = targetFormat else { return } playerNode.removeTap(onBus: 0) playerNode.installTap( onBus: 0, bufferSize: AVAudioFrameCount(bufferSize), format: format ) { [weak self] (buffer: AVAudioPCMBuffer, _) in guard let self = self, self.engine.isRunning else { return } // 最终校验:确保Tap格式与目标格式完全一致 guard buffer.format.commonFormat == format.commonFormat, buffer.format.sampleRate == format.sampleRate, buffer.format.channelCount == format.channelCount else { print("格式不匹配!预期:\(format),实际:\(buffer.format)") return } let validFrameLength = min(buffer.frameLength, AVAudioFrameCount(self.bufferSize)) buffer.frameLength = validFrameLength DispatchQueue.global().async { let spectra = self.analyzer.analyse(with: buffer) DispatchQueue.main.async { self.delegate?.microphone(self, didGenerateSpectrum: spectra) } } } logFormat("Tap实际格式", format: format) } private func logFormat(_ label: String, format: AVAudioFormat) { print("\(label):") print(" 采样率:\(format.sampleRate)") print(" 通道数:\(format.channelCount)") print(" 数据格式:\(format.commonFormat)") print(" 交错模式:\(format.isInterleaved)") } private func nearestPowerOfTwo(_ value: Int) -> Int { guard value > 1 else { return 1 } return 1 << (Int(log2(Double(value - 1))) + 1) } } // 频谱分析器类(保持不变) @objcMembers class RealtimeAnalyzer: NSObject { var fftSize: Int lazy var fftSetup = vDSP_create_fftsetup(vDSP_Length(Int(round(log2(Double(fftSize))))), FFTRadix(kFFTRadix2)) public var frequencyBands: Int = 32 public var startFrequency: Float = 100 public var endFrequency: Float = 18000 lazy var bands: [FrequencyBand] = { var bands = [FrequencyBand]() guard endFrequency > startFrequency, frequencyBands > 0 else { return bands } let n = log2(endFrequency/startFrequency) / Float(frequencyBands) var currentLower = startFrequency for i in 1...frequencyBands { let currentUpper = currentLower * powf(2, n) let adjustedUpper = i == frequencyBands ? endFrequency : currentUpper bands.append(FrequencyBand(lower: currentLower, upper: adjustedUpper)) currentLower = currentUpper } return bands }() private var spectrumBuffer = [[Float]]() private let spectrumQueue = DispatchQueue(label: "com.analyzer.spectrumQueue") public var spectrumSmooth: Float = 0.5 { didSet { spectrumSmooth = max(0.0, min(1.0, spectrumSmooth)) } } init(fftSize: Int) { self.fftSize = fftSize.nextPowerOfTwo super.init() } deinit { vDSP_destroy_fftsetup(fftSetup) } func analyse(with buffer: AVAudioPCMBuffer) -> [[Float]] { guard buffer.format.sampleRate > 0, let _ = buffer.floatChannelData else { return [] } let channelsAmplitudes = fft(buffer) let aWeights = createFrequencyWeights(for: buffer) guard !channelsAmplitudes.isEmpty, aWeights.count == channelsAmplitudes[0].count else { return [] } let result = spectrumQueue.sync { // 修正:band → bands if spectrumBuffer.count != channelsAmplitudes.count { spectrumBuffer = channelsAmplitudes.map { _ in Array(repeating: 0, count: bands.count) } } for (index, amplitudes) in channelsAmplitudes.enumerated() { guard index < spectrumBuffer.count, amplitudes.count == aWeights.count else { continue } let weightedAmplitudes = amplitudes.enumerated().map { $0.1 * aWeights[$0.0] } let bandWidth = Float(buffer.format.sampleRate) / Float(fftSize) // 修正:band → bands var spectrum = bands.enumerated().map { (i, band) -> Float in guard i < weightedAmplitudes.count else { return 0.0 } return findMaxAmplitude(for: band, in: weightedAmplitudes, with: bandWidth) * 5 } spectrum = highlightWaveform(spectrum: spectrum) guard spectrum.count == spectrumBuffer[index].count else { continue } spectrumBuffer[index] = zip(spectrumBuffer[index], spectrum).map { $0.0 * spectrumSmooth + $0.1 * (1 - spectrumSmooth) } } return spectrumBuffer } return result } private func fft(_ buffer: AVAudioPCMBuffer) -> [[Float]] { var amplitudes = [[Float]]() guard let floatChannelData = buffer.floatChannelData else { return amplitudes } let channelCount = Int(buffer.format.channelCount) let frameCount = buffer.frameLength guard frameCount <= UInt32(fftSize) else { return amplitudes } var channels = [UnsafeMutablePointer<Float>]() let isInterleaved = buffer.format.isInterleaved if isInterleaved { let interleavedData = UnsafeBufferPointer(start: floatChannelData[0], count: Int(frameCount) * channelCount) for channel in 0..<channelCount { var data = [Float](repeating: 0, count: fftSize) for i in 0..<Int(frameCount) { data[i] = interleavedData[i * channelCount + channel] } channels.append(&data) } } else { for channel in 0..<channelCount { var data = [Float](repeating: 0, count: fftSize) memcpy(&data, floatChannelData[channel], Int(frameCount) * MemoryLayout<Float>.stride) channels.append(&data) } } for channel in channels { var window = [Float](repeating: 0, count: fftSize) vDSP_hann_window(&window, vDSP_Length(fftSize), Int32(vDSP_HANN_NORM)) vDSP_vmul(channel, 1, window, 1, channel, 1, vDSP_Length(fftSize)) var realp = [Float](repeating: 0, count: fftSize/2) var imagp = [Float](repeating: 0, count: fftSize/2) var fftInOut = DSPSplitComplex(realp: &realp, imagp: &imagp) channel.withMemoryRebound(to: DSPComplex.self, capacity: fftSize) { vDSP_ctoz($0, 2, &fftInOut, 1, vDSP_Length(fftSize/2)) } let log2n = vDSP_Length(round(log2(Double(fftSize)))) vDSP_fft_zrip(fftSetup!, &fftInOut, 1, log2n, FFTDirection(FFT_FORWARD)) fftInOut.imagp[0] = 0 var normFactor = Float(1.0 / Double(fftSize)) vDSP_vsmul(fftInOut.realp, 1, &normFactor, fftInOut.realp, 1, vDSP_Length(fftSize/2)) vDSP_vsmul(fftInOut.imagp, 1, &normFactor, fftInOut.imagp, 1, vDSP_Length(fftSize/2)) var amp = [Float](repeating: 0, count: fftSize/2) vDSP_zvabs(&fftInOut, 1, &amp, 1, vDSP_Length(fftSize/2)) amp[0] /= 2 amplitudes.append(amp) } return amplitudes } private func findMaxAmplitude(for band: FrequencyBand, in amplitudes: [Float], with bandWidth: Float) -> Float { guard band.lower <= band.upper, bandWidth > 0 else { return 0 } let start = max(0, min(Int(round(band.lower / bandWidth)), amplitudes.count - 1)) let end = max(0, min(Int(round(band.upper / bandWidth)), amplitudes.count - 1)) guard start <= end else { return 0 } return amplitudes[start...end].max() ?? 0 } private func createFrequencyWeights(for buffer: AVAudioPCMBuffer) -> [Float] { let bins = fftSize / 2 let sampleRate = buffer.format.sampleRate let deltaF = Float(sampleRate) / Float(fftSize) var weights = [Float](repeating: 0, count: bins) for i in 0..<bins { let f = Float(i) * deltaF guard f > 0 else { continue } let f2 = f * f let c1 = powf(12194.217, 2) let c2 = powf(20.598997, 2) let c3 = powf(107.65265, 2) let c4 = powf(737.86223, 2) let num = c1 * f2 * f2 let den = (f2 + c2) * sqrtf((f2 + c3) * (f2 + c4)) * (f2 + c1) weights[i] = den == 0 ? 0 : 1.2589 * num / den } return weights } private func highlightWaveform(spectrum: [Float]) -> [Float] { let weights: [Float] = [1, 2, 3, 5, 3, 2, 1] let total = Float(weights.reduce(0, +)) let offset = weights.count / 2 guard !spectrum.isEmpty else { return [] } var result = [Float]() let safeOffset = min(offset, spectrum.count) result.append(contentsOf: spectrum[0..<safeOffset]) let maxIndex = spectrum.count - offset guard maxIndex > offset else { return spectrum } for i in offset..<maxIndex { let window = (0..<weights.count).map { let idx = i - offset + $0 return idx < spectrum.count ? spectrum[idx] : 0 } result.append(zip(window, weights).map { $0 * $1 }.reduce(0, +) / total) } let remainingStart = max(spectrum.count - offset, offset) if remainingStart < spectrum.count { result.append(contentsOf: spectrum[remainingStart..<spectrum.count]) } return result } } extension Int { var nextPowerOfTwo: Int { guard self > 1 else { return 1 } return 1 << (Int(log2(Double(self - 1))) + 1) } } import UIKit @objcMembers class SpectrumView: UIView { // 柱子宽度(略微加宽,增强存在感) var barWidth: CGFloat = 5.0 { didSet { if isDefaultState { barHeight = barWidth } setNeedsLayout() } } // 柱子间距(进一步缩小,让排列更密集) var space: CGFloat = 0.3 { didSet { setNeedsLayout() } } // 基础高度(默认状态下的最小高度) var barHeight: CGFloat = 5.0 // 边框宽度(加粗边框,增强灰色边框的存在感) var borderWidth: CGFloat = 1.0 // 填充色(使用稍深的颜色,与浅灰背景形成对比) var fillColor: UIColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1.0) { didSet { barLayers.forEach { $0.fillColor = fillColor.cgColor } } } // 灰色边框(中灰色,清晰可见) var borderColor: UIColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) { didSet { barLayers.forEach { $0.strokeColor = borderColor.cgColor } } } // 浅灰色背景 override var backgroundColor: UIColor? { didSet { // 强制背景为浅灰色,避免外部修改 super.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) } } // 存储所有柱子图层 private var barLayers = [CAShapeLayer]() // 标记是否为默认状态 private var isDefaultState: Bool = true // 频谱数据 var spectra: [[Float]]? { didSet { updateBars() } } // 振幅放大倍数(进一步提高,让变化更剧烈) var amplitudeScale: CGFloat = 6.0 // 从1.5提高到6.0 // 最大高度限制(适当放宽,允许更高的柱形) private var maxBarHeight: CGFloat { return bounds.height * 0.95 // 占视图高度的95% } // 默认状态下的圆点数量(与数据状态保持一致) private let defaultDotCount = 24 override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupView() } private func setupView() { // 强制设置浅灰色背景 backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) layer.contentsScale = UIScreen.main.scale barHeight = barWidth } private func updateBars() { // 清除旧图层 barLayers.forEach { $0.removeFromSuperlayer() } barLayers.removeAll() // 验证数据 guard let spectra = spectra, !spectra.isEmpty, spectra.count >= 2 else { drawDefaultDots() return } guard bounds.height > 0, bounds.width > 0 else { return } let centerY = bounds.height / 2 isDefaultState = true // 右声道数据(取绝对,避免负振幅) let amplitudes = spectra[1].map { abs($0) } let barCount = amplitudes.count // 找到最大振幅(用于动态调整灵敏度) let maxAmplitude = amplitudes.max() ?? 0 // 计算总宽度(所有柱子+间距的总长度) let totalBarWidth = CGFloat(barCount) * barWidth let totalSpaceWidth = CGFloat(barCount - 1) * space // 柱子间的总间距 let totalContentWidth = totalBarWidth + totalSpaceWidth // 计算起始X偏移量(让整体居中) let startX = (bounds.width - totalContentWidth) / 2 // 确保不超出左边界 let safeStartX = max(0, startX) // 绘制柱子 for (i, amplitude) in amplitudes.enumerated() { // 降低阈,更早进入非默认状态 if amplitude > 0.01 { isDefaultState = false } // 计算当前柱子的X位置(从左到右排列,整体居中) let x = safeStartX + CGFloat(i) * (barWidth + space) // 超出右边界则停止绘制 guard x + barWidth <= bounds.width else { break } // 计算高度 let barHeight = calculateBarHeight( amplitude: amplitude, maxAmplitude: maxAmplitude ) let y = centerY - barHeight / 2 // 垂直居中 // 创建柱子 let barLayer = createBarLayer( x: x, y: y, width: barWidth, height: barHeight ) layer.addSublayer(barLayer) barLayers.append(barLayer) } } // 绘制默认圆点(静态状态)- 已修改为居中布局 private func drawDefaultDots() { guard bounds.height > 0, bounds.width > 0 else { return } isDefaultState = true let centerY = bounds.height / 2 barHeight = barWidth // 计算总宽度(所有圆点+间距的总长度) let totalBarWidth = CGFloat(defaultDotCount) * barWidth let totalSpaceWidth = CGFloat(defaultDotCount - 1) * space let totalContentWidth = totalBarWidth + totalSpaceWidth // 计算起始X偏移量(让整体居中) let startX = (bounds.width - totalContentWidth) / 2 let safeStartX = max(0, startX) // 绘制默认圆点(使用与数据状态相同的居中布局逻辑) for i in 0..<defaultDotCount { // 从左到右排列,整体居中 let x = safeStartX + CGFloat(i) * (barWidth + space) guard x + barWidth <= bounds.width else { break } let dotLayer = createBarLayer( x: x, y: centerY - barHeight / 2, width: barWidth, height: barHeight ) layer.addSublayer(dotLayer) barLayers.append(dotLayer) } } // 创建柱子图层(优化圆角和动画) private func createBarLayer(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> CAShapeLayer { let layer = CAShapeLayer() layer.contentsScale = UIScreen.main.scale // 圆角半径(保持圆润感) let cornerRadius = width * 0.5 let rect = CGRect(x: x, y: y, width: width, height: height) layer.path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath // 样式设置 layer.fillColor = fillColor.cgColor layer.strokeColor = borderColor.cgColor layer.lineWidth = borderWidth // 加快动画响应速度,让变化更及时 let animation = CABasicAnimation(keyPath: "path") animation.duration = 0.08 // 从0.1缩短到0.08 animation.timingFunction = CAMediaTimingFunction(name: .easeOut) layer.add(animation, forKey: "heightAnimation") return layer } // 核心:计算高度(进一步放大振幅影响) private func calculateBarHeight(amplitude: Float, maxAmplitude: Float) -> CGFloat { // 1. 提高基础高度,确保柱子更明显 let baseHeight: CGFloat = 8.0 // 从6.0提高到8.0 // 2. 动态高度(根据振幅计算) let dynamicHeight: CGFloat if maxAmplitude < 0.1 { // 微弱信号时放大倍数更高 dynamicHeight = CGFloat(amplitude) * maxBarHeight * amplitudeScale * 3 // 从2倍提高到3倍 } else { // 正常信号时保持高放大倍数 dynamicHeight = CGFloat(amplitude) * maxBarHeight * amplitudeScale } // 3. 总高度 = 基础高度 + 动态高度 let totalHeight = baseHeight + dynamicHeight // 4. 限制最大高度,避免超出视图 return min(totalHeight, maxBarHeight) } override func layoutSubviews() { super.layoutSubviews() updateBars() } } // // QDRealTimeRecognizeViewController.m // QCloudSDKDemo // // Created by Sword on 2019/4/12. // Copyright © 2019 Tencent. All rights reserved. // #import "QDRealTimeRecognizeVC.h" #import <AVFoundation/AVFoundation.h> #import "UIView+Toast.h" #import <QCloudRealTime/QCloudRealTimeRecognizer.h> #import <QCloudRealTime/QCloudConfig.h> #import <QCloudRealTime/QCloudRealTimeResult.h> #import "Channel-Swift.h" @interface QDRealTimeRecognizeVC ()<QCloudRealTimeRecognizerDelegate,MicrophoneSpectrumDelegate> #pragma mark - 核心属性 @property (weak, nonatomic) IBOutlet UIView *contentBgView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewHeight;//530-600 @property (weak, nonatomic) IBOutlet UILabel *aleartLabel; @property (weak, nonatomic) IBOutlet SpectrumView *spectrumView;// 波形动画视图 @property (nonatomic,strong) MicrophoneSpectrum *spectrumAnalyzer; // 声明频谱分析器 @property (nonatomic, strong) QCloudRealTimeRecognizer *realTimeRecognizer; // 实时识别管理器 @property (nonatomic, assign) BOOL isRecording; // 录音状态标记 @property (nonatomic, assign) float currentVolume; // 当前音量 #pragma mark - UI组件 @property (weak, nonatomic) IBOutlet UITextView *recognizedTextView; // 识别结果展示 @property (weak, nonatomic) IBOutlet UISwitch *volumeDetectSwitch; // 音量检测开关 @property (weak, nonatomic) IBOutlet UISwitch *silenceDetectEndSwitch;// 静音停止开关 @property (weak, nonatomic) IBOutlet UIButton *recognizeButton; // 开始/停止按钮 @property (weak, nonatomic) IBOutlet UILabel *volumeLabel; // 音量显示标签 @end @implementation QDRealTimeRecognizeVC #pragma mark - 生命周期 - (void)viewDidLoad { [super viewDidLoad]; [self initMicrophone];//初始化分析器 [self setupUI]; // 初始化UI } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self configureAudioSession]; // 配置音频会话 } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self stopRecognizeIfNeeded]; // 页面消失时停止识别 } #pragma mark - 初始化 /** 初始化UI组件 */ //- (void)setupUI { // // // 设置半透明背景 // self.view.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3]; // // // 设置圆角 // self.contentViewHeight.constant = 530; // self.contentBgView.frame = CGRectMake(0,kScreenHeight, kScreenWidth, self.contentViewHeight.constant); // self.contentBgView.layer.cornerRadius = 20; // self.contentBgView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner; // // [UIView animateWithDuration:0.3 animations:^{ // self.contentBgView.frame = CGRectMake(0,kScreenHeight-self.contentViewHeight.constant, kScreenWidth, self.contentViewHeight.constant); // } completion:^(BOOL finished) { // [self.view layoutIfNeeded]; // // }]; // // //} - (void)setupUI { // 设置半透明背景 self.view.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3]; // 1. 配置contentBgView的约束(推荐用Auto Layout) self.contentBgView.translatesAutoresizingMaskIntoConstraints = NO; // 约束:底部对齐父视图底部,左右充满,高度固定为530 [NSLayoutConstraint activateConstraints:@[ [self.contentBgView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], [self.contentBgView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor], [self.contentBgView.rightAnchor constraintEqualToAnchor:self.view.rightAnchor], [self.contentBgView.heightAnchor constraintEqualToConstant:530] ]]; // 2. 设置圆角(只顶部两角) self.contentBgView.layer.cornerRadius = 20; self.contentBgView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner; // 注意:顶部两角需要用 MinY self.contentBgView.clipsToBounds = YES; // 确保圆角生效 // 3. 初始位置:让视图位于屏幕底部外侧(动画起点) self.contentBgView.transform = CGAffineTransformMakeTranslation(0, 530); // 4. 执行滑入动画 // [UIView animateWithDuration:0.3 animations:^{ self.contentBgView.transform = CGAffineTransformIdentity; // 恢复到原位置 // [self.view layoutIfNeeded]; // 确保约束动画生效 // }]; [self onRecognizeButtonTouched]; } // 初始化分析器 -(void)initMicrophone{ // 初始化分析器 self.spectrumAnalyzer = [[MicrophoneSpectrum alloc] init]; // 设置代理(自己作为代理接收频谱数据) self.spectrumAnalyzer.delegate = self; // 可选:调整缓冲区大小(必须是2的幂,如512、1024、2048等) self.spectrumAnalyzer.bufferSize = 1024; // 初始化频谱视图的默认参数 self.spectrumView.space = 5.0; // 设置间距 } /** 配置音频会话(录音模式) */ - (void)configureAudioSession { NSError *error = nil; AVAudioSession *session = [AVAudioSession sharedInstance]; // 设置为录音模式(仅支持录音,不播放声音) [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; if (error) { NSLog(@"音频会话配置错误: %@", error.localizedDescription); [self.view makeToast:error.localizedDescription duration:2 position:CSToastPositionCenter]; return; } [session setActive:YES error:&error]; [self.spectrumAnalyzer start]; } #pragma mark - 布局调整 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // 确保频谱视图有默认高度 if (CGRectGetHeight(self.spectrumView.bounds) == 0) { CGRect frame = self.spectrumView.frame; frame.size.height = 60; // 视图总高度 self.spectrumView.frame = frame; } // 计算总柱子数量 CGFloat totalBarCount = self.spectrumAnalyzer.analyzer.frequencyBands; // 使用 spectrumView 的 space 属性计算总间距 CGFloat totalSpace = self.spectrumView.space * (totalBarCount + 1); CGFloat availableWidth = CGRectGetWidth(self.spectrumView.bounds) - totalSpace; // 确保柱子宽度不超过可用空间(可选优化) if (availableWidth > 0) { CGFloat maxPossibleBarWidth = availableWidth / self.spectrumAnalyzer.analyzer.frequencyBands; // 保证线宽不超过最大可能宽度(但不强制覆盖用户设置的默认) if (self.spectrumView.barWidth > maxPossibleBarWidth) { self.spectrumView.barWidth = maxPossibleBarWidth; } } } #pragma mark - 识别控制 /** 开始/停止识别(根据当前状态切换) */ - (void)toggleRecognize { if (self.isRecording) { [self stopRecognize]; } else { [self startRecognize]; } } /** 开始识别 */ - (void)startRecognize { // 初始化识别器(首次调用时) if (!self.realTimeRecognizer) { [self setupRealTimeRecognizer]; } // 启动识别 [self.realTimeRecognizer start]; self.recognizedTextView.text = @""; // 清空历史结果 self.spectrumView.hidden = NO; // [self.spectrumAnalyzer start];//开始采集 } /** 停止识别 */ - (void)stopRecognize { [self.realTimeRecognizer stop]; [self.spectrumAnalyzer stop]; // 停止监测 self.spectrumView.hidden = YES; // 释放音频会话资源 [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; } /** 初始化实时识别器配置 */ - (void)setupRealTimeRecognizer { // 1. 配置鉴权与基础参数 QCloudConfig *config = [self createQCloudConfig]; // 2. 初始化识别器(使用内置录音器) self.realTimeRecognizer = [[QCloudRealTimeRecognizer alloc] initWithConfig:config]; self.realTimeRecognizer.delegate = self; } /** 创建腾讯云配置(鉴权+参数) */ - (QCloudConfig *)createQCloudConfig { QCloudConfig *config = nil; // 永久密钥鉴权(无token) if ([kQDToken isEqualToString:@""]) { config = [[QCloudConfig alloc] initWithAppId:kQDAppId secretId:kQDSecretId secretKey:kQDSecretKey projectId:[kQDProjectId integerValue]]; } else { // 临时密钥鉴权(带token) config = [[QCloudConfig alloc] initWithAppId:kQDAppId secretId:kQDSecretId secretKey:kQDSecretKey token:kQDToken projectId:[kQDProjectId integerValue]]; } // 2. 识别参数配置 config.sliceTime = 40; // 语音分片时长(40ms) config.enableDetectVolume = self.volumeDetectSwitch.on; // 开启音量检测 config.endRecognizeWhenDetectSilence = self.silenceDetectEndSwitch.on; // 静音停止识别 config.endRecognizeWhenDetectSilenceAutoStop = YES; // 静音时自动停止 config.silenceDetectDuration = 3.0; // 静音超时时间(3秒) config.requestTimeout = 10; // 请求超时(10秒) config.engineType = @"16k_zh"; // 引擎模型(16k中文普通话,必填!) config.reinforceHotword = 1; // 增强热词 config.noiseThreshold = 0.5; // 噪音阈 config.compression = YES; // 音频压缩(弱网优化) [config setApiParam:@"hotword_list" value:@"腾讯云|10,语音识别|5,ASR|11"]; // 热词配置 return config; } /** 必要时停止识别(页面消失等场景) */ - (void)stopRecognizeIfNeeded { if (self.isRecording) { [self stopRecognize]; } [self.spectrumAnalyzer stop];//音频幅 } #pragma mark - UI更新 /** 更新按钮标题(根据录音状态) */ - (void)updateButtonTitle { NSString *title = self.isRecording ? @"停止" : @"开始"; [self.recognizeButton setTitle:title forState:UIControlStateNormal]; } /** 更新音量显示 */ - (void)updateVolumeLabelWithVolume:(float)volume min:(float)min max:(float)max { if (self.volumeDetectSwitch.on) { self.volumeLabel.text = [NSString stringWithFormat:@"音量: %.2f (%.2f-%.2f)", volume, min, max]; } else { self.volumeLabel.text = @"音量检测已关闭"; } } #pragma mark - QCloudRealTimeRecognizerDelegate /** 开始录音回调 */ - (void)realTimeRecognizerDidStartRecord:(QCloudRealTimeRecognizer *)recorder error:(NSError *)error { if (!error) { self.isRecording = YES; [self updateButtonTitle]; self.currentVolume = 0; } else { NSLog(@"录音启动失败: %@", error.localizedDescription); [self.view makeToast:error.localizedDescription duration:2 position:CSToastPositionCenter]; } } /** 停止录音回调 */ - (void)realTimeRecognizerDidStopRecord:(QCloudRealTimeRecognizer *)recorder { _isRecording = NO; [self.spectrumAnalyzer stop];//停止采集 self.spectrumView.hidden = YES; [self updateButtonTitle]; } /** 实时识别结果回调(中间结果) */ - (void)realTimeRecognizerOnSliceRecognize:(QCloudRealTimeRecognizer *)recognizer result:(QCloudRealTimeResult *)result { if (result.code == 0) { self.recognizedTextView.text = result.recognizedText; } } - (void)realTimeRecognizerDidFinish:(QCloudRealTimeRecognizer *)recorder result:(NSString *)result { NSLog(@"realTimeRecognizerDidFinish:%@", result); } /** 音量更新回调 */ - (void)realTimeRecognizerDidUpdateVolumeDB:(QCloudRealTimeRecognizer *)recognizer volume:(float)volume { static float minVolume = MAXFLOAT; static float maxVolume = 0; self.currentVolume = volume; minVolume = MIN(minVolume, volume); maxVolume = MAX(maxVolume, volume); [self updateVolumeLabelWithVolume:volume min:minVolume max:maxVolume]; } /** 识别错误回调 */ - (void)realTimeRecognizerDidError:(QCloudRealTimeRecognizer *)recognizer result:(QCloudRealTimeResult *)result { NSString *errorMsg = result.clientErrCode != QCloudRealTimeClientErrCode_Success ? result.clientErrMessage : result.jsonText; NSLog(@"识别错误: %@", errorMsg); self.recognizedTextView.text = [NSString stringWithFormat:@"错误: %@", errorMsg]; [self.view makeToast:errorMsg duration:2 position:CSToastPositionCenter]; } /** 识别流程开始回调 */ - (void)realTimeRecognizerOnFlowRecognizeStart:(QCloudRealTimeRecognizer *)recognizer voiceId:(NSString *)voiceId seq:(NSInteger)seq { NSLog(@"识别流程开始 - voiceId: %@, seq: %ld", voiceId, seq); } - (void)realTimeRecognizerOnSegmentSuccessRecognize:(nonnull QCloudRealTimeRecognizer *)recognizer result:(nonnull QCloudRealTimeResult *)result { QCloudRealTimeResultResponse *currentResult = [result.resultList firstObject]; NSLog(@"realTimeRecognizerOnSegmentSuccessRecognize:%@ index:%ld", currentResult.voiceTextStr, currentResult.index); } -(void)realTimeRecognizerOnSliceDetectTimeOut{ NSLog(@"realTimeRecognizeronSliceDetectTimeOut:触发了静音超时"); //当QCloudConfig.endRecognizeWhenDetectSilence 打开时,触发静音超时事件会回调此事件 //当QCloudConfig.endRecognizeWhenDetectSilenceAutoStop 打开时,回调此事件的同时会停止本次识别,此配置默认打开 } #pragma mark - 事件响应 /** 开始/停止按钮点击 */ - (void)onRecognizeButtonTouched { [self toggleRecognize]; } /** 取消按钮点击 */ - (IBAction)onCancelButtonTouched:(UIButton *)sender { if (self.realTimeRecognizer) { [self.realTimeRecognizer cancel]; } } /** 音量检测开关切换(仅停止状态可修改) */ - (IBAction)onVolumeDetectSwitchChanged:(UISwitch *)sender { if (self.isRecording) { sender.on = !sender.on; // 强制还原 [self.view makeToast:@"识别中无法修改" duration:1.5 position:CSToastPositionCenter]; } } /** 静音停止开关切换(仅停止状态可修改) */ - (IBAction)onSilenceDetectSwitchChanged:(UISwitch *)sender { if (self.isRecording) { sender.on = !sender.on; // 强制还原 [self.view makeToast:@"识别中无法修改" duration:1.5 position:CSToastPositionCenter]; } else { self.realTimeRecognizer = nil; // 重新初始化识别器以应用新配置 } } #pragma mark - MicrophoneSpectrumDelegate - (void)microphone:(MicrophoneSpectrum *)microphone didGenerateSpectrum:(NSArray<NSArray<NSNumber *> *> *)spectra{ // spectra 是频谱数据数组,每个元素是一个 Float 数组,表示不同频段的能量(0~1范围) dispatch_async(dispatch_get_main_queue(), ^{ self.spectrumView.spectra = spectra; }); } #pragma mark - 界面控件设置 //关闭按钮 - (IBAction)closeBtnClicked:(UIButton *)sender { // [UIView animateWithDuration:0.3 animations:^{ // self.contentBgView.frame = CGRectMake(0,-self.contentViewHeight.constant, kScreenWidth, self.contentViewHeight.constant); // } completion:^(BOOL finished) { // [self.view layoutIfNeeded]; // [self dismissViewControllerAnimated:NO completion:nil]; // }]; [self.spectrumAnalyzer start]; } @end engine.connect(inputNode, to: mixerNode, format: hardwareFormat) 崩溃Thread 1: "required condition is false: IsFormatSampleRateAndChannelCountValid(format)"
08-19
现在有一些报错,我把所有的文件发给你,你再看看,并且注意devicemodel说重复声明,我先换了个名字换成了newlistdevicemodel。 // // NewListDeviceModel.swift // SurveillanceHome // // Created by MaCong on 2025/12/5. // Copyright © 2025 tplink. All rights reserved. // import Foundation class NewListDeviceModel { let identifier: UInt64 let alias: String let deviceType: DeviceType let displayOnline: Bool let channelsInfo: [ChannelModel] let siteId: Int64 let isFirstDeviceInSite: Bool let isVMSFavorited: Bool init(device: TPSSDeviceForDeviceList) { self.identifier = device.identifier self.alias = device.alias ?? "" self.deviceType = DeviceType(rawValue: device.deviceType.rawValue) ?? .IPC self.displayOnline = device.displayOnline self.channelsInfo = device.channelsInfo.map { ChannelModel(channel: $0) } self.siteId = device.siteId self.isFirstDeviceInSite = device.isFirstDeviceInSite self.isVMSFavorited = device.isVMSFavorited } } class ChannelModel { let channelId: Int32 let online: Bool let isVMSFavorited: Bool init(channel: TPSSChannelInfo) { self.channelId = channel.channelId self.online = channel.online self.isVMSFavorited = channel.isVMSFavorited } } enum DeviceType: Int { case IPC, NVR, solar, unknown init(rawValue: Int) { switch rawValue { case 1: self = .IPC case 2: self = .NVR case 3: self = .solar default: self = .unknown } } } // // DeviceListView.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // // MARK: - 更新后的 DeviceListView 支持传入数据并渲染 class DeviceListView: UIView { private let collectionView = UICollectionView( frame: .zero, collectionViewLayout: createLayout() ) var devices: [NewListDeviceModel] = [] { didSet { collectionView.reloadData() } } var onMoreButtonTap: ((NewListDeviceModel) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } private static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) layout.minimumLineSpacing = 10 return layout } private func setup() { collectionView.backgroundColor = .clear collectionView.showsVerticalScrollIndicator = false collectionView.register(DeviceListCell.self, forCellWithReuseIdentifier: "DeviceListCell") collectionView.delegate = self collectionView.dataSource = self addSubview(collectionView) collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } extension DeviceListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return devices.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceListCell", for: indexPath) as! DeviceListCell let device = devices[indexPath.item] cell.configure(with: device) // 注入点击事件 cell.moreButton.removeTarget(nil, action: nil, for: .allEvents) cell.moreButton.addTarget(self, action: #selector(moreButtonTapped(_:)), for: .touchUpInside) return cell } @objc private func moreButtonTapped(_ sender: UIButton) { let point = sender.convert(CGPoint.zero, to: collectionView) guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.item < devices.count else { return } let device = devices[indexPath.item] onMoreButtonTap?(device) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = collectionView.bounds.width - 32 let height = width * 9 / 16 + 60 // 图片高度 + 下方文字 return CGSize(width: collectionView.bounds.width, height: height) } } // // DeviceListCell.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit class DeviceListCell: UICollectionViewCell { private let bgView = UIView() private let nameLabel = UILabel() private let statusLabel = UILabel() private let thumbnailImageView = UIImageView() private let moreButton = UIButton(type: .system) override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } private func setupUI() { backgroundColor = .tpbCard layer.cornerRadius = 12 clipsToBounds = true bgView.backgroundColor = .tpbBackground bgView.layer.cornerRadius = 8 bgView.clipsToBounds = true thumbnailImageView.contentMode = .scaleAspectFill thumbnailImageView.backgroundColor = .systemGray nameLabel.font = UIFont.boldSystemFont(ofSize: 16) nameLabel.textColor = .tpbTextPrimary statusLabel.font = UIFont.systemFont(ofSize: 13) statusLabel.textColor = .black moreButton.setTitle("⋯", for: .normal) moreButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) moreButton.setTitleColor(.tpbTextPrimary, for: .normal) addSubview(bgView) bgView.addSubview(thumbnailImageView) addSubview(nameLabel) addSubview(statusLabel) addSubview(moreButton) bgView.translatesAutoresizingMaskIntoConstraints = false thumbnailImageView.translatesAutoresizingMaskIntoConstraints = false nameLabel.translatesAutoresizingMaskIntoConstraints = false statusLabel.translatesAutoresizingMaskIntoConstraints = false moreButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ bgView.topAnchor.constraint(equalTo: topAnchor, constant: 8), bgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), bgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), bgView.heightAspectRatio(to: bgView.widthAnchor, ratio: 9.0/16.0), thumbnailImageView.topAnchor.constraint(equalTo: bgView.topAnchor), thumbnailImageView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor), thumbnailImageView.trailingAnchor.constraint(equalTo: bgView.trailingAnchor), thumbnailImageView.bottomAnchor.constraint(equalTo: bgView.bottomAnchor), nameLabel.topAnchor.constraint(equalTo: bgView.bottomAnchor, constant: 12), nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: moreButton.leadingAnchor, constant: -8), statusLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4), statusLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: moreButton.leadingAnchor, constant: -8), statusLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12), moreButton.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), moreButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), moreButton.widthAnchor.constraint(equalToConstant: 30), moreButton.heightAnchor.constraint(equalToConstant: 30) ]) } func configure(with device: NewListDeviceModel) { nameLabel.text = device.alias statusLabel.text = device.displayOnline ? "在线" : "离线" thumbnailImageView.image = device.displayOnline ? UIImage(named: "preview_placeholder") : UIImage(named: "offline_placeholder") moreButton.isHidden = !device.displayOnline } } // // DeviceListNewViewController.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit import SnapKit // MARK: - DeviceListNewViewController class DeviceListNewViewController: SurveillanceCommonTableController, SelectOrganizationViewDelegate { // MARK: - 属性 private lazy var titleView: NewTitleView = { let view = NewTitleView() view.titleText = "设备列表" view.didClickCallback = { [weak self] _ in guard let self = self else { return } let selectOrgView = self.selectOrganizationView let titleView = self.titleView if titleView.isAnimating { return } if !titleView.isInExpandStatus { titleView.isAnimating = true self.view.addSubview(selectOrgView) selectOrgView.show(animated: true) { titleView.isAnimating = false } } else { titleView.isAnimating = true self.hideLoadingView() selectOrgView.dismiss(animated: true) { titleView.isAnimating = false } } // 更新箭头状态 titleView.changeToStatus(expand: !titleView.isInExpandStatus) } return view }() private lazy var multiScreenButton: UIButton = { let btn = UIButton(type: .custom) btn.setImage(TPImageLiteral("media_player_switch_multi_live"), for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(clickMultileLive), for: .touchUpInside) return btn }() var selectedTabType: DeviceListMasterSelectedType = .all { didSet { UserDefaults.standard.deviceListSelectedType = selectedTabType forceRefreshDeviceList() } } var selectedSiteInfo: TPSSVMSSubsiteInfo? { didSet { saveSelectedSiteInfo(selectedSiteInfo) forceRefreshDeviceList() } } private func saveSelectedSiteInfo(_ info: TPSSVMSSubsiteInfo?) { guard let siteInfo = info else { try? FileManager.default.removeItem(at: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) return } do { let data = try NSKeyedArchiver.archivedData(withRootObject: siteInfo, requiringSecureCoding: true) try data.write(to: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) } catch { print(error) } } private lazy var selectOrganizationView: SelectOrganizationView = { let view = SelectOrganizationView() view.rootViewController = self view.delegate = self view.frame = CGRect( x: 0, y: SelectOrganizationView.statusBarHeight + SelectOrganizationView.navigationBarHeight, width: screenWidth, height: screenHeight - SelectOrganizationView.statusBarHeight - SelectOrganizationView.navigationBarHeight ) return view }() // MARK: - 设备数据与视图 private var deviceModels: [DeviceModel] = [] { didSet { deviceListView.devices = deviceModels } } private lazy var deviceListView: DeviceListView = { let view = DeviceListView() view.onMoreButtonTap = { [weak self] device in self?.showDeviceMenu(for: device) } return view }() // MARK: - 生命周期 override func viewDidLoad() { super.viewDidLoad() setNavigation() tableView.contentInsetAdjustmentBehavior = .automatic reloadData() // 初始加载设备 loadDevicesFromContext() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: false) } // MARK: - 子视图与约束 override func tpbSetupSubviews() { super.tpbSetupSubviews() } override func tpbMakeConstraint() { tableView.snp.remakeConstraints { make in make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) make.leading.trailing.equalToSuperview() make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) } } // MARK: - 创建 UI 组件 private func setNavigation(){ navigationItem.titleView = titleView titleView.snp.makeConstraints { make in make.width.lessThanOrEqualTo(200) make.height.equalTo(44) } let backButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("common_light_back_nor"), andTarget: self, andAction: #selector(jumpToXXX)) let centralButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("central_surveillance_button"), andTarget: self, andAction: #selector(centralButtonClicked)) let messageButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("tabbar_message_nor"), andTarget: self, andAction: #selector(onMessageButtonTapped)) navigationItem.leftBarButtonItems = [backButtonItem, centralButtonItem] navigationItem.rightBarButtonItem = messageButtonItem } // MARK: - 创建组件:设备数量视图 private func createDeviceCountView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 4 containerView.clipsToBounds = true let labels = ["NVR", "4K", "2K", "HD"] var categoryViews: [UIView] = [] for text in labels { let categoryView = UIView() categoryView.backgroundColor = UIColor(white: 0.93, alpha: 1.0) categoryView.layer.cornerRadius = 8 categoryView.clipsToBounds = true let label = UILabel() label.text = text label.textColor = UIColor.tpbTextPrimary label.font = UIFont.systemFont(ofSize: 15, weight: .semibold) label.textAlignment = .center categoryView.addSubview(label) label.snp.makeConstraints { make in make.edges.equalToSuperview().inset(10) } containerView.addSubview(categoryView) categoryViews.append(categoryView) } for (index, view) in categoryViews.enumerated() { view.snp.makeConstraints { make in make.height.equalTo(60) make.centerY.equalTo(containerView) if index == 0 { make.leading.equalTo(containerView).offset(20) } else { make.leading.equalTo(categoryViews[index - 1].snp.trailing).offset(16) make.width.equalTo(categoryViews[0]) } if index == labels.count - 1 { make.trailing.equalTo(containerView).offset(-20) } } } return containerView } // MARK: - 创建组件:存储空间视图 private func createStorageUsageView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 8 containerView.clipsToBounds = true let titleLabel = UILabel() titleLabel.text = "存储空间" titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium) titleLabel.textColor = .tpbTextPrimary containerView.addSubview(titleLabel) let progressView = UIProgressView(progressViewStyle: .default) progressView.progressTintColor = UIColor.systemBlue progressView.trackTintColor = UIColor(white: 0.9, alpha: 1.0) containerView.addSubview(progressView) let detailLabel = UILabel() detailLabel.font = UIFont.systemFont(ofSize: 13, weight: .regular) detailLabel.textColor = .tpbTextPrimary detailLabel.textAlignment = .center containerView.addSubview(detailLabel) let used = 1.2 let total = 5.0 let percent = Float(used / total) progressView.setProgress(percent, animated: false) let percentage = Int(percent * 100) detailLabel.text = String(format: "%.1f TB / %.1f TB (%d%%)", used, total, percentage) titleLabel.snp.makeConstraints { make in make.top.leading.equalTo(containerView).offset(16) } progressView.snp.makeConstraints { make in make.centerX.centerY.equalTo(containerView) make.width.equalTo(200) make.height.equalTo(6) } detailLabel.snp.makeConstraints { make in make.top.equalTo(progressView.snp.bottom).offset(8) make.centerX.equalTo(progressView) } return containerView } // MARK: - 创建组件:设备列表容器 Cell private func createDeviceListCell() -> TPBBaseTableCellModel { let containerView = UIView() containerView.backgroundColor = .tpbBackground let paddedView = UIView() paddedView.backgroundColor = .clear paddedView.layer.cornerRadius = 3 paddedView.clipsToBounds = true containerView.addSubview(paddedView) paddedView.addSubview(deviceListView) paddedView.snp.makeConstraints { make in make.edges.equalTo(containerView).inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) } deviceListView.snp.makeConstraints { make in make.edges.equalTo(paddedView) make.height.equalTo(1) // 动态高度由 content 决定 } // 使用 customBlock 设置自动高度 let cellModel = TPBBaseTableCellModel.customContent { [weak self] cell, _ in guard let self = self else { return } self.deviceListView.updateHeightConstraint(in: cell.contentView) } cellModel.height = TPBTableElementHeight.autoHeight return cellModel } // MARK: - 创建组件:多屏按钮作为 Header View private func createMultiScreenHeader() -> UIView { let headerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 44)) headerView.backgroundColor = .clear let container = TPBBaseView() container.backgroundColor = .clear container.layer.cornerRadius = 3 container.clipsToBounds = true headerView.addSubview(container) container.snp.makeConstraints { make in make.trailing.equalTo(headerView).offset(-16) make.centerY.equalTo(headerView) make.size.equalTo(CGSize(width: 44, height: 44)) } multiScreenButton.removeFromSuperview() container.addSubview(multiScreenButton) multiScreenButton.snp.makeConstraints { make in make.edges.equalToSuperview() } return headerView } // MARK: - 创建合并后的 Section(Header + Cell) private func createMergedDeviceSection() -> TPBTableSectionModel { let section = TPBTableSectionModel() section.customHeaderView = createMultiScreenHeader() section.sectionHeaderHeight = TPBTableElementHeight.customHeight(44) section.cellModelArray = [createDeviceListCell()] return section } // MARK: - 刷新数据 private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() // Section 0: 设备数量 let section0 = TPBTableSectionModel() let deviceCountView = createDeviceCountView() let deviceCountCellModel = TPBBaseTableCellModel.customContent(with: deviceCountView) deviceCountCellModel.height = TPBTableElementHeight.customHeight(100) section0.cellModelArray = [deviceCountCellModel] tempSectionArray.append(section0) // Section 1: 存储空间 let section1 = TPBTableSectionModel() let storageView = createStorageUsageView() let storageCellModel = TPBBaseTableCellModel.customContent(with: storageView) storageCellModel.height = TPBTableElementHeight.customHeight(100) section1.cellModelArray = [storageCellModel] tempSectionArray.append(section1) // Section 2: 合并后的设备列表 let section2 = createMergedDeviceSection() tempSectionArray.append(section2) sectionArray = tempSectionArray tableView.reloadData() } // MARK: - Actions @objc private func clickMultileLive() { print("Multi-Screen 按钮被点击") } @objc private func jumpToXXX() { print("跳转到【待定】页面") } @objc private func centralButtonClicked() { print("跳转到【中心监控】页面") let centralJumpVC = CentralJumpViewController() let centralJumpNaviVC = BaseNavigationController(rootViewController: centralJumpVC) centralJumpNaviVC.view.frame = self.view.frame centralJumpNaviVC.view.backgroundColor = .clear centralJumpVC.currentSiteId = selectedTabType == .all ? nil : selectedSiteInfo?.siteId centralJumpVC.maskDidClickBlock = { centralJumpNaviVC.view.removeFromSuperview() } UIApplication.shared.keyWindow?.addSubview(centralJumpNaviVC.view) } @objc private func onMessageButtonTapped() { print("消息按钮被点击") } // MARK: - 加载视图控制 func hideLoadingView() { titleView.hideLoadingAnimation() } // MARK: - SelectOrganizationViewDelegate func didSelectOrganization(_ organization: OrganizationModel) { titleView.titleText = organization.name hideLoadingView() reloadData() } func didCancelSelectOrganization() {} // MARK: - 设备数据管理 private func loadDevicesFromContext() { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let devices = TPAppContextFactory.shared().devices(inDeviceGroup: "default", includingHiddenChannels: false) let filteredDevices = devices.filter { $0.listType == .remote && $0.displayOnline } let models = filteredDevices.map { DeviceModel(device: $0) } DispatchQueue.main.async { self.deviceModels = models } } } private func forceRefreshDeviceList() { loadDevicesFromContext() } // MARK: - 弹出菜单 private func showDeviceMenu(for device: DeviceModel) { let menu = DeviceListMenuView(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 150))) menu.action = { [weak self] item in switch item { case .setting: self?.jumpToDeviceSetting(for: device) case .alarmMode: self?.toggleNotification(for: device) default: break } } presentGuideWith(viewToPresent: menu, size: menu.sizeThatFits(.zero), source: self.multiScreenButton) } private func jumpToDeviceSetting(for device: DeviceModel) { print("跳转到设备设置: \(device.alias)") // 实际跳转逻辑 } private func toggleNotification(for device: DeviceModel) { print("切换通知: \(device.alias)") // 调用 API } } 其中另外两个DeviceListOrganizationModel、DeviceListTitleView未做改动,所以没有放上去。请仔细检查漏没漏什么东西
最新发布
12-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值