无法使用结合 SwiftUI 从 URL 获取响应

Posted

技术标签:

【中文标题】无法使用结合 SwiftUI 从 URL 获取响应【英文标题】:Unable to get the response from URL using combine with SwiftUI 【发布时间】:2021-02-04 10:16:54 【问题描述】:

那是我的模型课

struct LoginResponse: Codable 
    let main: LoginModel


struct LoginModel: Codable 
    
    let success: Bool?
    let token: String?
    let message: String?
    
    static var placeholder: LoginModel 
        return LoginModel(success: nil, token: nil, message: nil)
    
    

那是我的服务。我还有一个问题,我在这里使用了两个地图,但是当尝试删除 map.data 在 dataTaskPublisher 中出现错误时。错误提示如下

实例方法“decode(type:decoder:)”需要类型“URLSession.DataTaskPublisher.Output”(又名“(数据:数据,响应:URLResponse)”)和“JSONDecoder.Input”(又名“数据”)等价

class LoginService 
    func doLoginTask(username: String, password: String) -> AnyPublisher<LoginModel, Error> 
        
       
        
      let networkQueue = DispatchQueue(label: "Networking",
                                           qos: .default,
                                           attributes: .concurrent)
        
        guard let url = URL(string: Constants.URLs.baseUrl(urlPath: Constants.URLs.loginPath)) else 
            fatalError("Invalid URL")
         
        
        print("uri", url)
        
        let body: [String: String] = ["username": username, "password": password]

                let finalBody = try! JSONSerialization.data(withJSONObject: body)
                var request = URLRequest(url: url)
                request.httpMethod = "POST"
                request.httpBody = finalBody
                request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        return URLSession.shared.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: LoginResponse.self, decoder: JSONDecoder())
            .map  $0.main 
            .receive(on: networkQueue)
            .eraseToAnyPublisher()
        
    
    

那是我的内容视图

Button(action: 
                    self.counter += 1
                    print("count from action", self.counter)
                    
                
                    
                    func loaginTask() 
                        _ = loginService.doLoginTask(username: "1234567890", password: "12345")
                        .sink(
                          receiveCompletion: 
                            print("Received Completion: \($0)") ,
                          receiveValue:  doctor in
                            print("hhhhh")
                          //  print("yes ", doctor.message as Any)
                            
                          
                      )
                    
                )

这是我的 json 响应


    "success": true,
    "token": "ed48aa9b40c2d88079e6fd140c87ac61fc9ce78a",
    "expert-token": "6ec84e92ea93b793924d48aa9b40c2d88079e6fd140c87ac61fc9ce78ae4fa93",
    "message": "Logged in successfully"

【问题讨论】:

【参考方案1】:

首先,您的错误来自您想要返回 AnyPublisher&lt;LoginModel, Error&gt; 但您将响应映射为 .decode(type: LoginResponse.self, decoder: JSONDecoder()) 这与您的 json 响应不匹配。

在第二次中,我将使用基本授权作为 URL 请求的主体,因为它用于发送带有密码的用户凭据,该密码必须受到保护。您可以访问服务器端吗?后端如何处理这个 post 请求? 是授权还是内容类型?我会把这两个解决方案,尝试找到在服务器端设置的那个。

您的 LoginModel 必须与您的 json 响应相匹配。我注意到他们缺少了expertToken:

struct LoginModel: Codable 

  let success: Bool
  let token: String
  let expertToken: String
  let message: String

  enum CodingKeys: String, CodingKey 
    case success
    case token
    case expertToken = "expert-token"
    case message
  

所以我会以这种方式创建LoginService 类:

