【问题标题】:How to wait for URLSession Request to finish before rendering a SwiftUI Sheet View?如何在渲染 SwiftUI 工作表视图之前等待 URLSession 请求完成?
【发布时间】:2021-05-03 05:34:03
【问题描述】:

这里是菜鸟。

我正在制作一个歌词搜索应用程序,它简单地使用一个 API,它接收歌曲名称和艺术家姓名并简单地返回歌词。我基本上有两个问题:

第一个存在:我无法显示包含来自 API 的信息的新工作表。所以我的代码工作如下:从视图中,按下一个按钮,如果用户连接到互联网,调用一个执行整个 API 调用的方法,创建一个 SongDetails 对象,其中包含该歌曲的所有信息(名称,艺术家和歌词)并将其添加到 @Published searchedSongs 数组(之前检查过之前没有搜索过相同的歌曲)。完成后,我希望工作表显示该数组中的歌词。 我的问题是当我想从视图访问 searchedSongs 数组时,应用程序崩溃并出现 IndexOutOfRange 错误,因为它似乎实际上并没有等待 SongDetails 对象在渲染工作表之前完全添加到数组中。我猜这似乎是某种并发问题。有没有办法只在将 SongDetails 对象添加到数组后才显示工作表?我当前的代码是:

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 无法检索歌词,我怎么能从视图中知道这一点,甚至无法在第一名,因为根本没有歌词可以显示。

感谢任何帮助。谢谢!

【问题讨论】:

  • 您的 URLSession 问题可能应该拆分为自己的问题。当您这样做时,它还可以使用更多细节,例如,如果您指的是诸如 HTTP 错误之类的错误情况,或者您是否从 API 端点获得了某些表示错误的响应(即不同的 JSON 值)。
  • 谢谢!我已经提出了一个单独的问题,以防您想快速浏览一下!

标签: swift api rest swiftui urlsession


【解决方案1】:

您的问题没有包含足够的代码,我无法确切地向您展示该怎么做,但我可以为您提供一般步骤。

  1. 不要在loadApiSongData 调用之后直接设置showingLyricsSheetloadApiSongData 是异步的,因此这实际上可以保证在 API 调用加载之前显示工作表。相反,将工作表的表示绑定到视图模型上的变量,该变量仅在 API 请求完成后才设置。我建议使用sheet(item:) 表单而不是sheet(isPresented:),以避免在获取工作表中最近更新的值时常见的陷阱。

  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: ", "))
    }
}

【讨论】:

  • 非常感谢!该工作表现在按预期工作,并且仅在请求完成时呈现!谢谢!我还有另一个问题,由于我缺少字符,因此我将详细说明以下 pastebin:pastebin.com/uk8QhR7F 谢谢!
  • 很高兴它对你有用。可以使用单击绿色复选标记将答案标记为正确/接受吗?如果有帮助,您也可以使用箭头进行投票。关于您在 pastebin 中的问题,我建议您使用我在上面使用的相同 sheet(item:) 策略。它应该可以解决空白歌词视图问题。
  • 绝对!考虑完成。顺便说一句,使用 sheet(item:) 再次解决了它。非常感谢你:)。只要您有 2 分钟的时间,请随时查看我的其他问题!
  • 看起来您已经收到了回复,但如果它不符合您的需求,请告诉我,我可以尝试提出其他建议。
猜你喜欢
  • 2018-05-02
  • 2021-11-21
  • 2020-06-17
  • 1970-01-01
  • 1970-01-01
  • 2022-12-14
  • 1970-01-01
  • 2016-01-24
  • 2014-11-15
相关资源
最近更新 更多