如何在呈现 SwiftUI Sheet 视图之前等待 URLSession 请求完成?
How to wait for URLSession Request to finish before rendering a SwiftUI Sheet View?
这里是菜鸟。
我正在制作一个歌词搜索应用程序,它只使用 API 来接收歌曲名称和艺术家姓名以及 returns 歌词。我基本上有两个问题:
第一个存在:我无法显示一个新的 Sheet 以及来自 API 的信息。所以我的代码如下工作:从视图中,按下一个按钮,如果用户连接到互联网,调用一个方法来完成整个 API 调用,创建一个包含该歌曲所有信息的 SongDetails 对象(name, artist and lyrics) 并将其添加到@Published searchedSongs 数组中(之前检查之前是否搜索过相同的歌曲)。完成后,我希望 sheet 显示该数组中的歌词。
我的问题是,当我想从视图访问 searchedSongs 数组时,应用程序崩溃并出现 IndexOutOfRange 错误,因为它似乎实际上并没有在呈现 sheet 之前等待 SongDetails 对象完全添加到数组中。我猜这似乎是某种并发问题。一旦将 SongDetails 对象添加到数组中,有什么方法可以只显示 sheet 吗?我当前的代码是:
HomeView.swift
HStack {
Spacer()
Button(action: {
if(!NetworkMonitor.shared.isConnected) {
self.noConnectionAlert.toggle()
} else {
viewModel.loadApiSongData(songName: songName, artistName: artistName)
self.showingLyricsSheet = true
}
}, label: {
CustomButton(sfSymbolName: "music.note", text: "Search Lyrics!")
})
.alert(isPresented: $noConnectionAlert) {
Alert(title: Text("No internet connection"), message: Text("Oops! It seems you arent connected to the internet. Please connect and try again!"), dismissButton: .default(Text("Got it!")))
}
Spacer()
}
.padding(.top, 20)
.sheet(isPresented: $showingLyricsSheet) {
LyricsView(vm: self.viewModel, songName: songName, artistName: artistName)
}
ViewModel.swift
class ViewModel : ObservableObject {
@Published var searchedSongs = [SongDetails]()
func loadApiSongData(songName: String, artistName: String) {
let rawUrl = "https://api.lyrics.ovh/v1/\(artistName)/\(songName)"
let fixedUrl = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
print("Old url: \(rawUrl)")
print("New url: \(fixedUrl!)")
guard let url = URL(string: fixedUrl!) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Song.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
print("Good. Lyrics:")
if(!self.songAlreadySearched(songName: songName)) {
let song = SongDetails(songName: songName, artistName: artistName, lyrics: decodedResponse.lyrics)
self.searchedSongs.append(song)
}
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
LyricsView.swift
ScrollView {
Text(vm.searchedSongs[0].lyrics)
.foregroundColor(.white)
.multilineTextAlignment(.center)
}
第二个: 我很难理解 URLSession 如何处理错误情况。如果出于某种原因(比如我提交“asd”作为歌曲名称和“fds”作为艺术家名称)api 无法检索歌词,我怎么能从视图中知道这一点并且甚至无法显示歌词 sheet 首先,因为根本不会显示任何歌词。
感谢任何帮助。谢谢!
你的问题没有包含足够的代码,我无法确切地告诉你该怎么做,但我可以给你一般的步骤。
不要在 loadApiSongData
调用后直接设置 showingLyricsSheet
。 loadApiSongData
是异步的,因此这实际上可以保证 sheet 将在 API 调用加载之前显示。相反,将 sheet 的表示绑定到视图模型上的一个变量,该变量仅在 API 请求完成后才设置。我建议使用 sheet(item:)
形式而不是 sheet(isPresented:)
以避免在 sheet.
中获取最近更新的值时常见的陷阱
与其让 LyricsView
访问 vm.searchedSongs
,不如直接将歌曲作为参数传递给 LyricsView
。同样,使用 #1 中的策略(包括使用 sheet(item:)
)很容易。
这是一个简单的模型,说明了 #1 和 #2 中的概念:
struct APIResponse : Identifiable {
var id = UUID()
var apiValues : [String] = []
}
class ViewModel : ObservableObject {
@Published var apiResponse : APIResponse?
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.apiResponse = APIResponse(apiValues: ["Testing","1","2","3"])
}
}
}
struct ContentView : View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text("Hello, world")
.sheet(item: $viewModel.apiResponse) { item in
LyricsView(lyrics: item.apiValues)
}
.onAppear {
viewModel.apiCall()
}
}
}
struct LyricsView : View {
var lyrics : [String]
var body: some View {
Text(lyrics.joined(separator: ", "))
}
}
这里是菜鸟。
我正在制作一个歌词搜索应用程序,它只使用 API 来接收歌曲名称和艺术家姓名以及 returns 歌词。我基本上有两个问题:
第一个存在:我无法显示一个新的 Sheet 以及来自 API 的信息。所以我的代码如下工作:从视图中,按下一个按钮,如果用户连接到互联网,调用一个方法来完成整个 API 调用,创建一个包含该歌曲所有信息的 SongDetails 对象(name, artist and lyrics) 并将其添加到@Published searchedSongs 数组中(之前检查之前是否搜索过相同的歌曲)。完成后,我希望 sheet 显示该数组中的歌词。 我的问题是,当我想从视图访问 searchedSongs 数组时,应用程序崩溃并出现 IndexOutOfRange 错误,因为它似乎实际上并没有在呈现 sheet 之前等待 SongDetails 对象完全添加到数组中。我猜这似乎是某种并发问题。一旦将 SongDetails 对象添加到数组中,有什么方法可以只显示 sheet 吗?我当前的代码是:
HomeView.swift
HStack {
Spacer()
Button(action: {
if(!NetworkMonitor.shared.isConnected) {
self.noConnectionAlert.toggle()
} else {
viewModel.loadApiSongData(songName: songName, artistName: artistName)
self.showingLyricsSheet = true
}
}, label: {
CustomButton(sfSymbolName: "music.note", text: "Search Lyrics!")
})
.alert(isPresented: $noConnectionAlert) {
Alert(title: Text("No internet connection"), message: Text("Oops! It seems you arent connected to the internet. Please connect and try again!"), dismissButton: .default(Text("Got it!")))
}
Spacer()
}
.padding(.top, 20)
.sheet(isPresented: $showingLyricsSheet) {
LyricsView(vm: self.viewModel, songName: songName, artistName: artistName)
}
ViewModel.swift
class ViewModel : ObservableObject {
@Published var searchedSongs = [SongDetails]()
func loadApiSongData(songName: String, artistName: String) {
let rawUrl = "https://api.lyrics.ovh/v1/\(artistName)/\(songName)"
let fixedUrl = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
print("Old url: \(rawUrl)")
print("New url: \(fixedUrl!)")
guard let url = URL(string: fixedUrl!) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Song.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
print("Good. Lyrics:")
if(!self.songAlreadySearched(songName: songName)) {
let song = SongDetails(songName: songName, artistName: artistName, lyrics: decodedResponse.lyrics)
self.searchedSongs.append(song)
}
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
LyricsView.swift
ScrollView {
Text(vm.searchedSongs[0].lyrics)
.foregroundColor(.white)
.multilineTextAlignment(.center)
}
第二个: 我很难理解 URLSession 如何处理错误情况。如果出于某种原因(比如我提交“asd”作为歌曲名称和“fds”作为艺术家名称)api 无法检索歌词,我怎么能从视图中知道这一点并且甚至无法显示歌词 sheet 首先,因为根本不会显示任何歌词。
感谢任何帮助。谢谢!
你的问题没有包含足够的代码,我无法确切地告诉你该怎么做,但我可以给你一般的步骤。
不要在
中获取最近更新的值时常见的陷阱loadApiSongData
调用后直接设置showingLyricsSheet
。loadApiSongData
是异步的,因此这实际上可以保证 sheet 将在 API 调用加载之前显示。相反,将 sheet 的表示绑定到视图模型上的一个变量,该变量仅在 API 请求完成后才设置。我建议使用sheet(item:)
形式而不是sheet(isPresented:)
以避免在 sheet.与其让
LyricsView
访问vm.searchedSongs
,不如直接将歌曲作为参数传递给LyricsView
。同样,使用 #1 中的策略(包括使用sheet(item:)
)很容易。
这是一个简单的模型,说明了 #1 和 #2 中的概念:
struct APIResponse : Identifiable {
var id = UUID()
var apiValues : [String] = []
}
class ViewModel : ObservableObject {
@Published var apiResponse : APIResponse?
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.apiResponse = APIResponse(apiValues: ["Testing","1","2","3"])
}
}
}
struct ContentView : View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text("Hello, world")
.sheet(item: $viewModel.apiResponse) { item in
LyricsView(lyrics: item.apiValues)
}
.onAppear {
viewModel.apiCall()
}
}
}
struct LyricsView : View {
var lyrics : [String]
var body: some View {
Text(lyrics.joined(separator: ", "))
}
}