Unable to get the response from URL using combine with SwiftUI

家住魔仙堡 提交于 2021-02-10 17:56:15

问题


Thats my model class

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)
    }
    
}

Thats my service. I have one more issue i am using two map here but when try to remove map.data getting error in dataTaskPublisher. error mention below

Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent

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()
        
    }
    
}

Thats my contentView

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)
                            
                          }
                      )
                    }
                })

Thats my json response

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

回答1:


First of all, your error comes from the fact you want to return AnyPublisher<LoginModel, Error> but you map your response as .decode(type: LoginResponse.self, decoder: JSONDecoder()) which doesn't match your json response.

In the second time, I would use a Basic Authorization as a body of your URL request as it is to send user credentials with a password, which must be protected. Do you have access to the server side? How is the backend handling this post request? Is it with Authorization or Content-Type? I would put the two solutions, try to find the one that is set in the server side.

Your LoginModel must match your json response. I noticed their was expertToken missing:

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
  }
}

So I would create the LoginService class this way:

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
  }
}

So if you use a simple Content-Type, your body would be this one below. Replace from the code above logInBody(username:password:) -> String and basicAuthRequestSetup(url:body:) -> 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
}

I would then create a ViewModel to handle the logic that will be passed in your View.

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)
  }
}

And now, in your ContentView, you can pass the view model login action inside the button:

struct ContentView: View {

  @ObservedObject var viewModel = OnboardingViewModel()

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



回答2:


Your publisher is destroyed right below context of call due to cancellation, because you don't keep reference to subscriber.

To fix this you have to keep somewhere the reference to subscriber. Most appropriate variant is in some member property, but, as a variant, it can also be self-contained (if fits your goal), like

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)
        
      }
  )
}


来源:https://stackoverflow.com/questions/66043573/unable-to-get-the-response-from-url-using-combine-with-swiftui

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!