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;
}
相关推荐
吃好睡好便好1 小时前
泰戈尔的诗歌7
学习·生活
星夜夏空992 小时前
C++学习(2) —— 类与对象基础
开发语言·c++·学习
-To be number.wan3 小时前
数据库系统 | 数据库安全与完整性
数据库·学习
czysoft3 小时前
se被限速
科技·学习·it·技术·魔法·先进·领先
子不语1804 小时前
从0开始学习S7-1200+ET200SP(3)——两台S7-1200通过TCP连接
网络协议·学习·tcp/ip
llllliznc4 小时前
LLM 学习笔记 Day 5:Agent 核心组件——Planner、Memory 与 Reflection
笔记·学习
hyhsandy18034 小时前
STM32F103 TIM学习笔记
笔记·stm32·学习
GuHenryCheng5 小时前
【ESP32】ESP-IDF开发环境搭建(cursor)
git·stm32·单片机·学习
汤姆yu5 小时前
macOS系统下Aider完整安装、配置与实战使用教程
大数据·人工智能·算法·macos·github·copilot
编程圈子6 小时前
电机驱动开发学习18. SVPWM空间矢量调制算法详解与实现
驱动开发·学习·算法