玩转UITableView
阅读原文时间:2023年07月11日阅读:4

UITableView这个iOS开发中永远绕不开的UIView,那么就不可避免的要在多个页面多种场景下反复摩擦UITableView,就算是刚跳进火坑不久的iOS Developer也知道实现UITableView的数据源dataSource和代理delegate,写出一个UITableView也就基本OK了,但是这仅仅是写出一个UITableView而已,作为一个有想法的程序猿,要做的还有很多,如何利用UITableViewCell的重用机制,如何提高性能等,这些留在后面的系列中一一讲述,那么本文要解决的痛点又是什么呢?回答这个问题之前,我们先来看看上面提到的UITableView的两大核心:UITableViewDataSource、UITableViewDelegate!

一、UITableViewDataSource

UITableView需要一个数据源(dataSource)来显示数据,UITableView会向数据源查询一共有多少行数据以及每一行显示什么数据等。没有设置数据源的UITableView只是个空壳。凡是遵守UITableViewDataSource协议的OC对象,都可以是UITableView的数据源。查看源码:

@required // 必须实现

// 每个section的行数

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;

// 第section分区第row行的UITableViewCell对象

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@optional // 可选实现

// section个数,默认是1

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

// 第section分区的头部标题

  • (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;

// 第section分区的底部标题

  • (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section;

// 某一行是否可以编辑(删除)

  • (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;

// 某一行是否可以移动来进行重新排序

  • (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;

// UITableView右边的索引栏的内容
// return list of section titles to display in section index view (e.g. "ABCD…Z#")

  • (nullable NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView;

UITableViewDataSourc

二、UITableViewDelegate

通常都要为UITableView设置代理对象(delegate),以便在UITableView触发一下事件时做出相应的处理,比如选中了某一行。凡是遵守了UITableViewDelegate协议的OC对象,都可以是UITableView的代理对象。一般会让控制器充当UITableView的dataSource和delegate。查看源码:

@protocol UITableViewDelegate

@optional

// 每行高度

  • (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

// 每个section头部高度

  • (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;

// 每个section底部高度

  • (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

// 每个section头部自定义UIView

  • (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;

// 每个section底部自定义UIView

  • (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;

// 是否允许高亮

  • (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);

// 选中某行

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

UITableViewDelegate

到这里已经很明确了,在需要实现UITableView的控制器对象里,就不可避免的要设置数据源和设置代理,那么就不可避免的需要实现以上提到的那些代理方法,试想一下,如果不进行有效的封装,那极有可能每个需要UITableView的Controller里都有如下重复的代码行:

#pragma mark - UITableViewDelegate

  • (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0.000001;
    }

  • (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    return 0.000001;
    }

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return ;
    }

  • (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 0.000001;
    }

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return ;
    }

  • (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    return nil;
    }

  • (UIView*)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
    return nil;
    }

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"defaultType"];
    return cell;
    }

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    return;
    }

// lazy load

  • (UITableView*)tableView{
    if (!_tableView) {
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(, -, KS_Width, KS_Heigth+) style:UITableViewStyleGrouped];
    _tableView.delegate = (id)self;
    _tableView.dataSource = (id)self;
    [_tableView setSectionHeaderHeight:];
    _tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    _tableView.showsVerticalScrollIndicator = NO;
    _tableView.showsHorizontalScrollIndicator = NO;
    }
    return _tableView;
    }

重复代码块

这已经是够灾难的了,如果在项目周期中再遇到某个或者多个页面设计UI设计频繁的变动,那简直不敢想象,哪怕每次只是一点小小的改动,也可能需要修改上面重复代码块中UITableViewDelegate的多个地方,如新插入一行row或者一个section,所有涉及到section或者row的地方或许都需要更改!!!

OK,我现在可以回答上面的问题了,这边文章到底是做什么的?解决的痛点在那里?--- 解耦封装、简化代码、适者生存!

从重复代码块我们可以看出,一般会让控制器充当UITableView的dataSource和delegate,那么既然要解耦,那么就要打破思维定式,让UITableView自己做自己的dataSource和delegate!毕竟我的地盘我做主嘛!其次将UITableViewCell进行block封装对象化,让其所有的属性都自我集成。

一、首先来看UITableViewCell的封装 -- ZTCoolTableViewCell

@class UIView;
@class UITableViewCell;
@class UITableView;
@class NSIndexPath;

// 创建section头部 Or section底部的block
typedef UIView *(^buildCell)(UITableView *tableView, NSInteger section);
// 创建section对应的row数据源的block
typedef UITableViewCell *(^buildCellInfo)(UITableView *tableView, NSIndexPath *indexPath);
// 点击section对应row的事件block
typedef void (^clickBlock)(UITableView *tableView, NSIndexPath *indexPath);
// ZTCoolTableCellList刷新block
typedef void (^refreshBlock)();

@interface ZTCoolTableViewCell : NSObject

// 行高度
@property (nonatomic,assign) CGFloat height;
// 构造行
@property (nonatomic, copy) buildCell buildCell;

@end

@interface ZTCoolTableCellList : NSObject

// 头部
@property (nonatomic,strong) ZTCoolTableViewCell * headCell;
// 底部
@property (nonatomic,strong) ZTCoolTableViewCell * footCell;
// 构造行
@property (nonatomic,copy) buildCellInfo buildCellInfo;
// 列高(等于0表示自适应)
@property (nonatomic,assign) CGFloat cellHeigth;
// 行数量
@property (nonatomic,assign) NSInteger cellCount;
// 行点击事件
@property(nonatomic,copy) clickBlock clickBlock;
// 刷新事件(适用于需要动态更新tableview布局:新增或者删减section/row)
@property(nonatomic,copy) refreshBlock refreshBlock;
// 行标识
@property (nonatomic,copy) NSString *identifier;
@property (nonatomic,copy) NSString *xibName;

// 简单初始化 (单行cell)

  • (ZTCoolTableCellList *)initSimpleCell:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell;

// 复杂初始化 - 不可刷新

  • (ZTCoolTableCellList *)initComplexCellNoRefresh:(CGFloat)headHeigth
    buildHead:(buildCell)buildHead
    footHeight:(CGFloat)footHeight
    buildFoot:(buildCell)buildFoot
    cellHeight:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell
    cellCount:(NSInteger)cellCount
    identifier:(NSString *)identifier
    xibName:(NSString *)xibName;

// 复杂初始化 - 可刷新

  • (ZTCoolTableCellList *)initComplexCellHasRefresh:(CGFloat)headHeigth
    buildHead:(buildCell)buildHead
    footHeight:(CGFloat)footHeight
    buildFoot:(buildCell)buildFoot
    cellHeight:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell
    refreshCell:(refreshBlock)refreshCell
    cellCount:(NSInteger)cellCount
    identifier:(NSString *)identifier
    xibName:(NSString *)xibName;
    @end

.h文件

@implementation ZTCoolTableViewCell

@end

@implementation ZTCoolTableCellList

// 简单初始化

  • (ZTCoolTableCellList *)initSimpleCell:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell{

    return [self initComplexCellNoRefresh: buildHead:nil footHeight: buildFoot:nil cellHeight:cellHeight buildCell:buildCell clickCell:clickCell cellCount: identifier:nil xibName:nil];
    }

// 复杂初始化 - 不可刷新

  • (ZTCoolTableCellList *)initComplexCellNoRefresh:(CGFloat)headHeigth
    buildHead:(buildCell)buildHead
    footHeight:(CGFloat)footHeight
    buildFoot:(buildCell)buildFoot
    cellHeight:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell
    cellCount:(NSInteger)cellCount
    identifier:(NSString *)identifier
    xibName:(NSString *)xibName{

    if(headHeigth >){
    self.headCell = [[ZTCoolTableViewCell alloc] init];
    self.headCell.height = headHeigth;
    self.headCell.buildCell = buildHead;
    }

    if(footHeight >){
    self.footCell = [[ZTCoolTableViewCell alloc] init];
    self.footCell.height = footHeight;
    self.footCell.buildCell = buildFoot;
    }

    self.cellHeigth = cellHeight;
    self.buildCellInfo = buildCell;
    self.clickBlock = clickCell;
    self.cellCount = cellCount;
    self.identifier = identifier;
    self.xibName = xibName;

    return self;
    }

// 复杂初始化 - 可刷新

  • (ZTCoolTableCellList *)initComplexCellHasRefresh:(CGFloat)headHeigth
    buildHead:(buildCell)buildHead
    footHeight:(CGFloat)footHeight
    buildFoot:(buildCell)buildFoot
    cellHeight:(CGFloat)cellHeight
    buildCell:(buildCellInfo)buildCell
    clickCell:(clickBlock)clickCell
    refreshCell:(refreshBlock)refreshCell
    cellCount:(NSInteger)cellCount
    identifier:(NSString *)identifier
    xibName:(NSString *)xibName{

    if(headHeigth >){
    self.headCell = [[ZTCoolTableViewCell alloc] init];
    self.headCell.height = headHeigth;
    self.headCell.buildCell = buildHead;
    }

    if(footHeight >){
    self.footCell = [[ZTCoolTableViewCell alloc] init];
    self.footCell.height = footHeight;
    self.footCell.buildCell = buildFoot;
    }

    self.cellHeigth = cellHeight;
    self.buildCellInfo = buildCell;
    self.clickBlock = clickCell;

    if(refreshCell){
    self.refreshBlock = refreshCell;
    }

    self.cellCount = cellCount;
    self.identifier = identifier;
    self.xibName = xibName;

    return self;
    }

.m文件

二、让UITableView自己做自己的dataSource和delegate -- ZTCoolTableViewBase

@class ZTCoolTableCellList;

@interface ZTCoolTableViewBase : UITableView

// UITableView的数据集合
@property (nonatomic,strong) NSMutableArray *arrayTableViewCellList;

@end

.h文件

@implementation ZTCoolTableViewBase

#pragma mark-hitTest

  • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    id view = [super hitTest:point withEvent:event];
    if(![view isKindOfClass:[UITextField class]]){
    [self endEditing:YES];
    }
    return view;
    }

#pragma mark - TableViewDelegate
// section头部高度

  • (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section];
    if(cellList.headCell){
    return cellList.headCell.height;
    }else{
    return 0.00001;
    }
    }

// section底部高度

  • (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section];
    if(cellList.footCell){
    return cellList.footCell.height;
    }else{
    return 0.00001;
    }
    }

