添加语音websocket等,还没测试

This commit is contained in:
2026-01-16 13:38:03 +08:00
parent 169a1929d7
commit b021fd308f
33 changed files with 5098 additions and 8 deletions

View File

@@ -0,0 +1,16 @@
//
// KBAICommentView.h
// keyBoard
//
// Created by Mac on 2026/1/16.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBAICommentView : UIView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,20 @@
//
// KBAICommentView.m
// keyBoard
//
// Created by Mac on 2026/1/16.
//
#import "KBAICommentView.h"
@implementation KBAICommentView
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end

View File

@@ -0,0 +1,54 @@
//
// KBAiChatView.h
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 消息类型
typedef NS_ENUM(NSInteger, KBAiChatMessageType) {
KBAiChatMessageTypeUser, // 用户消息
KBAiChatMessageTypeAssistant // AI 回复
};
/// 聊天消息模型
@interface KBAiChatMessage : NSObject
@property(nonatomic, assign) KBAiChatMessageType type;
@property(nonatomic, copy) NSString *text;
@property(nonatomic, assign) BOOL isComplete; // 是否完成(用于打字机效果)
+ (instancetype)userMessageWithText:(NSString *)text;
+ (instancetype)assistantMessageWithText:(NSString *)text;
@end
/// 聊天视图
/// 显示用户消息和 AI 回复的气泡列表
@interface KBAiChatView : UIView
/// 添加用户消息
/// @param text 消息文本
- (void)addUserMessage:(NSString *)text;
/// 添加 AI 消息
/// @param text 消息文本
- (void)addAssistantMessage:(NSString *)text;
/// 更新最后一条 AI 消息(用于打字机效果)
/// @param text 当前可见文本
- (void)updateLastAssistantMessage:(NSString *)text;
/// 标记最后一条 AI 消息完成
- (void)markLastAssistantMessageComplete;
/// 清空所有消息
- (void)clearMessages;
/// 滚动到底部
- (void)scrollToBottom;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,296 @@
//
// KBAiChatView.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiChatView.h"
#pragma mark - KBAiChatMessage
@implementation KBAiChatMessage
+ (instancetype)userMessageWithText:(NSString *)text {
KBAiChatMessage *message = [[KBAiChatMessage alloc] init];
message.type = KBAiChatMessageTypeUser;
message.text = text;
message.isComplete = YES;
return message;
}
+ (instancetype)assistantMessageWithText:(NSString *)text {
KBAiChatMessage *message = [[KBAiChatMessage alloc] init];
message.type = KBAiChatMessageTypeAssistant;
message.text = text;
message.isComplete = NO;
return message;
}
@end
#pragma mark - KBAiChatBubbleCell
@interface KBAiChatBubbleCell : UITableViewCell
@property(nonatomic, strong) UIView *bubbleView;
@property(nonatomic, strong) UILabel *messageLabel;
@property(nonatomic, assign) KBAiChatMessageType messageType;
@end
@implementation KBAiChatBubbleCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.selectionStyle = UITableViewCellSelectionStyleNone;
//
self.bubbleView = [[UIView alloc] init];
self.bubbleView.layer.cornerRadius = 16;
self.bubbleView.layer.masksToBounds = YES;
self.bubbleView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.bubbleView];
//
self.messageLabel = [[UILabel alloc] init];
self.messageLabel.numberOfLines = 0;
self.messageLabel.font = [UIFont systemFontOfSize:16];
self.messageLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.bubbleView addSubview:self.messageLabel];
//
[NSLayoutConstraint activateConstraints:@[
[self.messageLabel.topAnchor
constraintEqualToAnchor:self.bubbleView.topAnchor
constant:10],
[self.messageLabel.bottomAnchor
constraintEqualToAnchor:self.bubbleView.bottomAnchor
constant:-10],
[self.messageLabel.leadingAnchor
constraintEqualToAnchor:self.bubbleView.leadingAnchor
constant:12],
[self.messageLabel.trailingAnchor
constraintEqualToAnchor:self.bubbleView.trailingAnchor
constant:-12],
]];
}
return self;
}
- (void)configureWithMessage:(KBAiChatMessage *)message {
self.messageLabel.text = message.text;
self.messageType = message.type;
//
for (NSLayoutConstraint *constraint in self.bubbleView.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeWidth) {
constraint.active = NO;
}
}
//
if (message.type == KBAiChatMessageTypeUser) {
//
self.bubbleView.backgroundColor = [UIColor systemBlueColor];
self.messageLabel.textColor = [UIColor whiteColor];
[NSLayoutConstraint deactivateConstraints:self.bubbleView.constraints];
[NSLayoutConstraint activateConstraints:@[
[self.bubbleView.topAnchor
constraintEqualToAnchor:self.contentView.topAnchor
constant:4],
[self.bubbleView.bottomAnchor
constraintEqualToAnchor:self.contentView.bottomAnchor
constant:-4],
[self.bubbleView.trailingAnchor
constraintEqualToAnchor:self.contentView.trailingAnchor
constant:-16],
[self.bubbleView.widthAnchor
constraintLessThanOrEqualToAnchor:self.contentView.widthAnchor
multiplier:0.75],
[self.messageLabel.topAnchor
constraintEqualToAnchor:self.bubbleView.topAnchor
constant:10],
[self.messageLabel.bottomAnchor
constraintEqualToAnchor:self.bubbleView.bottomAnchor
constant:-10],
[self.messageLabel.leadingAnchor
constraintEqualToAnchor:self.bubbleView.leadingAnchor
constant:12],
[self.messageLabel.trailingAnchor
constraintEqualToAnchor:self.bubbleView.trailingAnchor
constant:-12],
]];
} else {
// AI
self.bubbleView.backgroundColor = [UIColor systemGray5Color];
self.messageLabel.textColor = [UIColor labelColor];
[NSLayoutConstraint deactivateConstraints:self.bubbleView.constraints];
[NSLayoutConstraint activateConstraints:@[
[self.bubbleView.topAnchor
constraintEqualToAnchor:self.contentView.topAnchor
constant:4],
[self.bubbleView.bottomAnchor
constraintEqualToAnchor:self.contentView.bottomAnchor
constant:-4],
[self.bubbleView.leadingAnchor
constraintEqualToAnchor:self.contentView.leadingAnchor
constant:16],
[self.bubbleView.widthAnchor
constraintLessThanOrEqualToAnchor:self.contentView.widthAnchor
multiplier:0.75],
[self.messageLabel.topAnchor
constraintEqualToAnchor:self.bubbleView.topAnchor
constant:10],
[self.messageLabel.bottomAnchor
constraintEqualToAnchor:self.bubbleView.bottomAnchor
constant:-10],
[self.messageLabel.leadingAnchor
constraintEqualToAnchor:self.bubbleView.leadingAnchor
constant:12],
[self.messageLabel.trailingAnchor
constraintEqualToAnchor:self.bubbleView.trailingAnchor
constant:-12],
]];
}
}
@end
#pragma mark - KBAiChatView
@interface KBAiChatView () <UITableViewDataSource, UITableViewDelegate>
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages;
@end
@implementation KBAiChatView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
self.messages = [[NSMutableArray alloc] init];
self.tableView = [[UITableView alloc] initWithFrame:self.bounds
style:UITableViewStylePlain];
self.tableView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.backgroundColor = [UIColor clearColor];
self.tableView.estimatedRowHeight = 60;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[KBAiChatBubbleCell class]
forCellReuseIdentifier:@"ChatCell"];
[self addSubview:self.tableView];
}
#pragma mark - Public Methods
- (void)addUserMessage:(NSString *)text {
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
[self.messages addObject:message];
[self.tableView reloadData];
[self scrollToBottom];
}
- (void)addAssistantMessage:(NSString *)text {
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text];
[self.messages addObject:message];
[self.tableView reloadData];
[self scrollToBottom];
}
- (void)updateLastAssistantMessage:(NSString *)text {
// AI
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeAssistant && !message.isComplete) {
message.text = text;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
return;
}
}
//
[self addAssistantMessage:text];
}
- (void)markLastAssistantMessageComplete {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeAssistant) {
message.isComplete = YES;
return;
}
}
}
- (void)clearMessages {
[self.messages removeAllObjects];
[self.tableView reloadData];
}
- (void)scrollToBottom {
if (self.messages.count == 0)
return;
NSIndexPath *lastIndexPath =
[NSIndexPath indexPathForRow:self.messages.count - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return self.messages.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAiChatBubbleCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"ChatCell"
forIndexPath:indexPath];
KBAiChatMessage *message = self.messages[indexPath.row];
[cell configureWithMessage:message];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60;
}
@end

