HomeiOS DevelopmentHow one can obtain recordsdata with URLSession utilizing Mix Publishers and Subscribers?

How one can obtain recordsdata with URLSession utilizing Mix Publishers and Subscribers?


Learn to load a distant picture into an UIImageView asynchronously utilizing URLSessionDownloadTask and the Mix framework in Swift.

iOS

A easy picture downloader

Downloading a useful resource from an URL looks as if a trivial job, however is it actually that straightforward? Nicely, it relies upon. If you must obtain and parse a JSON file which is only a few KB, then you’ll be able to go together with the classical approach or you should use the brand new dataTaskPublisher methodology on the URLSession object from the Mix framework.

Dangerous practices ⚠️

There are some fast & soiled approaches that you should use to get some smaller knowledge from the web. The issue with these strategies is that you must deal rather a lot with threads and queues. Luckily utilizing the Dispatch framework helps rather a lot, so you’ll be able to flip your blocking features 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 Information(contentsOf: url)
}
catch {
    print(error.localizedDescription)
}



DispatchQueue.world().async { [weak self] in
    
    do {
        let content material = strive String(contentsOf: url)
        DispatchQueue.foremost.async {
            
            print(content material)
        }
    }
    catch {
        print(error.localizedDescription)
    }
}

Apple made an necessary be aware on their official Information documentation, that you need to NOT use these strategies for downloading non-file URLs, however nonetheless persons are instructing / utilizing these unhealthy practices, however why? 😥

Do not use this synchronous methodology to request network-based URLs.

My recommendation right here: at all times use the URLSession to carry out community associated data-transfers. Creating a knowledge job is easy, 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 circumstances you will not want Alamofire anymore for these sort of duties. Say no to dependencies! 🚫


URLSession.shared.dataTask(with: url) { knowledge, response, error in
    
    DispatchQueue.foremost.async {
        
    }
}.resume()

It is also price to say if you could use a unique HTTP methodology (apart from GET), ship particular headers (credentials, settle for insurance policies, and so forth.) or present further knowledge within the physique, you could assemble an URLRequest object first. You may solely ship these customized requests utilizing the URLSession APIs.

on Apple platforms you aren’t allowed to make use of the unsecure HTTP protocol anymore. If you wish to attain a URL with out the safe layer (HTTPS) you must disable App Transport Safety.

The issue with knowledge duties

What about huge recordsdata, equivalent to pictures? Let me present you just a few tutorials earlier than we dive in:

With all due respect, I believe all of those hyperlinks above are actually unhealthy examples of loading distant pictures. Certain they do the job, they’re additionally very straightforward to implement, however perhaps we should always cowl the entire story… 🤐

For small interactions with distant servers, you should use the URLSessionDataTask class to obtain response knowledge into reminiscence (versus utilizing the URLSessionDownloadTask class, which shops the info on to the file system). A knowledge job is right for makes use of like calling an online service endpoint.

What’s distinction between URLSessionDataTask vs URLSessionDownloadTask?

If we learn the docs fastidiously, it turns into clear that knowledge job is NOT the suitable candidate for downloading huge property. That class is designed to request solely smaller objects, because the underlying knowledge goes to be loaded into reminiscence. Then again the obtain job saves the content material of the response on the disk (as an alternative of reminiscence) and you’ll obtain a neighborhood file URL as an alternative of a Information object. Seems that transferring from knowledge duties to obtain duties could have a HUGE affect 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 undertaking. The fundamental RAM utilization was ~52MB, once I fetched the picture utilizing the URLSessionDataTask class, the reminiscence utilization jumped to ~82MB. Turning the info job right into a obtain job solely elevated the bottom reminiscence dimension by ~4MB (to a complete ~56MB), which is a big enchancment.

let url = URL(string: "https://pictures.unsplash.com/photo-1554773228-1f38662139db")!