// 有多少section

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return [self.arrayTableViewCellList count];
    }

// 改变行的高度

  • (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]];
    if(cellList.cellHeigth == ){
    UITableViewCell *cell = [self tableView:self cellForRowAtIndexPath:indexPath];
    return cell.frame.size.height;
    }else{
    return cellList.cellHeigth;
    }
    }

// 每个section有多少行

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section];
    return cellList.cellCount;
    }

// 头部

  • (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section];
    if(cellList.headCell.buildCell){
    return cellList.headCell.buildCell(tableView,section);
    }else{
    return nil;
    }
    }

// cell数据构造

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]];
    return cellList.buildCellInfo(tableView,indexPath);
    }

// 底部

  • (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section];
    if(cellList.footCell.buildCell){
    return cellList.footCell.buildCell(tableView,section);
    }else{
    return nil;
    }
    }

// 选中某个项

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]];
    if(cellList.clickBlock){
    return cellList.clickBlock(tableView,indexPath);
    }
    }

  • (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0){
    return YES;
    }

.m文件

如此,我们便实现了UITableViewCell的对象化封装和Controller于UITableView数据源及代理的耦合。

那么如何实际运用呢?我们来举个例子,如下图,实现这样一个页面:

按照以前的思维,将控制器充当UITableView的dataSource和delegate,那么就会出现