View File

@@ -0,0 +1,56 @@
//
// KBAiRecordButton.h
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 录音按钮状态
typedef NS_ENUM(NSInteger, KBAiRecordButtonState) {
KBAiRecordButtonStateNormal, // 正常状态
KBAiRecordButtonStateRecording, // 录音中
KBAiRecordButtonStateDisabled // 禁用
};
@class KBAiRecordButton;
/// 录音按钮代理
@protocol KBAiRecordButtonDelegate <NSObject>
@optional
/// 开始按下
- (void)recordButtonDidBeginPress:(KBAiRecordButton *)button;
/// 结束按下
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button;
/// 按下被取消(如手指滑出)
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button;
@end
/// 按住说话按钮
/// 支持长按手势和波形动画
@interface KBAiRecordButton : UIView
@property(nonatomic, weak) id<KBAiRecordButtonDelegate> delegate;
/// 当前状态
@property(nonatomic, assign) KBAiRecordButtonState state;
/// 按钮标题(正常状态)
@property(nonatomic, copy) NSString *normalTitle;
/// 按钮标题(录音状态)
@property(nonatomic, copy) NSString *recordingTitle;
/// 主色调
@property(nonatomic, strong) UIColor *tintColor;
/// 更新音量(用于波形动画)
/// @param rms 音量 RMS 值 (0.0 - 1.0)
- (void)updateVolumeRMS:(float)rms;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,248 @@
//
// KBAiRecordButton.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiRecordButton.h"
#import "KBAiWaveformView.h"
@interface KBAiRecordButton ()
@property(nonatomic, strong) UIView *backgroundView;
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) KBAiWaveformView *waveformView;
@property(nonatomic, strong) UIImageView *micIconView;
@property(nonatomic, assign) BOOL isPressing;
@end
@implementation KBAiRecordButton
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
_state = KBAiRecordButtonStateNormal;
_normalTitle = @"按住说话";
_recordingTitle = @"松开结束";
_tintColor = [UIColor systemBlueColor];
//
self.backgroundView = [[UIView alloc] init];
self.backgroundView.backgroundColor = [UIColor systemGray6Color];
self.backgroundView.layer.cornerRadius = 25;
self.backgroundView.layer.masksToBounds = YES;
self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.backgroundView];
//
self.micIconView = [[UIImageView alloc] init];
self.micIconView.image = [UIImage systemImageNamed:@"mic.fill"];
self.micIconView.tintColor = self.tintColor;
self.micIconView.contentMode = UIViewContentModeScaleAspectFit;
self.micIconView.translatesAutoresizingMaskIntoConstraints = NO;
[self.backgroundView addSubview:self.micIconView];
//
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = self.normalTitle;
self.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.titleLabel.textColor = [UIColor labelColor];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.backgroundView addSubview:self.titleLabel];
//
self.waveformView = [[KBAiWaveformView alloc] init];
self.waveformView.waveColor = self.tintColor;
self.waveformView.alpha = 0;
self.waveformView.translatesAutoresizingMaskIntoConstraints = NO;
[self.backgroundView addSubview:self.waveformView];
//
[NSLayoutConstraint activateConstraints:@[
[self.backgroundView.topAnchor constraintEqualToAnchor:self.topAnchor],
[self.backgroundView.bottomAnchor
constraintEqualToAnchor:self.bottomAnchor],
[self.backgroundView.leadingAnchor
constraintEqualToAnchor:self.leadingAnchor],
[self.backgroundView.trailingAnchor
constraintEqualToAnchor:self.trailingAnchor],
[self.micIconView.leadingAnchor
constraintEqualToAnchor:self.backgroundView.leadingAnchor
constant:20],
[self.micIconView.centerYAnchor
constraintEqualToAnchor:self.backgroundView.centerYAnchor],
[self.micIconView.widthAnchor constraintEqualToConstant:24],
[self.micIconView.heightAnchor constraintEqualToConstant:24],
[self.titleLabel.leadingAnchor
constraintEqualToAnchor:self.micIconView.trailingAnchor
constant:12],
[self.titleLabel.centerYAnchor
constraintEqualToAnchor:self.backgroundView.centerYAnchor],
[self.waveformView.trailingAnchor
constraintEqualToAnchor:self.backgroundView.trailingAnchor
constant:-20],
[self.waveformView.centerYAnchor
constraintEqualToAnchor:self.backgroundView.centerYAnchor],
[self.waveformView.widthAnchor constraintEqualToConstant:60],
[self.waveformView.heightAnchor constraintEqualToConstant:30],
]];
//
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)];
longPress.minimumPressDuration = 0.05;
[self addGestureRecognizer:longPress];
}
#pragma mark - Setters
- (void)setState:(KBAiRecordButtonState)state {
if (_state == state)
return;
_state = state;
[self updateAppearance];
}
- (void)setTintColor:(UIColor *)tintColor {
_tintColor = tintColor;
self.micIconView.tintColor = tintColor;
self.waveformView.waveColor = tintColor;
}
#pragma mark - Public Methods
- (void)updateVolumeRMS:(float)rms {
[self.waveformView updateWithRMS:rms];
}
#pragma mark - Private Methods
- (void)updateAppearance {
switch (self.state) {
case KBAiRecordButtonStateNormal:
self.titleLabel.text = self.normalTitle;
self.backgroundView.backgroundColor = [UIColor systemGray6Color];
self.micIconView.alpha = 1;
self.waveformView.alpha = 0;
[self.waveformView stopAnimation];
break;
case KBAiRecordButtonStateRecording:
self.titleLabel.text = self.recordingTitle;
self.backgroundView.backgroundColor =
[self.tintColor colorWithAlphaComponent:0.15];
self.micIconView.alpha = 1;
self.waveformView.alpha = 1;
[self.waveformView startIdleAnimation];
break;
case KBAiRecordButtonStateDisabled:
self.titleLabel.text = self.normalTitle;
self.backgroundView.backgroundColor = [UIColor systemGray5Color];
self.alpha = 0.5;
break;
}
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
if (self.state == KBAiRecordButtonStateDisabled) {
return;
}
CGPoint location = [gesture locationInView:self];
BOOL isInside = CGRectContainsPoint(self.bounds, location);
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
self.isPressing = YES;
[self animateScale:0.95];
self.state = KBAiRecordButtonStateRecording;
if ([self.delegate
respondsToSelector:@selector(recordButtonDidBeginPress:)]) {
[self.delegate recordButtonDidBeginPress:self];
}
break;
case UIGestureRecognizerStateChanged:
if (!isInside && self.isPressing) {
//
[self animateScale:1.0];
} else if (isInside && self.isPressing) {
//
[self animateScale:0.95];
}
break;
case UIGestureRecognizerStateEnded:
if (self.isPressing) {
self.isPressing = NO;
[self animateScale:1.0];
self.state = KBAiRecordButtonStateNormal;
[self.waveformView reset];
if (isInside) {
if ([self.delegate
respondsToSelector:@selector(recordButtonDidEndPress:)]) {
[self.delegate recordButtonDidEndPress:self];
}
} else {
if ([self.delegate
respondsToSelector:@selector(recordButtonDidCancelPress:)]) {
[self.delegate recordButtonDidCancelPress:self];
}
}
}
break;
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed:
if (self.isPressing) {
self.isPressing = NO;
[self animateScale:1.0];
self.state = KBAiRecordButtonStateNormal;
[self.waveformView reset];
if ([self.delegate
respondsToSelector:@selector(recordButtonDidCancelPress:)]) {
[self.delegate recordButtonDidCancelPress:self];
}
}
break;
default:
break;
}
}
- (void)animateScale:(CGFloat)scale {
[UIView animateWithDuration:0.15
animations:^{
self.backgroundView.transform =
CGAffineTransformMakeScale(scale, scale);
}];
}
@end

