为啥我的 Swift 网络请求不起作用?
Posted
技术标签:
【中文标题】为啥我的 Swift 网络请求不起作用?【英文标题】:Why aren't my Swift network requests working?为什么我的 Swift 网络请求不起作用? 【发布时间】:2021-03-14 00:30:25 【问题描述】:我是第一次尝试 Swift/SwiftUI,所以我决定制作一个小型 Hacker News 客户端。我似乎能够很好地获取热门故事的 ID 列表,但是一旦 dispatchGroup
参与其中,就没有任何效果。我做错了什么?
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
(可能没关系,但以防万一)
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()
【问题讨论】:
你没有在getStory
中调用resume()
。
【参考方案1】:
一个主要问题是这一行:
Fetch().getStories...
网络需要时间。您创建一个 Fetch 实例并立即让它自行销毁。因此,它的生存时间不足以进行任何联网!你需要配置一个持久化的单例。
另一个问题,正如 OOPer 在评论中指出的那样,是您的 getStory
创建了一个数据任务,但从未将其告诉 resume
— 所以该方法根本没有网络,即使它 有时间去做。
【讨论】:
【参考方案2】:FWIW,使用 Swift UI,我建议您考虑使用组合发布者来处理您的网络请求。
所以,出版商得到一个单一的故事:
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()
以及上述序列的发布者:
func hackerNewsIdsPublisher(for ids: [Int]) -> AnyPublisher<HNStory, Error>
Publishers.Sequence(sequence: ids.map hackerNewsStoryPublisher(for: $0) )
.flatMap(maxPublishers: .max(4)) $0
.eraseToAnyPublisher()
请注意,以上将其限制为一次四个,享受并发的性能提升,但限制它,因此您不会冒险让后面的请求超时:
无论如何,这里是第一次获取 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()
(现在,您可以将以上所有内容都塞进一个发布者中,但我喜欢让它们保持小规模,因此每个单独的发布者都很容易推理。)
因此,将所有这些放在一起,您就有了一个像这样的视图模型:
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()
而你的主要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()
【讨论】:
【参考方案3】:通过调用 Fetch().getStories,Fetch
类会立即超出范围并且不会被保留。
我建议将 Fetch
设为 ObservableObject
并将其设置为视图上的属性:
@StateObject var fetcher = Fetch()
然后,调用:
fetcher.getStories
self.stories = stories
如果您想使用它获得更多 SwiftUI 风格,您可能需要查看 ObservableObject
s 上的 @Published
属性,以及如何让您的视图自动响应它们。通过这样做,您可以完全避免在视图上出现@State
变量,不必有回调函数,而只需将故事加载到 ObservableObject 上的@Published
属性中。当@Published
属性更改时,您的视图将重新呈现。更多阅读:https://www.hackingwithswift.com/quick-start/swiftui/observable-objects-environment-objects-and-published
【讨论】:
以上是关于为啥我的 Swift 网络请求不起作用?的主要内容,如果未能解决你的问题,请参考以下文章