_tableView.delegate = (id)self;

_tableView.dataSource = (id)self;

而且每个Controller页面都是实现的协议代理方法,一长串的重复代码!!!

那么现在有了新需求,需要动态的再第一个section和第二个section之间新增一个section,包括两行row,这就需要重新代码布局,涉及到了所有     row点击事件极有可能需要重新绑定section与row值,对于能躺着绝对不站着的懒程序猿来说,这简直不要太扎心!如果使用上面封装的设计去实现,简直不要太舒服!

一、声明对象

// 主界面容器UITableView
@property (nonatomic,strong) ZTCoolTableViewBase *tableView;
// 第一个section(个人资料、我的钱包)
@property (nonatomic,strong) ZTCoolTableCellList *firstCell;
// 第二个section(交易记录、联系客服、设置)
@property (nonatomic,strong) ZTCoolTableCellList *secondCell;
// 第三个section(私人日记、统计面板)
@property (nonatomic,strong) ZTCoolTableCellList *thirdCell;

二、设置UITableView数据源和代理

- (ZTCoolTableViewBase *)tableView{
if (!_tableView) {
CGRect rect = [UIScreen mainScreen].bounds;
_tableView = [[ZTCoolTableViewBase alloc] initWithFrame:rect style:UITableViewStyleGrouped];
_tableView.arrayTableViewCellList = [[NSMutableArray alloc] initWithObjects:
self.firstCell,
self.thirdCell,
nil];
_tableView.delegate = _tableView;
_tableView.dataSource = _tableView;
_tableView.sectionHeaderHeight = ;
_tableView.separatorColor = [UIColor groupTableViewBackgroundColor];
}
return _tableView;
}

其中:

// 设置UITableView的代理为自己
_tableView.delegate = _tableView;
// 设置UITableView的数据源为自己
_tableView.dataSource = _tableView;

// 初始化UITableView的数据对象集合
_tableView.arrayTableViewCellList = [[NSMutableArray alloc] initWithObjects:
self.firstCell,
self.thirdCell,
nil];

三、懒加载数据集合

#pragma mark - firstCell

  • (ZTCoolTableCellList *)firstCell{
    if (!_firstCell) {
    BIWeakObj(self)
    static NSString *identifier = @"firstCell";
    _firstCell = [[ZTCoolTableCellList alloc] init];
    _firstCell = [_firstCell initComplexCellNoRefresh: buildHead:nil footHeight: buildFoot:nil cellHeight: buildCell:^UITableViewCell *(UITableView *tableView, NSIndexPath *indexPath) {

        UITableViewCell \*cell = \[selfWeak.tableView dequeueReusableCellWithIdentifier:identifier\];
    if(cell == nil){  
        cell = \[\[UITableViewCell alloc\] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier\];  
        cell.selectionStyle = UITableViewCellSelectionStyleNone;  
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;  
        cell.textLabel.font = \[UIFont systemFontOfSize:14.0f\];
    
        if(indexPath.row == ){  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_info"\];  
            cell.textLabel.text = @"个人资料";  
        }  
        else{  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_money"\];  
            cell.textLabel.text = @"我的钱包";  
        }  
    }
    
    return cell;
    } clickCell:^(UITableView \*tableView, NSIndexPath \*indexPath) {
    \[selfWeak clickCell:indexPath\];
    } cellCount: identifier:identifier xibName:nil\];

    }
    return _firstCell;
    }

firstCell

