问题
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