Swift upload file via WCF - swift

I've been fighting this for over 10 hours and done my best before coming here. Many tutorials I've looked out ended up being outdated by a version or two and/or missing some key knowledge in the explanation.
I'm trying to upload a PDF to a server using a WCF Rest Service. When I debug on the WCF service, the variable named document is a null stream. I've searched the web many hours trying many different things and I've exhausted MANY attempts to get such a little thing to work! What is really annoying is that this isn't the first time someone has needed to do this and yet I haven't found an answer anywhere.
If there is a better way to post the PDF other than what I've posted, I'm open to suggestions but I don't want to use third-party frameworks.
Requirements:
- Using the WCF service is required
- I'm not saving to a file system or UNC
I need to get a valid memory stream passed to the service and I can take care of the function code from there. I've tried using a Base64DataString before this and I could get that to work either. If you wanted to provide that as an option, I'm open to it.
Please help!
Swift 4 code:
import PDFKit
class FileUpload {
static func generateBoundaryString() -> String {
return "Boundary-\(UUID().uuidString)"
}
static func dataUploadBodyWithParameters(_ parameters: [String: Any]?, filename: String, mimetype: String, dataKey: String, data: Data, boundary: String) -> Data {
var body = Data()
// encode parameters first
if parameters != nil {
for (key, value) in parameters! {
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.appendString("\(value)\r\n")
}
}
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(dataKey)\"; filename=\"\(filename)\"\r\n")
body.appendString("Content-Type: \(mimetype)\r\n\r\n")
body.append(data)
body.appendString("\r\n")
body.appendString("--\(boundary)--\r\n")
print(body)
return body
}
static func uploadData(_ data: Data, toURL urlString: String, withFileKey fileKey: String, completion: ((_ success: Bool, _ result: Any?) -> Void)?) {
if let url = URL(string: urlString) {
// build request
let boundary = generateBoundaryString()
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// build body
let body = dataUploadBodyWithParameters(nil, filename: "iOSUpload.pdf", mimetype: "application/pdf", dataKey: fileKey, data: data, boundary: boundary)
request.httpBody = body
//UIApplication.shared.isNetworkActivityIndicatorVisible = true
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if data != nil && error == nil {
do {
let result = try JSONSerialization.jsonObject(with: data!, options: [])
print(result)
DispatchQueue.main.async(execute: { completion?(true, result) })
} catch {
DispatchQueue.main.async(execute: { completion?(false, nil) })
}
} else { DispatchQueue.main.async(execute: { completion?(false, nil) }) }
//UIApplication.shared.isNetworkActivityIndicatorVisible = false
}).resume()
} else { DispatchQueue.main.async(execute: { completion?(false, nil) }) }
}
}
extension Data {
mutating func appendString(_ string: String) {
let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true)
append(data!)
}
}
}
WCF Relevant Code:
<ServiceContract>
Public Interface IWebService
<OperationContract>
<WebInvoke(Method:="POST", UriTemplate:="UploadFile/{fileName}")>
Function UploadFile(fileName As String, document As Stream) As String
End Interface
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.Single)>
Public Class MyWebService : Implements IWebService
Public Function UploadFile(fileName As String, document As Stream) As String Implements IWebService.UploadFile
'document is null
End Function

Related

how to pass output of HTTP request Task to variable in Swift

In Chrome browser, I input this address
https://jsonplaceholder.typicode.com/todos/1
Then I see output on Chrome window:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
Now my question is that, can i do this in Swift?
Accessing an http address then get the output?
Now on this webpage, i find the below code.
import SwiftUI
// Create URL
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")
guard let requestUrl = url else { fatalError() }
// Create URL Request
var request = URLRequest(url: requestUrl)
// Specify HTTP Method to use
request.httpMethod = "GET"
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Read HTTP Response Status code
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
}
// Convert HTTP Response Data to a simple String
if let data = data, let dataString = String(data: data, encoding: .utf8) {
print("Response data string:\n \(dataString)")
}
}
task.resume()
I run this code in Playground in XCode. As it says on the webpage I got the right output:
Response data string:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
But it prints on the console of XCode. I cannot find a way to pass this output to a variable. So I cannot go further.
I tried add lines like:
return dataString
all got error.
Or put it in a Func(), then call the function, also got error.
I am not a programmer and very new to Swift, hope people here can help.
You need to decode from JSON and store the result in an object.
Create an object that conforms to Codable and can store the result - it need to have all the properties of the JSON:
struct MyObject: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
Create a function to decode:
func decode(data: Data) -> MyObject? {
do {
let result = try JSONDecoder().decode(MyObject.self, from: data)
return result
} catch {
print("\n-->> Error decoding JSON: \(error), \(error)")
return nil
}
}
Use the function:
if let data = data {
// downloaded will store the result from the JSON
let downloaded = decode(data: data)
print(downloaded) // Optional(Test_Swift.MyObject(userId: 1, id: 1, title: "delectus aut autem", completed: false))
print(downloaded?.title ?? "Could not decode") // delectus aut autem
}

Logging response and request in Moya 14

Is there any way to log my request and response in Moya 14 without using verbose?
container.register(NetworkLoggerPlugin.self) { r in
NetworkLoggerPlugin(verbose: true)
}.inObjectScope(.container)
Thank you in advance.
The initial guidance has been given elsewhere to create a custom plugin for Moya, but here's a working example of a verbose plugin that will display both request and response data.
Add the following code to wherever you are calling Moya from:
struct VerbosePlugin: PluginType {
let verbose: Bool
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
#if DEBUG
if let body = request.httpBody,
let str = String(data: body, encoding: .utf8) {
if verbose {
print("request to send: \(str))")
}
}
#endif
return request
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
#if DEBUG
switch result {
case .success(let body):
if verbose {
print("Response:")
if let json = try? JSONSerialization.jsonObject(with: body.data, options: .mutableContainers) {
print(json)
} else {
let response = String(data: body.data, encoding: .utf8)!
print(response)
}
}
case .failure( _):
break
}
#endif
}
}
In your set up, add the new plugin:
let APIManager = MoyaProvider<API>( plugins: [
VerbosePlugin(verbose: true)
])
This will output both the request being made and the response returned. If the response is JSON encoded, it will pretty-print the JSON, otherwise it will attempt to print out the raw response data.
MoyaProvider(plugins: [NetworkLoggerPlugin()])

Preserving Failure Type with Combine's tryMap

I'm using Combine to write a simple web scraper. I'm trying to map the returned data to a string of HTML, throwing ScraperErrors at each possible failure point. At the end, I want to pass this string to my htmlSubject, which is a PassthroughSubject<String, ScraperError>, for further processing.
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.tryMap { (data, response) -> String in
guard let html = String(data: data, encoding: .utf8) else {
throw ScraperError.readFailed
}
return html
}
.subscribe(htmlSubject) // <-- Not allowed because failure type is now Error
However, I'm finding that .tryMap is erasing my ScraperError to a regular Error, preventing me from chaining my htmlSubject to the end:
Instance method 'subscribe' requires the types 'Error' and
'ScraperError' be equivalent.
Is there an obvious way around this that I'm missing, or am I getting tripped up conceptually? I'm thinking of this chain as building blocks in a large function that maps <(Data, URLResponse), URLError> to <String, ScraperError>.
Any help is appreciated.
Use mapError to convert back to ScraperError after the tryMap:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.tryMap { (data, response) -> String in
guard let html = String(data: data, encoding: .utf8) else {
throw ScraperError.readFailed
}
return html
}
.mapError { $0 as! ScraperError }
.subscribe(htmlSubject)
If you don't want to use as!, you'll have to pick some other case to map to:
.mapError { $0 as? ScraperError ?? ScraperError.unknown }
If you don't like that either, you can use flatMap over Result<String, ScraperError>.Publisher:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.flatMap { (data, response) -> Result<String, ScraperError>.Publisher in
guard let html = String(data: data, encoding: .utf8) else {
return .init(.readFailed)
}
return .init(html)
}
.subscribe(htmlSubject)
I find the resulting code to be a bit more readable when wrapping Rob's flatMap approach into an extension:
extension Publisher {
func flatMapResult<T>(_ transform: #escaping (Self.Output) -> Result<T, Self.Failure>) -> Publishers.FlatMap<Result<T, Self.Failure>.Publisher, Self> {
self.flatMap { .init(transform($0)) }
}
}
The code example above would then become:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.flatMapResult { (data, response) -> Result<String, ScraperError> in
guard let html = String(data: data, encoding: .utf8) else {
return .failure(.readFailed)
}
return .success(html)
}
.subscribe(htmlSubject)

POST binary data using swift