URLSession.shared.dataTask(with: url) { [weak self] knowledge, response, error in
    guard let knowledge = knowledge else {
        return
    }
    DispatchQueue.foremost.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.foremost.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 info job, and ~93MB (whole: ~145MB) for the obtain job. Here is a fast abstract:

  • Information job: ~30MB
  • Information job with rendering: ~118MB
  • Obtain job: ~4MB
  • Obtain job with rendering: ~93MB

I hope you get my level. Please do not forget that the Basis networking layer comes with 4 kinds of session duties. It is best to at all times use the suitable one that matches the job. We are able to say that the distinction between URLSessionDataTask vs URLSessionDownloadTask is: loads of reminiscence (on this case about 25MB of RAM).

You should utilize Kingfisher or SDWebImage to obtain & manipulate distant pictures..

You may say that that is an edge case since a lot of the pictures (even HD ones) are most just a few hundred kilobytes. Nonetheless, my takeaway right here is that we are able to do higher, and we should always at all times achieve this if doable. 🤓


Downloading pictures utilizing Mix

WWDC19, Apple introduced the Mix framework, which brings us just a few new extensions for some Basis objects. Fashionable instances require fashionable APIs, proper? If you’re already acquainted with the brand new SDK that is good, but when you do not know what the heck is that this declarative practical reactive insanity, you need to learn my complete tutorial in regards to 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 job 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, you need to positively verify this one, I extremely suggest it. 🤘🏻

Now let’s begin creating our personal DownloadTaskPublisher. In the event you command + click on on the dataTaskPublisher methodology in Xcode, you’ll be able to see the corresponding interface. There’s additionally a DataTaskPublisher struct, proper under. Based mostly on that template we are able to create our personal extension. There are two variants of the identical knowledge job methodology, we’ll replicate this conduct. The opposite factor we want is a DownloadTaskPublisher struct, I am going to present you the Swift code first, then we’ll focus on 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. It’s a must 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 you must implement the obtain<S>(subscriber: S) generic methodology. This methodology is accountable for attaching a brand new subscriber by means of 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 plenty of values from the subscription as a way to start receiving these values.

A Writer and a Subscriber is related by means of a Subscription. The Writer solely creates the Subscription and passes it to the subscriber. The Subscription incorporates 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 believe 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
    {
        personal var subscriber: SubscriberType?
        personal weak var session: URLSession!
        personal var request: URLRequest!
        personal var job: 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.job = 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.job.resume()
        }

        func cancel() {
            self.job.cancel()
        }
    }
}

A Subscriber has an Enter and a Failure sort. A subscriber can solely subscribe to a writer with the identical varieties. The Writer’s Output & Failure varieties should be similar with the Subscription Enter and Failure varieties. This time we will not go together with an associatedType, however we now have to create a generic worth that has a constraint on these necessities by utilizing a the place clause. The explanation 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… 🤷‍♂️

We have now to go just a few properties within the init methodology, retailer them as occasion variables (watch out with lessons, you need 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 larger than zero, we’re good to go. You may ship messages to the subscriber by calling numerous obtain strategies on it.

It’s a must to manually ship the completion occasion with the .completed or the .failure(T) worth. Additionally we now have to maneuver the downloaded momentary file earlier than the completion block returns in any other case we’ll fully lose it. This time I will merely transfer the file to the appliance cache listing. As a free of charge cancellation is an effective way to finish battery draining operations. You simply must implement a customized cancel() methodology. In our case, we are able to 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?

As an instance that there are 4 sorts of subscriptions. You should utilize the .sink or the .assign methodology to make a brand new subscription, there may be additionally a factor referred to as Topic, which could be subscribed for writer occasions or you’ll be able to construct your very personal Subscriber object. In the event you select this path you should use the .subscribe methodology to affiliate the writer and the subscriber. It’s also possible to 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. We have now to be extraordinarily cautious with reminiscence administration. The obtained subscription can be saved as a powerful 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 now have to return a requirement. In our case it actually would not matter since we’ll solely have 1 incoming worth, however if you would like to restrict your writer, you should use e.g. .max(1) as a requirement.

Here’s a fast pattern code for all of the Mix subscriber varieties written in Swift 5.1:

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    static let url = URL(string: "https://pictures.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)
    }()

    @Revealed 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 approach earlier than you could possibly obtain something from the chain / stream.

Placing every little thing collectively

I promised a working picture downloader, so let me clarify the entire move. We have now a customized obtain job writer that’ll save our take away picture file regionally and returns a tuple with the file url and the response. ✅

Subsequent I will merely assume that there was a legitimate picture behind the url, and the server returned a legitimate response, so I will map the writer’s output to an UIImage object. I am additionally going to interchange any sort of error with a fallback picture worth. In a real-world utility, you need to at all times do some further 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 job it ought to occur on the primary thread, so we now have to make use of the obtain(on:) operation to modify context. If you wish to study extra about schedulers within the Mix framework you need to learn Vadim Bulavin‘s article. It is a gem. 💎

If you’re not receiving values on sure appleOS variations, that is may as a result of there was a change in Mix round December, 2019. It is best to verify these hyperlinks: link1, link2

Anyway, this is the ultimate Swift code for a doable 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://pictures.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.foremost)
            .assign(to: .picture, on: self.imageView)
    }
}

Lastly, we are able to show our picture. Ouch, however wait… there may be nonetheless room for enhancements. What about caching? Plus a 6000x4000px image is sort of big for a small show, should not we resize / scale the picture first? What occurs if I wish to use the picture in an inventory, should not I cancel the obtain duties when the consumer scrolls? 😳

Perhaps I am going to write about these points in an upcoming tutorial, however I believe 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.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments