iOS——QQ音乐仿写项目总结

QQ音乐仿写总结

完成QQ音乐的仿写项目后学习到了很多东西,现对其中比较关键的地方做总结

首页

tableView嵌套CollectionView

  • 这里是为了实现首页中能纵向滑动的同时可以横向滑动其中的歌单界面,主要原理是将collectionView放进自定义cell的tabelView的每个cell里面,再用collectionView展示出来结果

图片演示:

  • 这里与正常的tableView创建大差不差,唯一需要注意的是cell复用以后,里面的collectionView不会自动更新,需要手动更新一下collectionView
  • 代码展示如下
objc 复制代码
//
//  ProfileSectionTableViewCell.m
//  网易云音乐imit
//
//  Created by 秋雨梧桐叶落莳 on 2026/5/17.
//

#import "ProfileSectionTableViewCell.h"//嵌套
#import "LastCollectionViewCell.h"
#import <Masonry/Masonry.h>

@interface ProfileSectionTableViewCell () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) NSArray *data;
@property (nonatomic, strong) UICollectionView *collectionView;
@end

@implementation ProfileSectionTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self setupUI];
    }
    return self;
}

- (void)setupUI {
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    layout.minimumInteritemSpacing = 8;
    layout.itemSize = CGSizeMake(180, 180);
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    self.collectionView.showsHorizontalScrollIndicator = NO;
    [self.collectionView registerClass:[LastCollectionViewCell class] forCellWithReuseIdentifier:@"Last"];
    [self.contentView addSubview:self.collectionView];
    [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.data.count;
}

- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    LastCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Last" forIndexPath:indexPath];
    [cell configWithModel:self.data[indexPath.item]];
    return cell;
}

- (void)awakeFromNib {
    [super awakeFromNib];
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];
}

- (void)configWithModels:(NSArray *)data {
    self.data = data;
    [self.collectionView reloadData];////注意
}
@end

我的

照片墙

  • 这里也是使用了collectionView对照片进行布局,使用一个私有属性持有当前选择的照片,点击确定以后,通过反向传值把图片传回去更新原视图
  • 代码如下
objc 复制代码
//  AvatorChangeViewController.m
//  网易云音乐imit
//
//  Created by 秋雨梧桐叶落莳 on 2026/5/19.
//
#import <Masonry/Masonry.h>
#import "AvatorChangeViewController.h"

@interface AvatorChangeViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSMutableArray *picture;
@property (nonatomic, strong) UIImageView *avator;
@end

@implementation AvatorChangeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor systemBackgroundColor];
    [self setupData];
    [self setupBar];
    [self setupUI];
}

- (void)setupBar {
    UIBarButtonItem *btn = [[UIBarButtonItem alloc] initWithTitle:@"确定" style:UIBarButtonItemStylePlain target:self action:@selector(tapped)];
    self.navigationItem.rightBarButtonItem = btn;
}

- (void)setupData {
    self.picture = [NSMutableArray array];
    NSArray *arr = @[
        @"HeadPicture",
        @"IMG_0154",
        @"IMG_0201",
        @"IMG_0621",
        @"IMG_2048",
        @"IMG_4693",
        @"IMG_4806",
        @"IMG_4813",
        @"IMG_0202",
        @"IMG_0204"
    ];
    [self.picture addObjectsFromArray:arr];
}

- (void)tapped {
    if ([self.delegate respondsToSelector:@selector(changePictureWithImage:)]) {
        [self.delegate changePictureWithImage:self.avator.image];
    }
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)setupUI {
    self.avator = [[UIImageView alloc] initWithImage:self.avatorImage];
    self.avator.clipsToBounds = YES;
    self.avator.layer.cornerRadius = 50;
    [self.view addSubview:self.avator];
    [self.avator mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
        make.centerX.equalTo(self.view);
        make.height.width.mas_equalTo(100);
    }];
    
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    layout.minimumLineSpacing = 20;
    layout.minimumInteritemSpacing = 20;
    layout.itemSize = CGSizeMake(100, 100);
    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    [self.view addSubview:self.collectionView];
    [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.bottom.equalTo(self.view);
        make.top.equalTo(self.avator.mas_bottom).offset(24);
    }];
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"collection"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.picture.count;
}

- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"collection" forIndexPath:indexPath];
    UIImageView *img = [[UIImageView alloc] initWithImage:[UIImage imageNamed:self.picture[indexPath.item]]];
    [cell.contentView addSubview:img];
    [img mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(cell.contentView);
    }];
    return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    [collectionView deselectItemAtIndexPath:indexPath animated:YES];
    self.avator.image = [UIImage imageNamed:self.picture[indexPath.item]];
}
@end

segmentControl控制cell

这里原本的需求是通过一个segmentControl来切换展示的歌单,我想出了以下两种解决方案

  1. 在一个scrollView里面放两个tableView,通过segmentControl来控制在scrollView的偏移量
  2. 通过一个segmentControl来控制给一个tableView的数据源,再通过数据源切换给cell的显示,再添加左右滑手势

我最终选择了下面的写法,由于我把segmentControl放在了headerfooterview里面,所以需要把segmentControl的结果从里面传出来再更新,代码稍显繁琐

objc 复制代码
@property (nonatomic, assign) NSInteger judge;//0自建1喜欢



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
        ProfileSectionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProfileSection" forIndexPath:indexPath];
        [cell configWithModels:self.section];
        return cell;
    }
    ProfileGroupSongTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProfileGroup" forIndexPath:indexPath];
    if (self.judge == 0) {
        [cell configWithModel:self.section[indexPath.row]];
    } else {
        [cell configWithModel:self.loveSection[indexPath.row]];
    }
    return cell;
}//刷新时响应model改动

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    ProfileHeader *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"ProfileHeader"];
    if (section == 0) {
        [header configWithType:Last andIndex:self.judge];
    } else {
        [header configWithType:Myself andIndex:self.judge];
    }
    __weak typeof(self) weakSelf = self;
    header.changeSeg = ^(SectionType type) {
        if (type == Last) {
            weakSelf.judge = 0;
        } else {
            weakSelf.judge = 1;
        }
        NSIndexSet *set = [NSIndexSet indexSetWithIndex:1];
        [weakSelf.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationFade];
        
        
    };
    return header;
}//block传值响应segmentControl的改动

抽屉视图

这部分在我另一篇博客亦有记载,感兴趣的读者可以看看

iOS------抽屉视图详解

更多

黑夜模式

关于黑夜模式,我使用的是window的一个属性,叫做overrideUserInterfaceStyle,这个属性可以被两个枚举值赋值,分别是UIUserInterfaceStyleDarkUIUserInterfaceStyleLight,分别对应暗色模式和亮色模式

  • 由于在设置视图颜色的时候都设置为systemcolor以及文字使用labelcolor等会自动随黑夜模式更改而变色的颜色,所以更改亮暗色会自动更改,实现黑夜模式的更改

数据持久化

  • 最后就是对黑夜模式更改的数据持久化,这里使用到了NSUserDefaults,需要在sceneDelegate的\- (**void**)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions方法中,读取上一次存储的亮暗模式
objc 复制代码
		BOOL isDark = [[NSUserDefaults standardUserDefaults] boolForKey:@"isDark"];
    self.window.overrideUserInterfaceStyle = isDark ? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight;
  • 至于存储,直接在更多界面切换控制亮暗模式的UISwitch的时候直接存储
objc 复制代码
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    SettingTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    SectionSettingModel *sectionModel = self.section[indexPath.section];
    CellModel *modelNow = sectionModel.cell[indexPath.row];
    [cell configWithModel:modelNow];
    __weak typeof(self) weakSelf = self;
    cell.switchLight = ^(CellModel * _Nonnull model, BOOL isOn) {
        modelNow.isOn = isOn;
        if ([model.title isEqualToString:@"黑夜模式"]) {
            if (isOn == YES) {
                weakSelf.view.window.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
            } else {
                weakSelf.view.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
            }
        }
        [[NSUserDefaults standardUserDefaults] setBool:isOn forKey:@"isDark"];
    };
    return cell;
}
相关推荐
编程版小新2 小时前
Day1:体验产品,以画图方式梳理用户操作和管理员操作
学习
MXsoft6182 小时前
**用自动化脚本给MAC误阻断留条后路:可审计、可回滚的准入控制方案**
运维·macos·自动化
三品吉他手会点灯3 小时前
STM32F103 学习笔记-24-I2C-读写EEPROM(第2节)-I2C协议层介绍
笔记·stm32·学习
z200509303 小时前
【C++学习】C++ 类型转换深度解析:从 C 风格缺陷到 C++ 四种安全转换的思想内核
c语言·c++·学习
iUNPo3 小时前
WWDC26 技术解读:Apple Intelligence、Siri AI 与苹果生态的下一步
macos·ios·wwdc
三品吉他手会点灯3 小时前
STM32F103 学习笔记-24-I2C-读写EEPROM(第3节)-STM32的I2C框图详解
笔记·stm32·学习
踏着七彩祥云的小丑3 小时前
嵌入式测试学习第 36 天:串口日志分析、通过日志定位简单问题
单片机·嵌入式硬件·学习
小米渣的逆袭3 小时前
macos上一个好用的PDF文字提取工具方案
macos·pdf
MartinYeung53 小时前
[论文学习]LLM 情境学习资料的快速精确遗忘技术:基于 In-Context Learning 与量化 K-Means 的 ERASE 方法
学习·算法·kmeans