在 SwiftUI 中使用滑块创建音频播放器单元格
Posted
技术标签:
【中文标题】在 SwiftUI 中使用滑块创建音频播放器单元格【英文标题】:Create Audio Player cell with slider in SwiftUI 【发布时间】:2021-12-27 17:44:30 【问题描述】:我正在尝试创建一个类似于 WhatsApp 中显示的音频消息视图的视图。
播放器代码。
struct AudioPlayerControlsView: View
private enum PlaybackState: Int
case waitingForSelection
case buffering
case playing
let player: AVPlayer
let timeObserver: PlayerTimeObserver
let durationObserver: PlayerDurationObserver
let itemObserver: PlayerItemObserver
@State private var currentTime: TimeInterval = 0
@State private var currentDuration: TimeInterval = 0
@State private var state = PlaybackState.waitingForSelection
var body: some View
VStack
if state == .waitingForSelection
Text("Select a song below")
else if state == .buffering
Text("Buffering...")
else
Text("Great choice!")
Slider(value: $currentTime,
in: 0...currentDuration,
onEditingChanged: sliderEditingChanged,
minimumValueLabel: Text("\(Utility.formatSecondsToHMS(currentTime))"),
maximumValueLabel: Text("\(Utility.formatSecondsToHMS(currentDuration))"))
// I have no idea in what scenario this View is shown...
Text("seek/progress slider")
.disabled(state != .playing)
.padding()
// Listen out for the time observer publishing changes to the player's time
.onReceive(timeObserver.publisher) time in
// Update the local var
self.currentTime = time
// And flag that we've started playback
if time > 0
self.state = .playing
// Listen out for the duration observer publishing changes to the player's item duration
.onReceive(durationObserver.publisher) duration in
// Update the local var
self.currentDuration = duration
// Listen out for the item observer publishing a change to whether the player has an item
.onReceive(itemObserver.publisher) hasItem in
self.state = hasItem ? .buffering : .waitingForSelection
self.currentTime = 0
self.currentDuration = 0
// TODO the below could replace the above but causes a crash
// // Listen out for the player's item changing
// .onReceive(player.publisher(for: \.currentItem)) item in
// self.state = item != nil ? .buffering : .waitingForSelection
// self.currentTime = 0
// self.currentDuration = 0
//
// MARK: Private functions
private func sliderEditingChanged(editingStarted: Bool)
if editingStarted
// Tell the PlayerTimeObserver to stop publishing updates while the user is interacting
// with the slider (otherwise it would keep jumping from where they've moved it to, back
// to where the player is currently at)
timeObserver.pause(true)
else
// Editing finished, start the seek
state = .buffering
let targetTime = CMTime(seconds: currentTime,
preferredTimescale: 600)
player.seek(to: targetTime) _ in
// Now the (async) seek is completed, resume normal operation
self.timeObserver.pause(false)
self.state = .playing
现在我正在尝试将它添加到 SwiftUI 中的列表单元格中。
struct Audiomessage : Identifiable
var id: UUID
var url : String
var isPlaying : Bool
struct ChatView : View
let player = AVPlayer()
private let items = [ Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", isPlaying: false),Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3", isPlaying: false)]
var body: some View
VStack
LazyVStack
ForEach(items) reason in
AudioPlayerControlsView(player: player,
timeObserver: PlayerTimeObserver(player: player),
durationObserver: PlayerDurationObserver(player: player),
itemObserver: PlayerItemObserver(player: player)).onTapGesture
guard let url = URL(string: reason.url) else
return
let playerItem = AVPlayerItem(url: url)
self.player.replaceCurrentItem(with: playerItem)
self.player.play()
.padding(.bottom,37)
.padding()
.padding(.horizontal,10)
输出如下:
两者都开始在 Tap 上播放。我想一次播放一首歌。如何在 SwiftUI 中实现这一点?
【问题讨论】:
你拥有的不是minimal reproducible example——因为缺少类型,它不会编译。通常,您希望在父视图中存储一个状态是否有音频播放并将该状态(通过EnvironmentObject
或Binding
或类似的东西)传递给子视图,以便子视图可以停止以前的玩家,如果他们想开始。一般来说,我可能只保留 one AVPlayer
并从子视图中进行控制。
也许这太简单了,但你为什么不打电话给stop()
,然后再通过play()
开始。 developer.apple.com/documentation/avfaudio/avaudioplayer/… 然后所有当前的声音将停止并播放新的声音。 ``` .stop() .play() ```
【参考方案1】:
您最终分配了多个AVPlayers
,因为ContentView
结构经常重新创建(所有SwiftUI 视图都是这种情况),因此每次在此代码中发生时都会分配一个新的AVPlayer
:
struct ChatView : View
let player = AVPlayer()
在 ChatView 之外分配一次 AVPlayer
并将其传入,因此只有一个 AVPlayer。
甚至像这样简单的事情:
let player = AVPlayer()
struct ContentView: View
private let items = [ Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", isPlaying: false),
Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3", isPlaying: false)]
var body: some View
// ... rest of your code here
在我的测试中有效。
这是我的测试的完整代码(我删除了您未提供的类,并注释掉了一些用于更新值的代码,因为这需要您未提供的类)
在模拟器中,它会根据您的需要播放音乐并在曲目之间切换。
//
// AudioPlayerControlsView.swift
// TestForSOQuestion
//
//
import SwiftUI
import AVKit
enum Utility
static func formatSecondsToHMS(_ seconds: TimeInterval) -> String
let secondsInt:Int = Int(seconds.rounded(.towardZero))
let dh: Int = (secondsInt/3600)
let dm: Int = (secondsInt - (dh*3600))/60
let ds: Int = secondsInt - (dh*3600) - (dm*60)
let hs = "\(dh > 0 ? "\(dh):" : "")"
let ms = "\(dm<10 ? "0" : "")\(dm):"
let s = "\(ds<10 ? "0" : "")\(ds)"
return hs + ms + s
struct AudioPlayerControlsView: View
private enum PlaybackState: Int
case waitingForSelection
case buffering
case playing
let player: AVPlayer
let timeObserver: PlayerTimeObserver
let durationObserver: PlayerDurationObserver
let itemObserver: PlayerItemObserver
@State private var currentTime: TimeInterval = 0
@State private var currentDuration: TimeInterval = 0
@State private var state = PlaybackState.waitingForSelection
var body: some View
VStack
if state == .waitingForSelection
Text("Select a song below")
else if state == .buffering
Text("Buffering...")
else
Text("Great choice!")
Slider(value: $currentTime,
in: 0...currentDuration,
onEditingChanged: sliderEditingChanged,
minimumValueLabel: Text("\(Utility.formatSecondsToHMS(currentTime))"),
maximumValueLabel: Text("\(Utility.formatSecondsToHMS(currentDuration))"))
// I have no idea in what scenario this View is shown...
Text("seek/progress slider")
.disabled(state != .playing)
.padding()
// MARK: Private functions
private func sliderEditingChanged(editingStarted: Bool)
if editingStarted
// Tell the PlayerTimeObserver to stop publishing updates while the user is interacting
// with the slider (otherwise it would keep jumping from where they've moved it to, back
// to where the player is currently at)
timeObserver.pause(true)
else
// Editing finished, start the seek
state = .buffering
let targetTime = CMTime(seconds: currentTime,
preferredTimescale: 600)
player.seek(to: targetTime) _ in
// Now the (async) seek is completed, resume normal operation
self.timeObserver.pause(false)
self.state = .playing
struct AudioPlayerControlsView_Previews: PreviewProvider
static let previewPlayer: AVPlayer = AVPlayer()
static var previews: some View
AudioPlayerControlsView(player: previewPlayer,
timeObserver: PlayerTimeObserver(player: player),
durationObserver: PlayerDurationObserver(player: player),
itemObserver: PlayerItemObserver(player: player))
还有 ContentView.swift
//
// ContentView.swift
// TestForSOQuestion
//
import SwiftUI
import AVKit
import Combine
class PlayerTimeObserver: ObservableObject
let player: AVPlayer
init(player: AVPlayer)
self.player = player
func pause(_ flag: Bool)
class PlayerDurationObserver
let player: AVPlayer
init(player: AVPlayer)
self.player = player
class PlayerItemObserver
let player: AVPlayer
init(player: AVPlayer)
self.player = player
struct Audiomessage : Identifiable
var id: UUID
var url : String
var isPlaying : Bool
let player = AVPlayer()
struct ContentView: View
private let items = [ Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", isPlaying: false),
Audiomessage(id: UUID(), url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3", isPlaying: false)]
var body: some View
VStack
LazyVStack
ForEach(items) reason in
AudioPlayerControlsView(player: player,
timeObserver: PlayerTimeObserver(player: player),
durationObserver: PlayerDurationObserver(player: player),
itemObserver: PlayerItemObserver(player: player)).onTapGesture
guard let url = URL(string: reason.url) else
return
let playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem)
player.play()
.padding(.bottom,37)
.padding()
.padding(.horizontal,10)
struct ContentView_Previews: PreviewProvider
static var previews: some View
ContentView()
创建一个新的 ios 应用项目,将模板的 ContentView 替换为上面的实现,并添加一个新文件 AudioPlayerControlsView.swift 并将上面提供的内容放入其中。运行并单击其中一行,然后单击另一行,注意音乐播放并从一个切换到另一个。
最初点击时似乎有点延迟,但我认为这是音乐网址的加载。您将不得不弄清楚一些 UX 以明确收到点击操作并且用户需要等待。一旦你点击了每一行并给它时间加载数据似乎加载得更快或什么的。
干杯。
【讨论】:
以上是关于在 SwiftUI 中使用滑块创建音频播放器单元格的主要内容,如果未能解决你的问题,请参考以下文章