为啥我的 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 风格,您可能需要查看 ObservableObjects 上的 @Published 属性,以及如何让您的视图自动响应它们。通过这样做,您可以完全避免在视图上出现@State 变量,不必有回调函​​数,而只需将故事加载到 ObservableObject 上的@Published 属性中。当@Published 属性更改时,您的视图将重新呈现。更多阅读:https://www.hackingwithswift.com/quick-start/swiftui/observable-objects-environment-objects-and-published

【讨论】:

以上是关于为啥我的 Swift 网络请求不起作用?的主要内容,如果未能解决你的问题,请参考以下文章

在会话中发出后续 POST 请求不起作用 - 网络抓取

为啥我的 Mongoose 填充请求不起作用?

为啥我的 POST 请求仅对我的 API 不起作用?

为啥我的 cURL 请求在 XAMPP 中不起作用?

带有 NSURLSession 的 Swift 中的 HTTP 请求

为啥我的 html 请求在 javascript 中不起作用 [重复]