系统:iOS 17.2
开发平台:Xcode 15.1
编程语言:Swift 5.9.2
图形界面设计:UIKit 框架
数据持久化:UserDefaults
音乐播放:AVFoundation 框架
本项目的主要目标是提供一个直观、简单易用的平台,让用户能够方便地浏览、收藏和播放自己喜欢的音乐(大概)。
故事板是应用程序界面的可视化设计工具,通过连接各个场景(View Controller)来呈现应用的导航流程。以下是项目的六个主要界面的场景布局描述:
主要代码:
let user = userId.text!
let pwd = password.text!
userpwd = UserDefaults.standard.object(forKey: "userpwd") as?[String:String] ?? [String:String]()
if userpwd[user] == pwd {
self.performSegue(withIdentifier: "LOGIN", sender: nil)
}else{
let alertController = UIAlertController(title: "登录失败", message: nil, preferredStyle: .alert)
self.present(alertController, animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+1){
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
let user = userId.text!
let pwd = password.text!
userpwd = UserDefaults.standard.object(forKey: "userpwd") as?[String:String] ?? [String:String]()
userpwd[user] = pwd
UserDefaults.standard.set(userpwd, forKey: "userpwd")
UserDefaults.standard.synchronize()
let ok = UIAlertAction(title:"确定", style:.default){
action in self.dismiss(animated: true, completion: nil)
}
let alter = UIAlertController(title: "注册成功", message: nil, preferredStyle: .alert)
alter.addAction(ok)
self.present(alter,animated: true, completion: nil)
override func viewDidLoad() {
super.viewDidLoad()
let plistPath = Bundle.main.path(forResource: "MusicList", ofType: "plist")
self.listMusics = NSArray(contentsOfFile: plistPath!)
}
②表格显示:
在 numberOfSections 方法中返回表格的分区数,这里只有一个分区。
在 tableView(:numberOfRowsInSection:) 方法中返回表格的行数,即歌曲的数量。
在 tableView(:cellForRowAt:) 方法中配置每个表格行的显示,使用 MusicTableViewCell 自定义单元格来展示歌曲信息。
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listMusics.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ID", for: indexPath) as! MusicTableViewCell
let row = indexPath.row
let rowDict = self.listMusics[row] as! [String:String]
cell.myName.text = rowDict["name"]
cell.mySinger.text = rowDict["singer"]
let imagePath = String(format: "%@.png" , rowDict["image"]!)
cell.myImageView.image = UIImage(named: imagePath)
cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator
return cell
}
③表格行点击处理:
实现 prepare(for segue: UIStoryboardSegue, sender: Any?) 方法,在表格行点击时跳转到单曲播放界面。
通过获取选中行的索引,从 listMusics 数组中取得相应的歌曲信息,将其传递给 PlayingViewController。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PLAY" {
let playingViewController = segue.destination as! PlayingViewController
let indexPath = self.tableView.indexPathForSelectedRow as IndexPath?
let selectedIndex = indexPath!.row
let rowDict = self.listMusics[selectedIndex] as! [String:String]
playingViewController.listData = rowDict
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.label.text = "播放停止"
let imagePath = String(format: "%@.png", listData["image"]!)
ImageView.image = UIImage(named: imagePath)
}
②播放按钮处理: 实现 play(_ : ) 方法,用于处理播放按钮点击事件。在方法中,检查播放器是否已经初始化,若未初始化,则根据 listData 中的歌曲文件名获取音频文件路径,并创建 AVAudioPlayer 实例。播放器准备就绪后,根据当前播放状态进行播放或暂停操作,并更新界面文字显示。
if self.player == nil {
let path = Bundle.main.path(forResource: listData["name"]!, ofType: "mp3")
let url = URL(fileURLWithPath: path!)
do {
self.player = try AVAudioPlayer(contentsOf: url)
} catch let error as NSError {
self.player = nil
print(error.description)
self.label.text = "播放错误"
return
}
self.player.prepareToPlay()
self.player.numberOfLoops = -1
player.delegate = self
}
if !self.player.isPlaying {
player.play()
self.label.text = "正在播放"
let pauseImage = UIImage(named: "pause")
self.btnplay.setImage(pauseImage, for: UIControl.State())
}else{
player.pause()
self.label.text = "播放暂停"
let playImage = UIImage(named: "play")
self.btnplay.setImage(playImage, for: UIControl.State())
}
}
音乐播放时,文字状态改变:
if let player = self.player, player.isPlaying {
player.stop()
player.delegate = nil
self.player = nil
self.label.text = "播放停止"
let playImage = UIImage(named: "play")
self.btnplay.setImage(playImage, for: .normal)
}
**④添加到收藏功能:**实现 addToFavorites(_: ) 方法,用于将当前歌曲信息添加到收藏列表。
guard let listData = listData else {return}
let music = Music(name: listData["name"]!, singer: listData["singer"]!)
MusicFavorites.shared.addFavorite(music)
sender.setTitle("已收藏", for: .normal)
通过 MusicFavorites 类的共享实例,将当前歌曲信息创建为 Music 对象并添加到收藏列表中。
performSegue(withIdentifier: "ViewFavorites", sender: self)
使用 performSegue(withIdentifier:sender:) 进行界面跳转,并将当前视图控制器作为 sender 传递给目标视图控制器。
⑥ 进度条功能(未完善):
在界面中添加 UISlider 控件,用于显示和调整音乐播放进度。
实现 updateUI() 方法,更新界面元素,包括进度条的位置。
实现 playCurrentSong() 方法中,监听播放进度的变化,更新进度条位置。
⑦ 歌曲切换功能(未完善):
实现 previousSong( : ) 和 nextSong(: ) 方法,处理切换上一首和下一首按钮点击事件。 根据按钮点击的不同,更新当前歌曲索引,然后调用 playCurrentSong() 方法播放对应歌曲。
@IBAction func previousSong(_ sender: UIButton) {
currentSongIndex -= 1
if currentSongIndex < 0 {
currentSongIndex = allSongs.count - 1
}
playCurrentSong()
}
@IBAction func nextSong(_ sender: UIButton) {
currentSongIndex += 1
if currentSongIndex >= allSongs.count {
currentSongIndex = 0
}
playCurrentSong()
}
收藏列表功能允许用户将歌曲添加至收藏列表,并管理自己的收藏列表,包括查看、删除和移动歌曲。以下是对歌曲收藏功能的实现描述:
**①加载和保存收藏列表:**在 loadFavorites() 方法中,从 UserDefaults 中加载保存的收藏歌曲数据,使用 PropertyListDecoder 进行解码,将解码后的数据赋值给 favorites 数组。
func loadFavorites(){
if let savedFavoritesData = UserDefaults.standard.data(forKey: "favorites"),
let savedFavorites = try? PropertyListDecoder().decode([Music].self, from: savedFavoritesData){
favorites = savedFavorites
}
}
在 saveFavorites() 方法中,使用 PropertyListEncoder 将当前的收藏歌曲数组进行编码,并保存到 UserDefaults 中,实现数据的持久化。
func saveFavorites(){
if let encodedData = try? PropertyListEncoder()
.encode(favorites){
UserDefaults.standard.set(encodedData, forKey: "favorites")
}
}
②编辑模式切换:实现 editAction(_: ) 方法,该方法在点击编辑按钮时被调用,用于切换表格视图的编辑模式。通过设置 tableView.isEditing 属性,切换表格视图的编辑状态,并根据编辑状态改变编辑按钮的标题。
self.tableView.isEditing = !self.tableView.isEditing
switch tableView.isEditing {
case true:
editButton.title = "确认"
default:
editButton.title = "管理收藏"
}
③表格数据源方法:
重写 numberOfSections(in:) 方法,返回表格视图的分区数,此处只有一个分区。
重写 tableView(:numberOfRowsInSection:) 方法,返回收藏歌曲的数量。
重写 tableView(:cellForRowAt:) 方法,配置表格视图中的每一行,显示歌曲信息。
④编辑样式和操作:
重写 tableView(:editingStyleForRowAt:) 方法,根据编辑状态返回相应的编辑样式,此处为删除样式。
重写 tableView(:commit:forRowAt:) 方法,处理用户对行的删除操作,包括从数组中移除歌曲、删除对应的表格行以及保存更新后的收藏列表到 UserDefaults。
**⑤移动行操作:**重写 tableView(:moveRowAt:to:) 方法,处理用户移动行的操作,包括在收藏数组中移动歌曲,并保存更新后的收藏列表到 UserDefaults。
**⑥刷新数据:**在 viewWillAppear(: ) 方法中,每次视图即将显示时调用 loadFavorites() 方法加载最新的收藏列表,并刷新表格视图显示。
云音乐平台功能是通过使用 WKWebView 控件在应用内显示网易云音乐网站的功能。以下是对该功能的实现描述:
**①导航至网易云音乐网站:**在 viewDidLoad() 方法中,初始化 WKWebView 控件并设置其导航代理为当前视图控制器;构建一个 URLRequest 对象,其中包含网易云音乐网站的 URL。通过调用 load(_: ) 方法加载网页请求,显示网易云音乐网站内容。
override func viewDidLoad() {
super.viewDidLoad()
self.webView.navigationDelegate = self
if let url = URL(string: "http://music.163.com") {
let request = URLRequest(url: url)
self.webView.load(request)
}
}
②WKNavigationDelegate 方法:
实现 webView(_:didFinish:) 方法,该方法在网页加载完成时被调用,此处打印了 “Finish” 表示加载完成。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finish")
}
import UIKit
class RegisterViewController: UIViewController {
@IBOutlet weak var userId: UITextField!
@IBOutlet weak var password: UITextField!
var userpwd = [String:String]()
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func register(_ sender: UIButton) {
let user = userId.text!
let pwd = password.text!
userpwd = UserDefaults.standard.object(forKey: "userpwd") as?[String:String] ?? [String:String]()
userpwd[user] = pwd
UserDefaults.standard.set(userpwd, forKey: "userpwd")
UserDefaults.standard.synchronize()
let ok = UIAlertAction(title:"确定", style:.default){
action in self.dismiss(animated: true, completion: nil)
}
let alter = UIAlertController(title: "注册成功", message: nil, preferredStyle: .alert)
alter.addAction(ok)
self.present(alter,animated: true, completion: nil)
}
}
import UIKit
class LoginViewController: UIViewController {
@IBOutlet weak var userId: UITextField!
@IBOutlet weak var password: UITextField!
var userpwd = [String:String]()
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func Login(_ sender: UIButton) {
let user = userId.text!
let pwd = password.text!
userpwd = UserDefaults.standard.object(forKey: "userpwd") as?[String:String] ?? [String:String]()
if userpwd[user] == pwd {
self.performSegue(withIdentifier: "LOGIN", sender: nil)
}else{
let alertController = UIAlertController(title: "登录失败", message: nil, preferredStyle: .alert)
self.present(alertController, animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+1){
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
}
}
import UIKit
class MusicTableViewController: UITableViewController {
var listMusics:NSArray!
var listData:[String:String]!
override func viewDidLoad() {
super.viewDidLoad()
let plistPath = Bundle.main.path(forResource: "MusicList", ofType: "plist")
self.listMusics = NSArray(contentsOfFile: plistPath!)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listMusics.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ID", for: indexPath) as! MusicTableViewCell
let row = indexPath.row
let rowDict = self.listMusics[row] as! [String:String]
cell.myName.text = rowDict["name"]
cell.mySinger.text = rowDict["singer"]
let imagePath = String(format: "%@.png" , rowDict["image"]!)
cell.myImageView.image = UIImage(named: imagePath)
cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator
return cell
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PLAY" {
let playingViewController = segue.destination as! PlayingViewController
let indexPath = self.tableView.indexPathForSelectedRow as IndexPath?
let selectedIndex = indexPath!.row
let rowDict = self.listMusics[selectedIndex] as! [String:String]
playingViewController.listData = rowDict
}
}
}
import UIKit
class MusicTableViewCell: UITableViewCell {
@IBOutlet weak var myImageView: UIImageView!
@IBOutlet weak var myName: UILabel!
@IBOutlet weak var mySinger: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
import UIKit
import AVFoundation
extension Collection {
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
class PlayingViewController: UIViewController, AVAudioPlayerDelegate {
var listData: [String:String]!
var allSongs: [Music] = []
var currentSongIndex: Int = 0
@IBOutlet weak var ImageView: UIImageView!
@IBOutlet weak var label: UILabel!
@IBOutlet weak var btnplay: UIButton!
var player:AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
allSongs = loadMusicData()
self.label.text = "播放停止"
let imagePath = String(format: "%@.png", listData["image"]!)
ImageView.image = UIImage(named: imagePath)
progressSlider.maximumValue = Float(player.duration)
progressSlider.value = 0.0
// 添加事件处理函数
progressSlider.addTarget(self, action: #selector(progressSliderValueChanged(_:)), for: .valueChanged)
}
func loadMusicData() -> [Music] {
return []
}
func updateUI(){
}
@objc func progressSliderValueChanged(_ sender: UISlider) {
// 当进度条数值发生变化时,更新音乐播放器的播放进度
let newPosition = Double(sender.value)
player.currentTime = TimeInterval(newPosition)
}
func playCurrentSong(){
guard let currentSong = allSongs[safe: currentSongIndex] else {
return
}
let songURL = Bundle.main.url(forResource: currentSong.name, withExtension: "mp3")
do{
player = try AVAudioPlayer(contentsOf: songURL!)
player.prepareToPlay()
player.numberOfLoops = -1
player.delegate = self
player.play()
} catch {
print("创建失败: \(error.localizedDescription)")
}
updateUI()
}
@IBOutlet weak var progressSlider: UISlider!
@IBAction func play(_ sender: UIButton) {
/*
sender.isHighlighted = false
//检查播放按钮状态
//print("按钮状态:\(sender.state.rawValue)")
if self.player == nil {
// 获取音乐文件的完整路径
if let musicFilePath = Bundle.main.url(forResource: listData["name"], withExtension: "mp3", subdirectory: "MusicFile") {
//检查是否读取了正确的文件路径
//print("\(musicFilePath)")
*/
if self.player == nil {
let path = Bundle.main.path(forResource: listData["name"]!, ofType: "mp3")
let url = URL(fileURLWithPath: path!)
do {
self.player = try AVAudioPlayer(contentsOf: url)
} catch let error as NSError {
self.player = nil
print(error.description)
self.label.text = "播放错误"
return
}
self.player.prepareToPlay()
self.player.numberOfLoops = -1
player.delegate = self
}
if !self.player.isPlaying {
player.play()
self.label.text = "正在播放"
let pauseImage = UIImage(named: "pause")
self.btnplay.setImage(pauseImage, for: UIControl.State())
}else{
player.pause()
self.label.text = "播放暂停"
let playImage = UIImage(named: "play")
self.btnplay.setImage(playImage, for: UIControl.State())
}
}
@IBAction func stop(_ sender: UIButton) {
if let player = self.player, player.isPlaying {
player.stop()
player.delegate = nil
self.player = nil
self.label.text = "播放停止"
let playImage = UIImage(named: "play")
self.btnplay.setImage(playImage, for: .normal)
}
}
@IBAction func addToFavorites(_ sender: UIButton) {
guard let listData = listData else {return}
let music = Music(name: listData["name"]!, singer: listData["singer"]!)
MusicFavorites.shared.addFavorite(music)
sender.setTitle("已收藏", for: .normal)
}
@IBAction func viewFavorites(_ sender: UIButton) {
performSegue(withIdentifier: "ViewFavorites", sender: self)
}
@IBAction func previousSong(_ sender: UIButton) {
currentSongIndex -= 1
if currentSongIndex < 0 {
currentSongIndex = allSongs.count - 1
}
playCurrentSong()
}
@IBAction func nextSong(_ sender: UIButton) {
currentSongIndex += 1
if currentSongIndex >= allSongs.count {
currentSongIndex = 0
}
playCurrentSong()
}
}
import UIKit
import WebKit
class CloudViewController: UIViewController, WKNavigationDelegate {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.webView.navigationDelegate = self
if let url = URL(string: "http://music.163.com") {
let request = URLRequest(url: url)
self.webView.load(request)
}
}
// WKNavigationDelegate 方法
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finish")
}
}
import UIKit
class FavoritesTableViewCell: UITableViewCell {
@IBOutlet weak var songNameLabel: UILabel!
@IBOutlet weak var singerLabel: UILabel!
func configureCell(with song: Music){
songNameLabel.text = song.name
singerLabel.text = song.singer
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
import UIKit
class FavoritesTableViewController: UITableViewController {
@IBOutlet weak var editButton: UIBarButtonItem!
var favorites: [Music] = []
override func viewDidLoad() {
super.viewDidLoad()
loadFavorites()
tableView.reloadData()
}
func loadFavorites(){
if let savedFavoritesData = UserDefaults.standard.data(forKey: "favorites"),
let savedFavorites = try? PropertyListDecoder().decode([Music].self, from: savedFavoritesData){
favorites = savedFavorites
}
}
func saveFavorites(){
if let encodedData = try? PropertyListEncoder()
.encode(favorites){
UserDefaults.standard.set(encodedData, forKey: "favorites")
}
}
@IBAction func editAction(_ sender: UIBarButtonItem) {
//切换编辑模式
self.tableView.isEditing = !self.tableView.isEditing
switch tableView.isEditing {
case true:
editButton.title = "确认"
default:
editButton.title = "管理收藏"
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 获取收藏的歌曲数量
//return MusicFavorites.shared.favoriteSongs.count
return favorites.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FavoritesCell", for: indexPath) as! FavoritesTableViewCell
//获取对应位置的歌曲信息
//let song = MusicFavorites.shared.favoriteSongs[indexPath.row]
let song = favorites[indexPath.row]
cell.configureCell(with: song)
return cell
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadFavorites()
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
if (self.tableView.isEditing) {
return UITableViewCell.EditingStyle.delete
}
return UITableViewCell.EditingStyle.none
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
favorites.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
// 保存收藏数组到UserDefaults
saveFavorites()
}
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movedSong = favorites[sourceIndexPath.row]
favorites.remove(at: sourceIndexPath.row)
favorites.insert(movedSong, at: destinationIndexPath.row)
saveFavorites()
}
}
import Foundation
class MusicFavorites{
static let shared = MusicFavorites()
private init(){
}
var favoriteSongs: [Music] = []
func addFavorite(_ song: Music){
favoriteSongs.append(song)
saveFavorites()
}
func saveFavorites(){
if let encodedData = try? PropertyListEncoder() .encode(favoriteSongs){
UserDefaults.standard.set(encodedData, forKey: "favorites")
}
}
}
import Foundation
class Music: Codable {
var name: String
var singer: String
init(name: String, singer: String) {
self.name = name
self.singer = singer
}
}
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- gamedaodao.com 版权所有 湘ICP备2022005869号-6
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务