#pragma mark - secondCell

  • (ZTCoolTableCellList *)secondCell{
    if (!_secondCell) {
    BIWeakObj(self)
    static NSString *identifier = @"secondCell";
    _secondCell = [[ZTCoolTableCellList alloc] init];
    _secondCell = [_secondCell initComplexCellNoRefresh: buildHead:nil footHeight: buildFoot:nil cellHeight: buildCell:^UITableViewCell *(UITableView *tableView, NSIndexPath *indexPath) {

        UITableViewCell \*cell = \[selfWeak.tableView dequeueReusableCellWithIdentifier:identifier\];
    if(cell == nil){  
        cell = \[\[UITableViewCell alloc\] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier\];  
        cell.selectionStyle = UITableViewCellSelectionStyleNone;  
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;  
        cell.textLabel.font = \[UIFont systemFontOfSize:14.0f\];
    
        if(indexPath.row == ){  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_log"\];  
            cell.textLabel.text = @"私人日记";  
        }  
        else{  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_statistic"\];  
            cell.textLabel.text = @"统计面板";  
        }  
    }
    
    return cell;
    } clickCell:^(UITableView \*tableView, NSIndexPath \*indexPath) {
    \[selfWeak clickCell:indexPath\];
    } cellCount: identifier:identifier xibName:nil\];

    }
    return _secondCell;
    }

secondCell

#pragma mark - thirdCell

  • (ZTCoolTableCellList *)thirdCell{
    if (!_thirdCell) {
    BIWeakObj(self)
    static NSString *identifier = @"thirdCell";
    _thirdCell = [[ZTCoolTableCellList alloc] init];

    \_thirdCell = \[\_thirdCell initComplexCellHasRefresh: buildHead:nil footHeight: buildFoot:nil cellHeight: buildCell:^UITableViewCell \*(UITableView \*tableView, NSIndexPath \*indexPath) {
    UITableViewCell \*cell = \[selfWeak.tableView dequeueReusableCellWithIdentifier:identifier\];
    
    if(cell == nil){  
        cell = \[\[UITableViewCell alloc\] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier\];  
        cell.selectionStyle = UITableViewCellSelectionStyleNone;  
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;  
        cell.textLabel.font = \[UIFont systemFontOfSize:14.0f\];
    
        if(indexPath.row == ){  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_quotebill"\];  
            cell.textLabel.text = @"交易记录";  
        }  
        else if(indexPath.row == ){  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_service"\];  
            cell.textLabel.text = @"联系客服";  
        }  
        else{  
            cell.imageView.image = \[UIImage imageNamed:@"ic\_my\_setup"\];  
            cell.textLabel.text = @"设置";  
        }  
    }
    
    return cell;
    } clickCell:^(UITableView \*tableView, NSIndexPath \*indexPath) {
    \[selfWeak clickCell:indexPath\];
    } refreshCell:^{
    \[selfWeak.tableView.arrayTableViewCellList insertObject:selfWeak.secondCell atIndex:\];  
    \[selfWeak.tableView reloadData\];
    } cellCount: identifier:identifier xibName:nil\];

    }
    return _thirdCell;
    }

thirdCell

其中第三个cell可刷新(为了给第二个cell指定新增时的入口)这里是个block:

refreshCell:^{

        \[selfWeak.tableView.arrayTableViewCellList insertObject:selfWeak.secondCell atIndex:\];  
        \[selfWeak.tableView reloadData\];

    }

新增按钮点击事件:

- (void)addTableviewSection:(id)sender{
if(self.thirdCell.refreshBlock){
self.thirdCell.refreshBlock();
}
}

如此实现,在解耦的同时还能简化重复代码量,并且可以最小的代价cost适应频繁变化的UI设计!

PS:目前的封装只支持每个section块的每行row高度是一样的,如果存在不一致的需求,可在我的基础上进行二次封装变化,如果我的文章对您有些许帮助,帮忙点赞标星,如需转载,请说明出处,谢谢!

demo  Github地址:https://github.com/BeckWang0912/ZTCoolTableView  喜欢就标个星星吧✨✨~~~✨✨^o^