Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Request Subclasses and New Progress APIs #1455

Merged
merged 12 commits into from Sep 6, 2016

Conversation

cnoon
Copy link
Member

@cnoon cnoon commented Sep 5, 2016

This PR splits up the Request class into DataRequest, DownloadRequest, UploadRequest and StreamRequest subclasses.

Motivation

There were several reasons why the Request class could greatly benefit from being split up.

Progress

The first reason is that the progress APIs were stomping on each other and were confusing the users. For example, the upload progress behavior was very ambiguous considering that the same Progress instance was used to report both upload progress and then download progress after the upload was complete.

Session Manager Duplication

The feature/adapter-and-retrier PR added the TaskConvertible enumeration to the Request class. Unfortunately, there is a large amount of duplication between the TaskConvertible implementation and the Downloadable, Uploadable and Streamable enumerations inside the SessionManager. Not only is there duplication, but the Downloadable, Uploadable and Streamable enumerations don't really belong in the SessionManager anyways. They're really the pieces required to construct a concrete Request.

By splitting up the Request into subclasses, we could move the Downloadable, Uploadable and Streamable implementations into their respective subclass and eliminate the duplication with the TaskConvertible enumeration in the Request class.

In the previous PR where the TaskConvertible enumeration was added, the Downloadable, Uploadable and Streamable enumerations

Response Serializers

Another awesome reason for splitting up the Request class is to associate response serializers with a particular request type. For example, right now you can chain a responseData closure onto a Request that is backed by a URLSessionDownloadTask or a ULRSessionStreamTask. However, if you do this, what's the behavior? It currently gets really weird in all these cases when you start to consider resume data.

Also, how does one get the destination URL of a download? Great question, right now it is WAY more difficult than it should be. By splitting up the Request, we can extend only certain subclasses to support specific response serializers that are tailored to the underlying type of task. These changes will be coming in a future PR.

Solution

So now that we have some background as to why these changes would be useful, let's dig into what had to change to make this work.

TaskConvertible

By pulling the Downloadable, Uploadable and Streamable enumerations into their respective subclasses and adding a Requestable enumeration to the DataRequest subclass, I was able to get rid of the TaskConvertible enumeration and make it a protocol. The protocol now creates a URLSessionTask from a session, adapter and queue. This allows the subclassed enumerations to construct the tasks directly without having to do this in the SessionManager. This greatly simplifies the SessionManager.retry method.

Progress

The progress APIs have been completely rebuilt from scratch. You can now monitor downloadProgress on data, download and upload requests. You can also monitor uploadProgress on an upload request. There are variants for both the Progress object and the broken out Int64 values. Each variant can also call the progress closure on a specified queue.

Here's a quick example of how you can use the new APIs.

Alamofire.upload(data, to: urlString, withMethod: .post)
    .uploadProgress { progress in
        // Called on main dispatch queue by default
        print("upload progress: \(progress.fractionCompleted)")
    }
    .uploadProgress(queue: DispatchQueue.utility) { bytesSent, totalBytesSent, totalBytesExpectedToSend in
        // Can customize the dispatch queue for background operations if needed
        print("Sent: \(bytesSent), Total Sent: \(totalBytesSent), Total Expected: \(totalBytesExpectedToSend)")
    }
    .downloadProgress { progress in
        // Called on main dispatch queue by default
        print("download progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
        // Called on main dispatch queue by default
        print("Read: \(bytesRead), Total Read: \(totalBytesRead), Total Expected: \(totalBytesExpectedToRead)")
    }
    .response { response in
        debugPrint(response)
    }

Download Progress

The download progress is now being tracked in the DataTaskDelegate. This allows the data, download and upload requests to leverage it. I debated for a long time as to whether to call this progress or downloadProgress. I would rather error on the side of verbosity here I think. I ultimately decided upon downloadProgress because progress is REALLY ambiguous when it comes to an upload request.

If anyone strongly disagrees here, please voice your concerns. I'm still on the fence on this one, but right now lean towards downloadProgress and uploadProgress APIs to completely avoid ambiguity.

Upload Progress

The upload progress is all managed by the UploadTaskDelegate and is only exposed publicly through the upload request subclass. The stream request cannot chain downloadProgress or uploadProgress APIs because it no longer inherits them.

Top-Level Stream APIs

I added the top-level stream APIs to Alamofire.swift because they were missing. Just wanted to callout that this random change made its way into this PR as well.


Summary

These changes vastly improve the progress APIs and open up the ability to greatly specialize the available chained methods for each subclass. This will help avoid confusion and will allow for better customization for each request type.

@cnoon
Copy link
Member Author

cnoon commented Sep 5, 2016

cc @jshier and @kcharwood for review.

open static func authorizationHeaderFrom(user: String, password: String) -> [String: String] {
guard let data = "\(user):\(password)".data(using: String.Encoding.utf8) else { return [:] }
/// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
open static func authorizationHeaderFrom(user: String, password: String) -> (key: String, value: String)? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a part of this PR, but I think the naming here could be better. Perhaps authorizationHeader(withUser:password).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really want to get rid of the with altogether here. Any factor type methods seem like they don't need it. For example, the expectation(withDescription:) API in XCTest is now expectation(description:). Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guidelines don't offer much guidance in this case, except to say:

When the first argument forms part of a prepositional phrase, give it an argument label. The argument label should normally begin at the preposition, e.g. x.removeBoxes(havingLength: 12).

I believe with is the prepostion here. That said, I'm okay with dropping it in most cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004b315.

@jshier
Copy link
Contributor

jshier commented Sep 6, 2016

@cnoon Review complete. Not much feedback here, looks great!

@cnoon cnoon changed the base branch from feature/adapter-and-retrier-system to swift3 September 6, 2016 04:00
… tuple.

In the case that utf8 encoding fails, this method should return `nil`, not an empty dictionary. It’s also strange to use a dictionary to return a single key-value pair in the success case. By switching to an optional tuple, the caller must be more explicit about handling the `nil` case making the call safer.
…uracy.

There were several issues with the previous implementation. The main problem was the upload tasks uses the same `Progress` property for the upload and download portions of the request. This could cause all sorts of strange progress reporting behavior. Additionally, there were no APIs that returned the `Progress` object directly and you couldn’t specify what queue the progress closures should be called on.

This commit addresses all these limitations. The `downloadProgress` APIs are available on the data, download and upload requests. The `uploadProgress` APIs are only available on upload requests.
@cnoon cnoon force-pushed the feature/request-subclasses-and-progress branch from db619cc to 97658eb Compare September 6, 2016 04:01
@cnoon
Copy link
Member Author

cnoon commented Sep 6, 2016

All changes have been addressed @jshier. I'm going to merge this bad boy.

@davewang
Copy link

davewang commented Jul 3, 2017

I download file url is http://10.7.7.100:8080/auth0919.tar.gz progress always 0.0,BTW Other url(not .tar.gz ) is ok

@gnik2036
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants