【问题标题】: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()

标签: swift swiftui httprequest


【解决方案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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-13
        • 1970-01-01
        • 2022-12-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多