final class LoginService 

  /// The request your use when the button is pressed.
  func logIn(username: String, password: String) -> AnyPublisher<LoginModel, Error> 

    let url = URL(string: "http://your.api.endpoints/")!
    let body = logInBody(username: username, password: password)
    let urlRequest = basicAuthRequestSetup(url: url, body: body)

    return URLSession.shared
      .dataTaskPublisher(for: urlRequest)
      .receive(on: DispatchQueue.main)
      .tryMap  try self.validate($0.data, $0.response) 
      .decode(
        type: LoginModel.self,
        decoder: JSONDecoder())
      .eraseToAnyPublisher()
  

  /// The body for a basic authorization with encoded credentials.
  func logInBody(username: String, password: String) -> String 

    let body = String(format: "%@:%@",
                      username,
                      password)

    guard let bodyData = body.data(using: .utf8) else  return String() 

    let encodedBody = bodyData.base64EncodedString()
    return encodedBody
  

  /// The authorization setup
  func basicAuthRequestSetup(url: URL, body: String) -> URLRequest 

    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = "POST"
    urlRequest.setValue("Basic \(body)",
                        forHTTPHeaderField: "Authorization")

    return urlRequest
  

  /// Validation of the Data and the response.
  /// You can handle response with status code for more precision.
  func validate(_ data: Data, _ response: URLResponse) throws -> Data 
    guard let httpResponse = response as? HTTPURLResponse else 
      throw NetworkError.unknown
    
    guard (200..<300).contains(httpResponse.statusCode) else 
      throw networkRequestError(from: httpResponse.statusCode)
    
    return data
  

  /// Handle the status code errors to populate to user.
  func networkRequestError(from statusCode: Int) -> Error 
    switch statusCode 
    case 401:
      return NetworkError.unauthorized
    default:
      return NetworkError.unknown
    
  

  /// Define your different Error here that can come back from
  /// your backend.
  enum NetworkError: Error, Equatable 

    case unauthorized
    case unknown
  

所以如果你使用一个简单的 Content-Type,你的 body 就是下面这个。从上面的代码替换logInBody(username:password:) -&gt; StringbasicAuthRequestSetup(url:body:) -&gt; URLRequest

/// Classic body for content type.
/// Keys must match the one in your server side.
func contentTypeBody(username: String, password: String) -> [String: Any] 
  [
    "username": username,
    "password": password
  ] as [String: Any]


/// Classic Content-Type but not secure. To avoid when having
/// passwords.
func contentTypeRequestSetup(url: URL,
                      body: [String: Any]) -> URLRequest 

  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "POST"
  urlRequest.setValue("application/json",
                      forHTTPHeaderField: "Content-Type")
  urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)

  return urlRequest

然后我将创建一个 ViewModel 来处理将在您的视图中传递的逻辑。

final class OnboardingViewModel: ObservableObject 

  var logInService = LoginService()

  var subscriptions = Set<AnyCancellable>()

  func logIn() 
    logInService.logIn(username: "Shubhank", password: "1234")
      .sink(receiveCompletion:  completion in
              print(completion) ,
            receiveValue:  data in
              print(data.expertToken) )  // This is your response
      .store(in: &subscriptions)
  

现在,在您的 ContentView 中,您可以在按钮内传递视图模型登录操作:

struct ContentView: View 

  @ObservedObject var viewModel = OnboardingViewModel()

  var body: some View 
    Button(action:  viewModel.logIn() ) 
      Text("Log In")
    
  

【讨论】:

非常感谢兄弟,这是工作...再次感谢您节省了我的一天。【参考方案2】:

由于取消,您的发布者在调用上下文下方被销毁,因为您没有保留对订阅者的引用。

要解决此问题,您必须在某处保留对订阅者的引用。最合适的变体在某些成员属性中,但是,作为变体,它也可以是自包含的(如果符合您的目标),例如

func loaginTask() 
    var subscriber: AnyCancellable?
    subscriber = loginService.doLoginTask(username: "1234567890", password: "12345")
    .sink(
      receiveCompletion:  [subscriber] result in
        print("Received Completion: \(result)") 
        subscriber = nil                     // << keeps until completed
      ,
      receiveValue:  doctor in
        print("hhhhh")
      //  print("yes ", doctor.message as Any)
        
      
  )

【讨论】:

很抱歉打扰您,但我还是做不到

以上是关于无法使用结合 SwiftUI 从 URL 获取响应的主要内容,如果未能解决你的问题,请参考以下文章

swiftui combine 无法获取数据 [关闭]

SwiftUI - 结合和 Alamofire 解析错误响应

从 SwiftUI 中的 URL 获取 JSON

结合 SwiftUI 远程获取数据 - ObjectBinding 不更新视图

如何使用 LinkPresentation 在 SwiftUI 中获取链接元数据?

无法使用 SwiftUI 中的组合框架从 Spotify API 获取数据