Skip to content

Commit 1e2e3f7

Browse files
Copilotfarfromrefug
andcommitted
Implement cleaner API names and streaming downloads for iOS
Agent-Logs-Url: https://github.com/nativescript-community/https/sessions/99200ca0-c02e-437d-bbdc-d3c5f6f221c3 Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
1 parent 814d31f commit 1e2e3f7

File tree

4 files changed

+159
-15
lines changed

4 files changed

+159
-15
lines changed

packages/https/platforms/ios/src/AlamofireWrapper.swift

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public class AlamofireWrapper: NSObject {
6060

6161
// MARK: - Request Methods
6262

63-
@objc public func dataTaskWithHTTPMethodURLStringParametersHeadersUploadProgressDownloadProgressSuccessFailure(
63+
// Clean API: New shorter method name
64+
@objc public func request(
6465
_ method: String,
6566
_ urlString: String,
6667
_ parameters: NSDictionary?,
@@ -149,9 +150,9 @@ public class AlamofireWrapper: NSObject {
149150

150151
// MARK: - Multipart Form Data
151152

152-
@objc public func POSTParametersHeadersConstructingBodyWithBlockProgressSuccessFailure(
153+
// Clean API: New shorter method name for multipart upload
154+
@objc public func uploadMultipart(
153155
_ urlString: String,
154-
_ parameters: NSDictionary?,
155156
_ headers: NSDictionary?,
156157
_ constructingBodyWithBlock: @escaping (MultipartFormDataWrapper) -> Void,
157158
_ progress: ((Progress) -> Void)?,
@@ -233,7 +234,8 @@ public class AlamofireWrapper: NSObject {
233234

234235
// MARK: - Upload Tasks
235236

236-
@objc public func uploadTaskWithRequestFromFileProgressCompletionHandler(
237+
// Clean API: Upload file
238+
@objc public func uploadFile(
237239
_ request: URLRequest,
238240
_ fileURL: URL,
239241
_ progress: ((Progress) -> Void)?,
@@ -280,7 +282,8 @@ public class AlamofireWrapper: NSObject {
280282
return afRequest.task
281283
}
282284

283-
@objc public func uploadTaskWithRequestFromDataProgressCompletionHandler(
285+
// Clean API: Upload data
286+
@objc public func uploadData(
284287
_ request: URLRequest,
285288
_ bodyData: Data,
286289
_ progress: ((Progress) -> Void)?,
@@ -327,6 +330,82 @@ public class AlamofireWrapper: NSObject {
327330
return afRequest.task
328331
}
329332

333+
// MARK: - Download Tasks
334+
335+
// Clean API: Download file with streaming to disk (optimized, no memory loading)
336+
@objc public func downloadToFile(
337+
_ urlString: String,
338+
_ destinationPath: String,
339+
_ headers: NSDictionary?,
340+
_ progress: ((Progress) -> Void)?,
341+
_ completionHandler: @escaping (URLResponse?, String?, Error?) -> Void
342+
) -> URLSessionDownloadTask? {
343+
344+
guard let url = URL(string: urlString) else {
345+
let error = NSError(domain: "AlamofireWrapper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
346+
completionHandler(nil, nil, error)
347+
return nil
348+
}
349+
350+
var request: URLRequest
351+
do {
352+
request = try requestSerializer.createRequest(
353+
url: url,
354+
method: .get,
355+
parameters: nil,
356+
headers: headers
357+
)
358+
} catch {
359+
completionHandler(nil, nil, error)
360+
return nil
361+
}
362+
363+
// Create destination closure that moves file to the specified path
364+
let destination: DownloadRequest.Destination = { temporaryURL, response in
365+
let destinationURL = URL(fileURLWithPath: destinationPath)
366+
367+
// Ensure parent directory exists
368+
let parentDirectory = destinationURL.deletingLastPathComponent()
369+
try? FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
370+
371+
return (destinationURL, [.removePreviousFile, .createIntermediateDirectories])
372+
}
373+
374+
var downloadRequest = session.download(request, to: destination)
375+
376+
// Apply server trust evaluation if security policy is set
377+
if let secPolicy = securityPolicy, let host = url.host {
378+
downloadRequest = downloadRequest.validate { _, response, _ in
379+
do {
380+
try secPolicy.evaluate(response.serverTrust!, forHost: host)
381+
return .success(Void())
382+
} catch {
383+
return .failure(error)
384+
}
385+
}
386+
}
387+
388+
// Download progress
389+
if let progress = progress {
390+
downloadRequest = downloadRequest.downloadProgress { progressInfo in
391+
progress(progressInfo)
392+
}
393+
}
394+
395+
// Response handling
396+
downloadRequest.response(queue: .main) { response in
397+
if let error = response.error {
398+
completionHandler(response.response, nil, error)
399+
return
400+
}
401+
402+
// Return the destination path on success
403+
completionHandler(response.response, destinationPath, nil)
404+
}
405+
406+
return downloadRequest.task as? URLSessionDownloadTask
407+
}
408+
330409
// MARK: - Helper Methods
331410

332411
private func createNSError(from error: Error, response: HTTPURLResponse?, data: Data?) -> NSError {

src/https/request.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export interface HttpsRequestOptions extends HttpRequestOptions {
7171
* default to true. Android and iOS only store cookies in memory! it will be cleared after an app restart
7272
*/
7373
cookiesEnabled?: boolean;
74+
75+
/**
76+
* iOS: When set, downloads will be streamed directly to the specified file path without loading into memory.
77+
* This is more memory efficient for large files.
78+
*/
79+
downloadFilePath?: string;
7480
}
7581

7682
export interface HttpsResponse<T = any> {

src/https/request.ios.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,58 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
413413
},
414414
cancel: () => task && task.cancel(),
415415
run(resolve, reject) {
416+
// Handle streaming download if downloadFilePath is specified
417+
if (opts.downloadFilePath && opts.method === 'GET') {
418+
const downloadTask = manager.downloadToFile(
419+
opts.url,
420+
opts.downloadFilePath,
421+
headers,
422+
progress,
423+
(response: NSURLResponse, filePath: string, error: NSError) => {
424+
clearRunningRequest();
425+
if (error) {
426+
reject(error);
427+
return;
428+
}
429+
430+
const httpResponse = response as NSHTTPURLResponse;
431+
const contentLength = httpResponse?.expectedContentLength || 0;
432+
433+
// Create a File object pointing to the downloaded file
434+
const file = File.fromPath(filePath);
435+
436+
let getHeaders = () => ({});
437+
const sendi = {
438+
content: useLegacy ? { toFile: () => Promise.resolve(file) } : filePath,
439+
contentLength,
440+
get headers() {
441+
return getHeaders();
442+
}
443+
} as any as HttpsResponse;
444+
445+
if (!Utils.isNullOrUndefined(httpResponse)) {
446+
sendi.statusCode = httpResponse.statusCode;
447+
getHeaders = function () {
448+
const dict = httpResponse.allHeaderFields;
449+
if (dict) {
450+
const headers = {};
451+
dict.enumerateKeysAndObjectsUsingBlock((k, v) => (headers[k] = v));
452+
return headers;
453+
}
454+
return null;
455+
};
456+
}
457+
resolve(sendi);
458+
}
459+
);
460+
461+
task = downloadTask as any;
462+
if (task && tag) {
463+
runningRequests[tag] = task;
464+
}
465+
return;
466+
}
467+
416468
const success = function (task: NSURLSessionDataTask, data?: any) {
417469
clearRunningRequest();
418470
// TODO: refactor this code with failure one.
@@ -455,9 +507,8 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
455507
case 'POST':
456508
// we need to remove the Content-Type or the boundary wont be set correctly
457509
headers.removeObjectForKey('Content-Type');
458-
task = manager.POSTParametersHeadersConstructingBodyWithBlockProgressSuccessFailure(
510+
task = manager.uploadMultipart(
459511
opts.url,
460-
null,
461512
headers,
462513
(formData) => {
463514
(opts.body as HttpsFormDataParam[]).forEach((param) => {
@@ -502,7 +553,7 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
502553
Object.keys(heads).forEach((k) => {
503554
request.setValueForHTTPHeaderField(heads[k], k);
504555
});
505-
task = manager.uploadTaskWithRequestFromFileProgressCompletionHandler(
556+
task = manager.uploadFile(
506557
request,
507558
NSURL.fileURLWithPath(opts.body.path),
508559
progress,
@@ -530,7 +581,7 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
530581
Object.keys(heads).forEach((k) => {
531582
request.setValueForHTTPHeaderField(heads[k], k);
532583
});
533-
task = manager.uploadTaskWithRequestFromDataProgressCompletionHandler(request, data, progress, (response: NSURLResponse, responseObject: any, error: NSError) => {
584+
task = manager.uploadData(request, data, progress, (response: NSURLResponse, responseObject: any, error: NSError) => {
534585
if (error) {
535586
failure(task, error);
536587
} else {
@@ -550,7 +601,7 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
550601
} else if (typeof opts.content === 'string') {
551602
dict = NSJSONSerialization.JSONObjectWithDataOptionsError(NSString.stringWithString(opts.content).dataUsingEncoding(NSUTF8StringEncoding), 0 as any);
552603
}
553-
task = manager.dataTaskWithHTTPMethodURLStringParametersHeadersUploadProgressDownloadProgressSuccessFailure(opts.method, opts.url, dict, headers, progress, progress, success, failure);
604+
task = manager.request(opts.method, opts.url, dict, headers, progress, progress, success, failure);
554605
task.resume();
555606
}
556607
if (task && tag) {

src/https/typings/objc!AlamofireWrapper.d.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ declare class AlamofireWrapper extends NSObject {
1212

1313
setDataTaskWillCacheResponseBlock(block: (session: NSURLSession, task: NSURLSessionDataTask, cacheResponse: NSCachedURLResponse) => NSCachedURLResponse): void;
1414

15-
dataTaskWithHTTPMethodURLStringParametersHeadersUploadProgressDownloadProgressSuccessFailure(
15+
// New clean API methods
16+
request(
1617
method: string,
1718
urlString: string,
1819
parameters: NSDictionary<string, any>,
@@ -23,29 +24,36 @@ declare class AlamofireWrapper extends NSObject {
2324
failure: (task: NSURLSessionDataTask, error: NSError) => void
2425
): NSURLSessionDataTask;
2526

26-
POSTParametersHeadersConstructingBodyWithBlockProgressSuccessFailure(
27+
uploadMultipart(
2728
urlString: string,
28-
parameters: NSDictionary<string, any>,
2929
headers: NSDictionary<string, any>,
3030
constructingBodyWithBlock: (formData: MultipartFormDataWrapper) => void,
3131
progress: (progress: NSProgress) => void,
3232
success: (task: NSURLSessionDataTask, data: any) => void,
3333
failure: (task: NSURLSessionDataTask, error: NSError) => void
3434
): NSURLSessionDataTask;
3535

36-
uploadTaskWithRequestFromFileProgressCompletionHandler(
36+
uploadFile(
3737
request: NSMutableURLRequest,
3838
fileURL: NSURL,
3939
progress: (progress: NSProgress) => void,
4040
completionHandler: (response: NSURLResponse, responseObject: any, error: NSError) => void
4141
): NSURLSessionDataTask;
4242

43-
uploadTaskWithRequestFromDataProgressCompletionHandler(
43+
uploadData(
4444
request: NSMutableURLRequest,
4545
bodyData: NSData,
4646
progress: (progress: NSProgress) => void,
4747
completionHandler: (response: NSURLResponse, responseObject: any, error: NSError) => void
4848
): NSURLSessionDataTask;
49+
50+
downloadToFile(
51+
urlString: string,
52+
destinationPath: string,
53+
headers: NSDictionary<string, any>,
54+
progress: (progress: NSProgress) => void,
55+
completionHandler: (response: NSURLResponse, filePath: string, error: NSError) => void
56+
): NSURLSessionDownloadTask;
4957
}
5058

5159
declare class RequestSerializer extends NSObject {

0 commit comments

Comments
 (0)