如何在呈现 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 首先,因为根本不会显示任何歌词。

感谢任何帮助。谢谢!

你的问题没有包含足够的代码,我无法确切地告诉你该怎么做,但我可以给你一般的步骤。

  1. 不要在 loadApiSongData 调用后直接设置 showingLyricsSheetloadApiSongData 是异步的,因此这实际上可以保证 sheet 将在 API 调用加载之前显示。相反,将 sheet 的表示绑定到视图模型上的一个变量,该变量仅在 API 请求完成后才设置。我建议使用 sheet(item:) 形式而不是 sheet(isPresented:) 以避免在 sheet.

    中获取最近更新的值时常见的陷阱
  2. 与其让 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: ", "))
    }
}