I would like to post binary data through RxAlamofire, Alamofire or even without any library but after some days of research and tries, I'm not able to do it.
Here you can find the POSTMAN example of the request that I am trying to reproduce is:
Is a post method with the Authorization and Content-Type headers and the binary data attached.
I have tried to find some example or something related but I couldn't find a solution. I could just find multipart form data examples but with multipart form data the server doesn't work (is a external API)
If someone could guide me or show me some example code.
Here the code used for login as example and to show you something that I want to achieve:
public class APIClient: DataSource {
public static var shared: APIClient = APIClient()
private init(){}
public func login(email:String, password:String) -> Observable<LoginResponse> {
return RxAlamofire.requestJSON(APIRouter.login(email:email, password:password))
.subscribeOn(MainScheduler.asyncInstance)
.debug()
.mapObject(type: LoginResponse.self)
}
}
Here the LoginResponse object:
public struct LoginResponse: Mappable {
var tokenId: String?
var userId: String?
public init?(map: Map) {}
public mutating func mapping(map: Map) {
tokenId <- map["id"]
userId <- map["userId"]
}
}
And finally the APIRouter extending URLRequestConvertible:
enum APIRouter: URLRequestConvertible {
case login(email: String, password: String)
private var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
private var path: String {
switch self {
case .login:
return "users/login"
}
}
private var parameters: Parameters? {
switch self {
case .login(let email, let password):
return [APIConstants.LoginParameterKey.email: email, APIConstants.LoginParameterKey.password: password]
}
}
private var query: [URLQueryItem]? {
var queryItems = [URLQueryItem]()
switch self {
case .login:
return nil
}
}
func asURLRequest() throws -> URLRequest {
var urlComponents = URLComponents(string: APIConstants.ProductionServer.baseURL)!
if let query = query {
urlComponents.queryItems = query
}
var urlRequest = URLRequest(url: urlComponents.url!.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}
Thank you in advance!
EDIT To convert into RxAlamofire
With the code below I could solve the problem and convert it into RxSwift but I would like to use RxAlamofire to obtain the same result:
public func upload(media: Data) -> Observable<ContentUri> {
let headers = [
"content-type": "image/png",
"authorization": "token header"
]
return Observable<ContentUri>.create({observer in
Alamofire.upload(media, to: "\(endPoint)/api/media/upload", headers: headers)
.validate()
.responseJSON { response in
print(response)
}
return Disposables.create();
})
}
Alamofire.upload() (which returns an UploadRequest) might do what you want:
let headers = [
"Content-Type":"image/jpeg",
"Authorization":"sometoken",
]
let yourData = ... // Data of your image you want to upload
let endPoint = ...
Alamofire.upload(yourData, to: "\(endPoint)/api/media/upload", headers: headers)
.validate(statusCode: 200..<300)
.responseJSON { response in
// handle response
}
This example does not include RxAlamofire - but I am pretty sure it has a similar upload function. I hope it helps!

Generic function with Alamofire

I work with iOS app that use Alamofire, I want to write a generic function(s) which used to send and retrieve data from server to a decodable objects, my function was as below :
func pop <T : Codable> (_ Url: inout String, _ popedList: inout [T]) {
let url = URL(string:Url)
Alamofire.request(url!, method: .post).responseJSON { response in
let result = response.data
do {
let data = try JSONDecoder().decode(popedList, from: result!)// get error here
print(data[0])
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(data[0])
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
} catch let e as NSError {
print("error : \(e)")
}
}
}
and a function to send an object to server as below:
func push <T : Codable> (_ Url: inout String, _ pushObject: inout T) {
let jsonData = try! JSONEncoder().encode(pushObject)
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
let url = URL(string:Url)
Alamofire.request(url!,
method: .post,
parameters:jsonString)//it's need to creat a Dictionary instate of String
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.response { response in
// response handling code
let result = response.data
print(response.data)
}
}
I get an error in first function,
"Cannot invoke 'decode' with an argument list of type '([T], from: Data)'"
and
"Escaping closures can only capture inout parameters explicitly by value"
What is the best way to write these to function as generic?
After a few searches and trying to edit my functions I capable to rewrite my two functions in such away that I get what I need:
func pop<T: Decodable>(from: URL, decodable: T.Type, completion:#escaping (_ details: [T]) -> Void)
{
Alamofire.request(from, method: .post).responseJSON { response in
let result_ = response.data
do {
let data = try JSONDecoder().decode([T].self, from: result_!)
//let data = try JSONDecoder().decode(decodable, from: result_!)// get error here
//print(data[0])
print("data[0] : \(data[0])")
completion(data)
} catch let e as NSError {
print("error : \(e)")
}
}
}
func push <T : Codable> (_ Url: String, _ pushObject: T)
{
let jsonData = try! JSONEncoder().encode(pushObject)
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
let url = URL(string:Url)
Alamofire.request(url!,
method: .post,
parameters:convertToDictionary(text: jsonString!))//it's need to creat a Dictionary instate of String
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.response { response in
// response handling code
print(response.data!)
if let jsonData = response.data {
let jsonString = String(data: jsonData, encoding: .utf8)
print("response.data: \(String(describing: jsonString))")
}
}
}
func convertToDictionary(text: String) -> [String: Any]? {
if let data = text.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
print(error.localizedDescription)
}
}
return nil
}
For the first function, JSONDecoder.decode() wants 2 parameters:
Type to decode to: the class/struct you want it to decode to. This is not an instantiated object, just the type.
Data to decode from: the generic Data object that will be converted to the type you specified.
So, in order to be able to write your function so that it has a generic URL and result object, you would need to pass it the object type and a callback to pass the result to, since network operations are asynchronous.
func dec<T: Decodable>(from: URL, decodable: T.Type, result: (T) -> Void) {
// your Alamofire logic
let data = try JSONDecoder().decode(popedList, from: result!)
result(data)
}
You can apply the same logic to the second function.
Note that this is not the best way to take care of eventual errors, just an example of how you can handle encoding with a generic function.
JSONDecoder().decode method takes type and data parameter. Pass type not popedList.
let data = try JSONDecoder().decode([T].self, from: result!)
Inout Paramaters
Function parameters are constants by default. Trying to change the value of a function parameter from within the body of that function results in a compile-time error. This means that you can’t change the value of a parameter by mistake. If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.
You are not changing value of popedList in both functions, so using inout is meaningless.