A easy picture downloader
Downloading a useful resource from an URL looks like a trivial activity, however is it actually that simple? Properly, it relies upon. If it’s a must to obtain and parse a JSON file which is only a few KB, then you possibly can go along with the classical means or you need to use the brand new dataTaskPublisher
methodology on the URLSession object from the Mix framework.
Unhealthy practices ⚠️
There are some fast & soiled approaches that you need to use to get some smaller knowledge from the web. The issue with these strategies is that it’s a must to deal so much with threads and queues. Luckily utilizing the Dispatch framework helps so much, so you possibly can flip your blocking capabilities into non-blocking ones. 🚧
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
do {
let content material = strive String(contentsOf: url)
print(content material)
let knowledge = strive Knowledge(contentsOf: url)
}
catch {
print(error.localizedDescription)
}
DispatchQueue.international().async { [weak self] in
do {
let content material = strive String(contentsOf: url)
DispatchQueue.primary.async {
print(content material)
}
}
catch {
print(error.localizedDescription)
}
}
Apple made an necessary word on their official Knowledge documentation, that it is best to NOT use these strategies for downloading non-file URLs, however nonetheless individuals are educating / utilizing these dangerous practices, however why? 😥
Do not use this synchronous methodology to request network-based URLs.
My recommendation right here: all the time use the URLSession to carry out community associated data-transfers. Creating an information activity is straightforward, it is an asynchronous operation by default, the callback runs on a background thread, so nothing can be blocked by default. Fashionable networking APIs are actual good on iOS, in 99% of the instances you will not want Alamofire anymore for these type of duties. Say no to dependencies! 🚫
URLSession.shared.dataTask(with: url) { knowledge, response, error in
DispatchQueue.primary.async {
}
}.resume()
It is also value to say if you must use a special HTTP methodology (apart from GET), ship particular headers (credentials, settle for insurance policies, and so on.) or present additional knowledge within the physique, you must assemble an URLRequest
object first. You’ll be able to solely ship these customized requests utilizing the URLSession
APIs.
On Apple platforms you aren’t allowed to make use of the insecure HTTP protocol anymore. If you wish to attain a URL with out the safe layer (HTTPS) it’s a must to disable App Transport Safety.
The issue with knowledge duties
What about large recordsdata, similar to photos? Let me present you a couple of tutorials earlier than we dive in:
With all due respect, I feel all of those hyperlinks above are actually dangerous examples of loading distant photos. Certain they do the job, they’re additionally very simple to implement, however possibly we should always cowl the entire story… 🤐
For small interactions with distant servers, you need to use the URLSessionDataTask class to obtain response knowledge into reminiscence (versus utilizing the URLSessionDownloadTask class, which shops the information on to the file system). An information activity is good for makes use of like calling an online service endpoint.
What’s distinction between URLSessionDataTask
vs URLSessionDownloadTask
?
If we learn the docs rigorously, it turns into clear that knowledge activity is NOT the proper candidate for downloading large property. That class is designed to request solely smaller objects, for the reason that underlying knowledge goes to be loaded into reminiscence. Alternatively the obtain activity saves the content material of the response on the disk (as an alternative of reminiscence) and you’ll obtain an area file URL as an alternative of a Knowledge object. Seems that transferring from knowledge duties to obtain duties could have a HUGE influence in your reminiscence consumption. I’ve some numbers. 📈
I downloaded the following picture file (6000x4000px 💾 13,1MB) utilizing each strategies. I made a model new storyboard primarily based Swift 5.1 venture. The fundamental RAM utilization was ~52MB, once I fetched the picture utilizing the URLSessionDataTask
class, the reminiscence utilization jumped to ~82MB. Turning the information activity right into a obtain activity solely elevated the bottom reminiscence measurement by ~4MB (to a complete ~56MB), which is a big enchancment.
let url = URL(string: "https://photos.unsplash.com/photo-1554773228-1f38662139db")!
URLSession.shared.dataTask(with: url) { [weak self] knowledge, response, error in
guard let knowledge = knowledge else {
return
}
DispatchQueue.primary.async {
self?.imageView.picture = UIImage(knowledge: knowledge)
}
}.resume()
URLSession.shared.downloadTask(with: url) { [weak self] url, response, error in
guard
let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first,
let url = url
else {
return
}
do {
let file = cache.appendingPathComponent("(UUID().uuidString).jpg")
strive FileManager.default.moveItem(atPath: url.path,
toPath: file.path)
DispatchQueue.primary.async {
self?.imageView.picture = UIImage(contentsOfFile: file.path)
}
}
catch {
print(error.localizedDescription)
}
}.resume()
After I rendered the picture utilizing an UIImageView
the reminiscence footprint was ~118MB (whole: ~170MB) for the information activity, and ~93MB (whole: ~145MB) for the obtain activity. This is a fast abstract:
- Knowledge activity: ~30MB
- Knowledge activity with rendering: ~118MB
- Obtain activity: ~4MB
- Obtain activity with rendering: ~93MB
I hope you get my level. Please do not forget that the Basis networking layer comes with 4 varieties of session duties. You need to all the time use the proper one that matches the job. We are able to say that the distinction between URLSessionDataTask vs URLSessionDownloadTask is: a whole lot of reminiscence (on this case about 25MB of RAM).
You should use Kingfisher or SDWebImage to obtain & manipulate distant photos..
You may say that that is an edge case since many of the photos (even HD ones) are most a couple of hundred kilobytes. Nonetheless, my takeaway right here is that we will do higher, and we should always all the time achieve this if attainable. 🤓
Downloading photos utilizing Mix
WWDC19, Apple introduced the Mix framework, which brings us a couple of new extensions for some Basis objects. Fashionable occasions require fashionable APIs, proper? In case you are already acquainted with the brand new SDK that is good, but when you do not know what the heck is that this declarative purposeful reactive insanity, it is best to learn my complete tutorial concerning the Mix framework.
The primary model of Mix shipped with a pleasant dataTaskPublisher
extension methodology for the URLSession
class. Wait, the place are the others? No obtain activity writer? What ought to we do now? 🤔
How one can write a customized Writer?
SwiftLee has a pleasant tutorial about Mix that may assist you a large number with UIControl occasions. One other nice learn (even higher than the primary one) by Donny Wals is about understanding Publishers and Subscribers. It is a actually well-written article, it is best to positively examine this one, I extremely advocate it. 🤘🏻
Now let’s begin creating our personal DownloadTaskPublisher
. For those who command + click on on the dataTaskPublisher
methodology in Xcode, you possibly can see the corresponding interface. There’s additionally a DataTaskPublisher
struct, proper under. Primarily based on that template we will create our personal extension. There are two variants of the identical knowledge activity methodology, we’ll replicate this habits. The opposite factor we want is a DownloadTaskPublisher
struct, I am going to present you the Swift code first, then we’ll talk about the implementation particulars.
extension URLSession {
public func downloadTaskPublisher(for url: URL) -> URLSession.DownloadTaskPublisher {
self.downloadTaskPublisher(for: .init(url: url))
}
public func downloadTaskPublisher(for request: URLRequest) -> URLSession.DownloadTaskPublisher {
.init(request: request, session: self)
}
public struct DownloadTaskPublisher: Writer {
public typealias Output = (url: URL, response: URLResponse)
public typealias Failure = URLError
public let request: URLRequest
public let session: URLSession
public init(request: URLRequest, session: URLSession) {
self.request = request
self.session = session
}
public func obtain<S>(subscriber: S) the place S: Subscriber,
DownloadTaskPublisher.Failure == S.Failure,
DownloadTaskPublisher.Output == S.Enter
{
let subscription = DownloadTaskSubscription(subscriber: subscriber, session: self.session, request: self.request)
subscriber.obtain(subscription: subscription)
}
}
}
A Writer can ship an Output or a Failure message to an hooked up subscriber. You need to create a brand new typealias for every sort, since they each are generic constraints outlined on the protocol stage. Subsequent, we’ll retailer the session and the request objects for later use. The final a part of the protocol conformance is that it’s a must to implement the obtain<S>(subscriber: S)
generic methodology. This methodology is answerable for attaching a brand new subscriber by a subscription object. Ummm… what? 🤨
A writer/subscriber relationship in Mix is solidified in a 3rd object, the subscription. When a subscriber is created and subscribes to a writer, the writer will create a subscription object and it passes a reference to the subscription to the subscriber. The subscriber will then request numerous values from the subscription with a purpose to start receiving these values.
A Writer
and a Subscriber
is linked by a Subscription
. The Writer solely creates the Subscription and passes it to the subscriber. The Subscription comprises the logic that’ll fetch new knowledge for the Subscriber. The Subscriber receives the Subscription, the values and the completion (success or failure).
- The Subscriber subscribes to a Writer
- The Writer creates a Subscription
- The Writer offers this Subscription to the Subscriber
- The Subscriber calls for some values from the Subscription
- The Subscription tries to gather the values (success or failure)
- The Subscription sends the values to the Subscriber primarily based on the demand coverage
- The Subscription sends a Failure completion to the Subscriber if an error occurs
- The Subscription sends completion if no extra values can be found
How one can make a customized Subscription?
Okay, time to create our subscription for our little Mix primarily based downloader, I feel that you’ll perceive the connection between these three objects if we put collectively the ultimate items of the code. 🧩
extension URLSession {
remaining class DownloadTaskSubscription<SubscriberType: Subscriber>: Subscription the place
SubscriberType.Enter == (url: URL, response: URLResponse),
SubscriberType.Failure == URLError
{
non-public var subscriber: SubscriberType?
non-public weak var session: URLSession!
non-public var request: URLRequest!
non-public var activity: URLSessionDownloadTask!
init(subscriber: SubscriberType, session: URLSession, request: URLRequest) {
self.subscriber = subscriber
self.session = session
self.request = request
}
func request(_ demand: Subscribers.Demand) {
guard demand > 0 else {
return
}
self.activity = self.session.downloadTask(with: request) { [weak self] url, response, error in
if let error = error as? URLError {
self?.subscriber?.obtain(completion: .failure(error))
return
}
guard let response = response else {
self?.subscriber?.obtain(completion: .failure(URLError(.badServerResponse)))
return
}
guard let url = url else {
self?.subscriber?.obtain(completion: .failure(URLError(.badURL)))
return
}
do {
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let fileUrl = cacheDir.appendingPathComponent((UUID().uuidString))
strive FileManager.default.moveItem(atPath: url.path, toPath: fileUrl.path)
_ = self?.subscriber?.obtain((url: fileUrl, response: response))
self?.subscriber?.obtain(completion: .completed)
}
catch {
self?.subscriber?.obtain(completion: .failure(URLError(.cannotCreateFile)))
}
}
self.activity.resume()
}
func cancel() {
self.activity.cancel()
}
}
}
A Subscriber has an Enter and a Failure sort. A subscriber can solely subscribe to a writer with the identical sorts. The Writer’s Output & Failure sorts must be equivalent with the Subscription Enter and Failure sorts. This time we won’t go along with an associatedType, however we’ve got to create a generic worth that has a constraint on these necessities through the use of a the place clause. The rationale behind that is that we do not know what sort of Subscriber will subscribe to this subscription. It may be both a category A
or B
, who is aware of… 🤷♂️
Now we have to go a couple of properties within the init methodology, retailer them as occasion variables (watch out with lessons, it is best to use weak if relevant). Lastly we implement the worth request methodology, by respecting the demand coverage. The demand is only a quantity. It tells us what number of values can we ship again to the subscriber at most. In our case we’ll have max 1 worth, so if the demand is bigger than zero, we’re good to go. You’ll be able to ship messages to the subscriber by calling numerous obtain strategies on it.
You need to manually ship the completion occasion with the .completed
or the .failure(T)
worth. Additionally we’ve got to maneuver the downloaded short-term file earlier than the completion block returns in any other case we’ll utterly lose it. This time I will merely transfer the file to the applying cache listing. As a free of charge cancellation is a good way to finish battery draining operations. You simply have to implement a customized cancel()
methodology. In our case, we will name the identical methodology on the underlying URLSessionDownloadTask
.
That is it. We’re prepared with the customized writer & subscription. Wanna strive them out?
How one can create a customized Subscriber?
For example that there are 4 sorts of subscriptions. You should use the .sink
or the .assign
methodology to make a brand new subscription, there’s additionally a factor known as Topic, which might be subscribed for writer occasions or you possibly can construct your very personal Subscriber
object. For those who select this path you need to use the .subscribe
methodology to affiliate the writer and the subscriber. You may as well subscribe a topic.
remaining class DownloadTaskSubscriber: Subscriber {
typealias Enter = (url: URL, response: URLResponse)
typealias Failure = URLError
var subscription: Subscription?
func obtain(subscription: Subscription) {
self.subscription = subscription
self.subscription?.request(.limitless)
}
func obtain(_ enter: Enter) -> Subscribers.Demand {
print("Subscriber worth (enter.url)")
return .limitless
}
func obtain(completion: Subscribers.Completion<Failure>) {
print("Subscriber completion (completion)")
self.subscription?.cancel()
self.subscription = nil
}
}
The subscriber above will merely print out the incoming values. Now we have to be extraordinarily cautious with reminiscence administration. The obtained subscription can be saved as a robust property, however when the writer sends a completion occasion we should always cancel the subscription and take away the reference.
When a worth arrives we’ve got to return a requirement. In our case it actually does not matter since we’ll solely have 1 incoming worth, however if you would like to restrict your writer, you need to use e.g. .max(1)
as a requirement.
Here’s a fast pattern code for all of the Mix subscriber sorts written in Swift 5.1:
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
static let url = URL(string: "https://photos.unsplash.com/photo-1554773228-1f38662139db")!
static var defaultValue: (url: URL, response: URLResponse) = {
let fallbackUrl = URL(fileURLWithPath: "fallback-image-path")
let fallbackResponse = URLResponse(url: fallbackUrl, mimeType: "foo", expectedContentLength: 1, textEncodingName: "bar")
return (url: fallbackUrl, response: fallbackResponse)
}()
@Printed var worth: (url: URL, response: URLResponse) = ViewController.defaultValue
let topic = PassthroughSubject<(url: URL, response: URLResponse), URLError>()
let subscriber = DownloadTaskSubscriber()
var sinkOperation: AnyCancellable?
var assignOperation: AnyCancellable?
var assignSinkOperation: AnyCancellable?
var subjectOperation: AnyCancellable?
var subjectSinkOperation: AnyCancellable?
override func viewDidLoad() {
tremendous.viewDidLoad()
self.sinkExample()
self.assignExample()
self.subjectExample()
self.subscriberExample()
}
func sinkExample() {
self.sinkOperation = URLSession.shared
.downloadTaskPublisher(for: ViewController.url)
.sink(receiveCompletion: { completion in
print("Sink completion: (completion)")
}) { worth in
print("Sink worth: (worth.url)")
}
}
func assignExample() {
self.assignSinkOperation = self.$worth.sink { worth in
print("Assign worth: (worth.url)")
}
self.assignOperation = URLSession.shared
.downloadTaskPublisher(for: ViewController.url)
.replaceError(with: ViewController.defaultValue)
.assign(to: .worth, on: self)
}
func subjectExample() {
self.subjectSinkOperation = self.topic.sink(receiveCompletion: { completion in
print("Topic completion: (completion)")
}) { worth in
print("Topic worth: (worth.url)")
}
self.subjectOperation = URLSession.shared
.downloadTaskPublisher(for: ViewController.url)
.subscribe(self.topic)
}
func subscriberExample() {
URLSession.shared
.downloadTaskPublisher(for: ViewController.url)
.subscribe(DownloadTaskSubscriber())
}
}
That is very nice. We are able to obtain a file utilizing our customized Mix primarily based URLSession extension.
Do not forget to retailer the AnyCancellable pointer in any other case your complete Mix operation can be deallocated means earlier than you possibly can obtain something from the chain / stream.
Placing all the pieces collectively
I promised a working picture downloader, so let me clarify the entire movement. Now we have a customized obtain activity writer that’ll save our take away picture file domestically and returns a tuple with the file URL and the response. ✅
Subsequent I will merely assume that there was a sound picture behind the URL, and the server returned a sound response, so I will map the writer’s output to an UIImage
object. I am additionally going to interchange any type of error with a fallback picture worth. In a real-world utility, it is best to all the time do some additional checkings on the URLResponse
object, however for the sake of simplicity I am going to skip that for now.
The very last thing is to replace our picture view with the returned picture. Since this can be a UI activity it ought to occur on the principle thread, so we’ve got to make use of the obtain(on:)
operation to change context. If you wish to study extra about schedulers within the Mix framework it is best to learn Vadim Bulavin’s article. It is a gem. 💎
In case you are not receiving values on sure appleOS variations, that is may as a result of there was a change in Mix round December, 2019. You need to examine these hyperlinks: link1, link2
Anyway, this is the ultimate Swift code for a attainable picture obtain operation, easy & declarative. 👍
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
var operation: AnyCancellable?
override func viewDidLoad() {
tremendous.viewDidLoad()
let url = URL(string: "https://photos.unsplash.com/photo-1554773228-1f38662139db")!
self.operation = URLSession.shared
.downloadTaskPublisher(for: url)
.map { UIImage(contentsOfFile: $0.url.path)! }
.replaceError(with: UIImage(named: "fallback"))
.obtain(on: DispatchQueue.primary)
.assign(to: .picture, on: self.imageView)
}
}
Lastly, we will show our picture. Ouch, however wait… there’s nonetheless room for enhancements. What about caching? Plus a 6000x4000px image is sort of enormous for a small show, should not we resize / scale the picture first? What occurs if I need to use the picture in a listing, should not I cancel the obtain duties when the person scrolls? 😳
Possibly I am going to write about these points in an upcoming tutorial, however I feel that is the purpose the place I ought to finish this text. Be at liberty to mess around with my resolution and please share your concepts & ideas with me on Twitter.