[英]Why aren't my Swift network requests working?
I've been trying out Swift/SwiftUI for the first time, so I decided to make a small Hacker News client.我是第一次尝试 Swift/SwiftUI,所以我决定做一个小型的 Hacker News 客户端。 I seem to be able to get the list of ids of the top stories back fine, but once the dispatchGroup
gets involved, nothing works.我似乎能够很好地获取热门故事的 ID 列表,但是一旦dispatchGroup
参与进来,就没有任何效果。 What am I doing wrong?我究竟做错了什么?
Data.swift
import SwiftUI
struct HNStory: Codable, Identifiable {
var id: UInt
var title: String
var score: UInt
}
class Fetch {
func getStory(id: Int, completion: @escaping (HNStory) -> ()) {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")
URLSession.shared.dataTask(with: url!) { (data, _, _) in
let story = try!JSONDecoder().decode(HNStory.self, from: data!)
print(id, story)
DispatchQueue.main.async {
completion(story)
}
}
}
func getStories(completion: @escaping ([HNStory]) -> ()) {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")
var stories: [HNStory] = []
print("here")
URLSession.shared.dataTask(with: url!) { (data, _, _) in
var ids = try!JSONDecoder().decode([Int].self, from: data!)
ids = Array(ids[0...10])
print(ids)
let dispatchGroup = DispatchGroup()
for id in ids {
dispatchGroup.enter()
self.getStory(id: id) { (story) in
stories.append(story)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
print("Completed work")
DispatchQueue.main.async {
completion(stories)
}
}
}.resume()
}
}
ContentView.swift
(probably doesn't matter, but just in case) ContentView.swift
(可能没关系,但以防万一)
import SwiftUI
struct ContentView: View {
@State private var stories: [HNStory] = []
var body: some View {
Text("Hacker News").font(.headline)
List(stories) { story in
VStack {
Text(story.title)
Text(story.score.description)
}
}.onAppear{
Fetch().getStories { (stories) in
self.stories = stories
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
One major problem is this line:一个主要问题是这一行:
Fetch().getStories...
Networking takes time.网络需要时间。 You make a Fetch instance and immediately let it destroy itself.您创建一个 Fetch 实例并立即让它自行销毁。 Thus it does not survive long enough to do any networking.因此,它的生存时间不足以进行任何联网。 You need to configure a singleton that persists.您需要配置一个持续存在的 singleton。
Another problem, as OOPer points out in a comment, is that your getStory
creates a data task but never tells it to resume
— so that method is doing no networking at all, even if it did have time to do so.正如 OOPer 在评论中指出的那样,另一个问题是您的getStory
创建了一个数据任务,但从不告诉它resume
- 因此该方法根本没有网络,即使它确实有时间这样做。
FWIW, with Swift UI, I would suggest you consider using Combine publishers for your network requests. FWIW,使用 Swift UI,我建议您考虑为您的网络请求使用组合发布者。
So, the publisher to get a single story:所以,出版商得到一个单一的故事:
func storyUrl(for id: Int) -> URL {
URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!
}
func hackerNewsStoryPublisher(for identifier: Int) -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: storyUrl(for: identifier))
.map(\.data)
.decode(type: HNStory.self, decoder: decoder)
.eraseToAnyPublisher()
}
And a publisher for a sequence of the above:以及上述序列的发布者:
func hackerNewsIdsPublisher(for ids: [Int]) -> AnyPublisher<HNStory, Error> {
Publishers.Sequence(sequence: ids.map { hackerNewsStoryPublisher(for: $0) })
.flatMap(maxPublishers: .max(4)) { $0 }
.eraseToAnyPublisher()
}
Note, the above constrains it to four at a time, enjoying the performance gains of concurrency, but limiting it so you do not risk having latter requests time out:请注意,上面一次将其限制为四个,享受并发的性能提升,但限制它,这样您就不会冒后面的请求超时的风险:
Anyway, here is the first fetch of the array of ids and then launching the above:无论如何,这是第一次获取 ids 数组,然后启动上面的内容:
let mainUrl = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")!
func hackerNewsPublisher() -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: mainUrl)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.flatMap { self.hackerNewsIdsPublisher(for: $0) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
(Now, you could probably cram all of the above into a single publisher, but I like to keep them small, so each individual publisher is very easy to reason about.) (现在,您可能可以将以上所有内容都塞进一个发布者中,但我喜欢让它们保持小规模,因此每个单独的发布者都很容易推理。)
So, pulling that all together, you have a view model like so:因此,将所有这些放在一起,您会看到 model 如下所示:
import Combine
struct HNStory: Codable, Identifiable {
var id: UInt
var title: String
var score: UInt
}
class ViewModel: ObservableObject {
@Published var stories: [HNStory] = []
private let networkManager = NetworkManager()
private var request: AnyCancellable?
func fetch() {
request = networkManager.hackerNewsPublisher().sink { completion in
if case .failure(let error) = completion {
print(error)
}
} receiveValue: {
self.stories.append($0)
}
}
}
class NetworkManager {
private let decoder = JSONDecoder()
let mainUrl = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")!
func storyUrl(for id: Int) -> URL {
URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!
}
func hackerNewsPublisher() -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: mainUrl)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.flatMap { self.hackerNewsIdsPublisher(for: $0) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
// publisher for array of news stories, processing max of 4 at a time
func hackerNewsIdsPublisher(for ids: [Int]) -> AnyPublisher<HNStory, Error> {
Publishers.Sequence(sequence: ids.map { hackerNewsStoryPublisher(for: $0) })
.flatMap(maxPublishers: .max(4)) { $0 }
.eraseToAnyPublisher()
}
// publisher for single news story
func hackerNewsStoryPublisher(for identifier: Int) -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: storyUrl(for: identifier))
.map(\.data)
.decode(type: HNStory.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
And your main ContentView
is:你的主要ContentView
是:
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Hacker News").font(.headline)
List(viewModel.stories) { story in
VStack {
Text(story.title)
Text(story.score.description)
}
}.onAppear {
viewModel.fetch()
}
}
}
By calling Fetch().getStories, the Fetch
class goes out of scope immediately and isn't retained.通过调用 Fetch().getStories, Fetch
class 会立即从 scope 中取出并且不会保留。
I'd recommend making Fetch
an ObservableObject
and setting it as a property on your view:我建议制作Fetch
一个ObservableObject
并将其设置为您视图上的属性:
@StateObject var fetcher = Fetch()
Then, call:然后,调用:
fetcher.getStories {
self.stories = stories
}
If you wanted to get even more SwiftUI-ish with it, you may want to look into @Published
properties on ObservableObject
s and how you can make your view respond to them automatically.如果您想使用它获得更多 SwiftUI 风格,您可能需要查看ObservableObject
上的@Published
属性以及如何使您的视图自动响应它们。 By doing this, you could avoid having a @State
variable on your view at all, not have to have a callback function, and instead just load the stories into a @Published
property on the ObservableObject.通过这样做,您可以完全避免在视图中使用@State
变量,不必有回调 function,而只需将故事加载到 ObservableObject 上的@Published
属性中。 Your view will be re-rendered when the @Published
property changes.当@Published
属性更改时,您的视图将重新呈现。 More reading: https://www.hackingwithswift.com/quick-start/swiftui/observable-objects-environment-objects-and-published更多阅读: https://www.hackingwithswift.com/quick-start/swiftui/observable-objects-environment-objects-and-published
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.