View File

@@ -0,0 +1,43 @@
//
// KBAiWaveformView.h
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 波形动画视图
/// 根据音量 RMS 值实时显示波形动画
@interface KBAiWaveformView : UIView
/// 波形颜色
@property(nonatomic, strong) UIColor *waveColor;
/// 波形条数量
@property(nonatomic, assign) NSInteger barCount;
/// 波形条宽度
@property(nonatomic, assign) CGFloat barWidth;
/// 波形条间距
@property(nonatomic, assign) CGFloat barSpacing;
/// 更新音量值
/// @param rms 音量 RMS 值 (0.0 - 1.0)
- (void)updateWithRMS:(float)rms;
/// 开始动画(空闲波动)
- (void)startIdleAnimation;
/// 停止动画
- (void)stopAnimation;
/// 重置波形
- (void)reset;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,163 @@
//
// KBAiWaveformView.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "KBAiWaveformView.h"
@interface KBAiWaveformView ()
@property(nonatomic, strong) NSMutableArray<CAShapeLayer *> *barLayers;
@property(nonatomic, strong) NSMutableArray<NSNumber *> *barHeights;
@property(nonatomic, strong) CADisplayLink *displayLink;
@property(nonatomic, assign) float currentRMS;
@property(nonatomic, assign) float targetRMS;
@property(nonatomic, assign) BOOL isAnimating;
@end
@implementation KBAiWaveformView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
_waveColor = [UIColor systemBlueColor];
_barCount = 5;
_barWidth = 4;
_barSpacing = 3;
_barLayers = [[NSMutableArray alloc] init];
_barHeights = [[NSMutableArray alloc] init];
_currentRMS = 0;
_targetRMS = 0;
self.backgroundColor = [UIColor clearColor];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self setupBars];
}
- (void)setupBars {
//
for (CAShapeLayer *layer in self.barLayers) {
[layer removeFromSuperlayer];
}
[self.barLayers removeAllObjects];
[self.barHeights removeAllObjects];
//
CGFloat totalWidth =
self.barCount * self.barWidth + (self.barCount - 1) * self.barSpacing;
CGFloat startX = (self.bounds.size.width - totalWidth) / 2;
CGFloat maxHeight = self.bounds.size.height;
CGFloat minHeight = maxHeight * 0.2;
for (NSInteger i = 0; i < self.barCount; i++) {
CAShapeLayer *barLayer = [CAShapeLayer layer];
barLayer.fillColor = self.waveColor.CGColor;
barLayer.cornerRadius = self.barWidth / 2;
CGFloat x = startX + i * (self.barWidth + self.barSpacing);
CGFloat height = minHeight;
CGFloat y = (maxHeight - height) / 2;
barLayer.frame = CGRectMake(x, y, self.barWidth, height);
barLayer.backgroundColor = self.waveColor.CGColor;
[self.layer addSublayer:barLayer];
[self.barLayers addObject:barLayer];
[self.barHeights addObject:@(height)];
}
}
#pragma mark - Public Methods
- (void)updateWithRMS:(float)rms {
self.targetRMS = MIN(MAX(rms, 0), 1);
}
- (void)startIdleAnimation {
if (self.isAnimating)
return;
self.isAnimating = YES;
self.displayLink =
[CADisplayLink displayLinkWithTarget:self
selector:@selector(updateAnimation)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
}
- (void)stopAnimation {
self.isAnimating = NO;
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)reset {
self.currentRMS = 0;
self.targetRMS = 0;
[self updateBarsWithRMS:0];
}
#pragma mark - Animation
- (void)updateAnimation {
// RMS
CGFloat smoothing = 0.3;
self.currentRMS =
self.currentRMS + (self.targetRMS - self.currentRMS) * smoothing;
[self updateBarsWithRMS:self.currentRMS];
}
- (void)updateBarsWithRMS:(float)rms {
CGFloat maxHeight = self.bounds.size.height;
CGFloat minHeight = maxHeight * 0.2;
CGFloat range = maxHeight - minHeight;
//
NSTimeInterval time = CACurrentMediaTime();
for (NSInteger i = 0; i < self.barLayers.count; i++) {
CAShapeLayer *layer = self.barLayers[i];
//
CGFloat phase = (CGFloat)i / self.barLayers.count * M_PI * 2;
CGFloat wave = sin(time * 3 + phase) * 0.3 + 0.7; // 0.4 - 1.0
//
CGFloat heightFactor = rms * wave;
CGFloat height = minHeight + range * heightFactor;
height = MAX(minHeight, MIN(maxHeight, height));
//
CGFloat y = (maxHeight - height) / 2;
[CATransaction begin];
[CATransaction setDisableActions:YES];
layer.frame = CGRectMake(layer.frame.origin.x, y, self.barWidth, height);
[CATransaction commit];
}
}
- (void)dealloc {
[self stopAnimation];
}
@end