in ,

Using Apple's Combine Framework, Hacker News


Included are a series of patterns and examples of Publishers, Subscribers, and pipelines. These examples are meant to illustrate how to use the Combine framework to accomplish various tasks.

Since this is a work in progress:if you have a suggestion for a pattern or recipe, I’m happy to consider it.

Creating a subscriber with sink

Goal
  • To receive the output, and the errors or completion messages, generated from a publisher or through a pipeline, you can create a subscriber withsink.

References
See also
Code and explanation

Sink creates an all-purpose subscriber to capture or react the data from a Combine pipeline, while also supporting cancellation and thepublisher subscriber lifecycle.

simple sink

letcancellablePipeline=publishingSource.sink{someValuein(1)    / / do what you want with the resulting value passed down    / / be aware that depending on the data type being returned, you may get this closure invoked    / / multiple times.    print(" .sink () received (someValue)")} )
1) The simple version of a sink is very compact, with a single trailing closure that only receives data when presented through the pipeline.

sink with completions and data

letcancellablePipeline=publishingSource.sink(receiveCompletion:{(completion)   (in)(1)    switchcompletion{    case.finished:        / / no associated data, but you can react to knowing the request has been completed        break    case.failure(letanError) :        / / do what you want with the error details, presenting, logging, or hiding as appropriate        print(" received the error: ",anError)        break    }} ,receiveValue:{someValuein    / / do what you want with the resulting value passed down    / / be aware that depending on the data type being returned, you may get this closure invoked    / / multiple times.    print(" .sink () received (someValue)")} )cancellablePipeline.cancel()(2)
1) Sinks are created by chaining the code from a publisher or pipeline, and terminate the pipeline. When the sink is created or invoked on a publisher, it implicitly startsthe lifecyclewith thesubscribeand will request unlimited data.
(2) Creating a sink is cancellable subscriber, so at any time you can take the reference that terminated with sink and invoke. cancel ()on it to invalidate and shut down the pipeline.

Creating a subscriber with assign

Goal
  • To use the results of a pipeline to set a value, often a property on a user interface view or control, but any KVO compliant object can be the target

References
See also
Code and explanation

Assign is a subscriber that’s specifically designed to apply data from a publisher or pipeline into a property, updating that property whenever it receives data. Like sink, it activates when created and requests an unlimited data updates. Assign requires the failure type to be specified as, so if your pipeline could fail (such as using an operator like tryMap) you will need toconvert or handle the failure casesbefore using.assign.

simple sink

letcancellablePipeline=publishingSource(1)    .receive(on:RunLoop.main)(2)    .assign(to:.isEnabled,on:yourButton)(3)cancellablePipeline.cancel()(4)
1) . assignis typically chained onto a publisher when you create it, and the return value is cancellable.
(2) If. assignis being used to update a user interface element, you need to make sure that it is being updated on the main thread. This call makes sure the subscriber is received on the main thread. (3) Assign references the property being updated using a (key path) , and a reference to the object being updated. (4) At any time you can cancel to terminate and invalidate pipelines with cancel (). Frequently, you cancel the pipelines when you deactivate the objects (such as a viewController) that are getting updated from the pipeline.

Making a network request with dataTaskPublisher

Goal
  • One of the common use cases is requesting JSON data from a URL and decoding it.

References
See also
Code and explanation

This can be readily accomplished with Combine usingURLSession.dataTaskPublisherfollowed by a series of operators that process the data. Minimally, this ismapandDecodebefore going into your subscriber.

The simplest case of using this might be:

letmyURL=URL(string:"https: // postman -echo.com/time/valid?timestamp=2016 - 10 - 10 ")/ / checks the validity of a timestamp - this one returns {"valid": true}/ / matching the data structure returned from https://postman-echo.com/time/validfileprivatestructPostmanEchoTimeStampCheckResponse:Decodable,Hashable{(1)    letvalid:Bool}letremoteDataPublisher=URLSession.shared.dataTaskPublisher(for:myURL!)(2)    / / the dataTaskPublisher output combination is (data: Data, response: URLResponse)    .map{$ 0.data}(3)    .decode(type:PostmanEchoTimeStampCheckResponse.self,decod er:JSONDecoder())(4)letcancellableSink=remoteDataPublisher    .sink(receiveCompletion:{completionin            print(" .sink () received the completion ",String(describing:completion))            switchcompletion{                case.finished:(5)                    break                case.failure(letanError) :( 6)                    print(" received error: ",anError)            }    } ,receiveValue:{someValuein(7)        print(" .sink () received (someValue)")    } )
(1) Commonly you’ll have a struct defined that supports at leastDecodable(if not the fullCodable protocol). This struct can be defined to only pull the pieces you’re interested in from the JSON provided over the network.
(2) dataTaskPublisher is instantiated from URLSession. You can configure your own options on URLSession, or use the general shared session as you require. (3) The data that is returns down the pipeline is a tuple:(data: Data, response: URLResponse ). The (map) operator is used to get the data and drop the URL response, returning just Data down the pipeline. (4) Decodeis used to load the data and attempt to transform it into the struct defined. Decode can throw an error itself if the decode fails. If it succeeds, the object passed down the pipeline will be the struct from the JSON data. (5) If the decoding happened without errors, the finished completion will be triggered, and the value will also be passed to the receiveValue closure. (6) If the a failure happened (either with the original network request or the decoding), the error will be passed into with the.failureCompletion. (7) Only if the data succeeded with request and decoding will this closure get invoked, and the data format received will be an instance of the structPostmanEchoTimeStampCheckResponse.

Stricter request processing with dataTaskPublisher

Goal
  • When URLSession makes a connection, it only reports an error if the remote server doesn’t respond. You may want to consider a number of responses, based on status code, to be errors. To accomplish this, you can use tryMap to inspect the http response and throw an error in the pipeline.

References
See also
Code and explanation

To have more control over what is considered a failure in the URL response, use atryMapoperator on the tuple response from dataTaskPublisher. Since dataTaskPublisher returns both the response data and the URLResponse into the pipeline, you can immediately inspect the response and throw an error of your own if desired.

An example of that might look like:

letmyURL=URL(string:"https: // postman -echo.com/time/valid?timestamp=2016 - 10 - 10 ")/ / checks the validity of a timestamp - this one returns {"valid": true}/ / matching the data structure returned from https://postman-echo.com/time/validfileprivatestructPostmanEchoTimeStampCheckResponse:Decodable,Hashable{    letvalid:Bool}enumtestFailureCondition:Error{    caseinvalidServerResponse}letremoteDataPublisher=URLSession.shared.dataTaskPublisher(for:myURL!)    .tryMap{data,Response->Datain(1)                guardlethttpResponse=ResponseasHTTPURLResponse,(2)                    httpResponse.statusCode==200else{(3)                        throwtestFailureCondition.invalidServerResponse(4)                }                returndata(5)    }    .decode(type:PostmanEchoTimeStampCheckResponse.self,decod er:(JSONDecoder)() )letcancellableSink=remoteDataPublisher    .sink(receiveCompletion:{completionin            print(" .sink () received the completion ",String(describing:completion))            switchcompletion{                case.finished:                    break                case.failure(letanError) :                    print(" received error: ",anError)            }    } ,receiveValue:{someValuein        print(" .sink () received (someValue)")    } )

Where theprevious patternused amapoperator, this uses tryMap, which allows us to identify and throw errors in the pipeline based on what was returned.

1) (tryMap) still gets the tuple of(data: Data, response: URLResponse), and is defined here as returning just the type of Data down the pipeline.
(2) Within the closure for tryMap, we can cast the response to HTTPURLResponse and dig deeper into it, including looking at the specific status code. (3) In this case, we want to consideranythingother than a 200 response code as a failure. HTTPURLResponse.status_code is an Int type, so you could also have logic such ashttpResponse.statusCode>300. (4) If the predicates aren’t met, then we can throw an instance of an error of our choosing,invalidServerResponsein this case. (5) If no error has occured, then we simply pass down Data for further processing.

When an error is triggered on the pipeline, aFailurecompletion is sent with the error encapsulated within it, regardless of where it happened in the pipeline.


Wrapping an asynchronous call with a Future to create a one-shot publisher

Goal
  • Using Future to turn an an asynchronous call into publisher to use the result in a combine pipeline.

References
See also
Code and explanation
importContactsletfutureAsyncPublisher=FutureBool,Error>{promisein(1)    CNContactStore().requestAccess(for:.contacts){grantedAccess,errin(2)        / / err is an optional        ifleterr=err{(3)            promise(.failure(err) )        }        returnpromise(.success(grantedAccess) )(4)    }}.eraseToAnyPublisher()
1) Future itself has you define the return types and takes a closure. It hands in a Result object matching the type description, which you interact.
(2) You can invoke the async API however is relevant, including passing in it’s required closure. (3) Within the completion handler, you determine what would cause a failure or a success. A call to (promise) .failure ())returns the failure. (4) Or a call to (promise) .success ())returns a value.

Sequencing operations with Combine

Goal
  • To explicitly order asynchronous operations with a Combine pipeline

References
See also
Code and explanation

Any asynchronous (or synchronous) set of tasks that need to happen in a specific order can also be coordinated using a combine pipeline. By using (Future) operator, the very act of completing an asynchronous call can be captured, and sequencing operators provides the structure of that coordination.

For example, by wrapping an asynchronous API calls with the (Future) publisher and then chaining them together with theflatMapoperator, you invoke the wrapped asynchronous API calls in the order of the pipeline. Multiple parallel asynchronous efforts can be created by creating multiple pipelines, with (Future) or another publisher. You can wait for all parallel pipelines to complete before continuing by merging them together with the (zip) *************************************** (operator.)

If you want force an (Future) publisher to not be invoked until another has completed, then creating the future publisher in theflatMapclosure causes it to wait to be created until a value has been passed to the flatMap operator.

These techniques can be composed, creating any structure of parallel or serial tasks.

This technique of coordinating asynchronous calls can be especially effective if later tasks need data from earlier tasks. In those cases, the data results needed can be passed directly the pipeline.

An example of this sequencing follows below. In this example, buttons (arranged visually to show the ordering of actions) are highlighted when they complete. The whole sequence is triggered by a seperate button action, which also resets the state of all the buttons and cancels any existing running sequence if it’s not yet finished. In this example, the asynchronous API call is a call that simple takes a random amount of time to complete to provide an example of how the timing works.

The workflow that is created is represented in steps:

  • step 1 runs first.

  • step 2 has three parallel efforts, running after step 1 completes.

  • step 3 waits to start until all three elements of step 2 complete.

  • step 4 runs after step 3 has completed.

Additionally, there is an activity indicator that is triggered to start animating when the sequence begins, stopping when step 4 has run to completion.

import(UIKit)importCombineclassAsyncCoordinatorViewController:UIViewController{    @ IBOutletweakvarstartButton:UIButton    @ IBOutletweakvarstep1_button:  UIButton!    @ IBOutletweakvarstep2_1_button:UIButton    @ IBOutletweakvarstep2_2_button:UIButton    @ IBOutletweakvarstep2_3_button:UIButton    @ IBOutletweakvarstep3_button:UIButton    @ IBOutletweakvarstep4_button:UIButton    @ IBOutletweakvaractivityIndicator:UIActivityIndicatorView    varCancellable:AnyCancellable?    varcoordinatedPipeline:AnyPublisherBool,Error>?    @ IBActionFuncDoit(_sender:Any){        runItAll()    }    FuncrunItAll(){        ifself.Cancellable!=nil{(1)            print(" Canceling existing run ")            Cancellable?.cancel()            self.activityIndicator.stopAnimating()        }        print(" resetting all the steps ")        self.resetAllSteps()(2)        / / driving it by attaching it to .sink        self.activityIndicator.startAnimating()(3)        print(" attaching a new sink to start things going ")        self.Cancellable=coordinatedPipeline?(4)            .print()            .sink(receiveCompletion:{completionin                print(" .sink () received the completion: ",String(describing:completion))                self.activityIndicator.stopAnimating()            } ,receiveValue:{valuein                print(" .sink () received value: ",value)            } )    }    / / MARK: - helper pieces that would normally be in other files    / / this emulates an async API call with a completion callback    / / it does nothing other than wait and ultimately return with a boolean value    FuncrandomAsyncAPI(completioncompletionBlock:@ Escaping(((Bool),Error?)->Void)){        DispatchQueue.global(qos:.background).Async{            sleep(.Random(in:1. ..(4)))            completionBlock(true,nil)        }    }    / // Creates and returns pipeline that uses a Future to wrap randomAsyncAPI, then updates a UIButton to represent    / // the completion of the async work before returning a boolean True    / // - Parameter button: button to be updated    FunccreateFuturePublisher(button:UIButton)->AnyPublisher(Bool),(Error)>{( 5)        returnFutureBool,Error>{promisein            self.randomAsyncAPI(){(result,err)in                ifleterr=err{                    promise(.failure(err) )                }                promise(.success(result))            }        }        .receive(on:RunLoop.main)            / / so that we can update UI elements to show the "completion"            / / of this step        .map{inValue->Boolin( 6)            / / intentially side effecting here to show progress of pipeline            self.markStepDone(button:button)            returntrue        }        .eraseToAnyPublisher()    }    / // highlights a button and changes the background color to green    / // - Parameter button: reference to button being updated    FuncmarkStepDone(button:UIButton){        button.backgroundColor=.systemGreen        button.isHighlighted=true    }    FuncresetAllSteps(){        forbuttonin[self.step1_button,self.step2_1_button,self.step2_2_button,self.step2_3_button,self.step3_button,self.step4_button]{            button?.backgroundColor=.lightGray            button?.isHighlighted=false        }        self.activityIndicator.stopAnimating()    }    / / MARK: - view setup    OverrideFuncviewDidLoad(){        super.viewDidLoad()        self.activityIndicator.stopAnimating()        / / Do any additional setup after loading the view.        coordinatedPipeline=createFuturePublisher(button:self.step1_button)(7)            .flatMap{flatMapInValue->AnyPublisherBool,Error>in            letstep2_1=self.createFuturePublisher(button:self.step2_1_button)            letstep2_2=self.createFuturePublisher(button:self.step2_2_button)            letstep2_3=self.createFuturePublisher(button:self.step2_3_button)            returnPublishers.Zip3(step2_1,step2_2,step2_3)                .map{_->Boolin                    returntrue                }                .eraseToAnyPublisher()            }        .flatMap{_in            returnself.createFuturePublisher(button:self.step3_button)        }        .flatMap{_in            returnself.createFuturePublisher(button:self.step4_button)        }        .eraseToAnyPublisher()    }}
(1) runItAllcoordinates the operation of this little workflow, starting with checking to see if one is currently running. If defined, it calls the cancel on the existing subscriber.
(2) resetAllStepsiterates through all the existing buttons used represent the progress of this workflow, and resets them to gray and unhighlighted to reflect an initial state. It also verifies that the activity indicator is not currently animated. (3) Then we get things started, first with activating the animation on the activity indicator. (4) Creating the subscriber with (sink) and storing the reference initiates the workflow. The publisher to which it is subscribing is setup outside this function, allowing it to be re-used multiple times. Theprintoperator in the pipeline is for debugging, to show console output of when the pipeline is triggered. (5) Each step is represented by the invocation of a (Future) publisher, followed immediately by pipeline elements to switch to the main thread and then update a UIButton’s background to show the step has completed. This is encapsulated in acreateFuturePublishercall, usingeraseToAnyPublisherto simplify the type being returned. (6) Themapoperator is used to create this specific side effect of updating the a UIbutton to show the step has been completed. (7) The creation of the overall pipeline and it’s structure of serial and parallel tasks is created from the combination of calls tocreateFuturePublisheralong with the operators (flatMap) and (zip) .

Error Handling

The examples above expected that the subscriber would handle the error conditions, if they occured. However, you are not always able to control the subscriber – as might be the case if you’re using SwiftUI view properties as the subscriber, and you’re providing the publisher. In these cases, you need to build your pipeline so that the output types match the subscriber types.

For example, if you are working with SwiftUI and the you want to use (assign) to set theisEnabledproperty on a button, the subscriber will have a few requirements:

  1. the subcriber should match the type output of,

  2. the subscriber should be called on the main thread

With a publisher that can throw an error (such asURLSession.dataTaskPublisher), you need to construct a pipeline to convert the output type, but also handle the error within the pipeline to match a failure type of.

How you handle the errors within a pipeline is very dependent on how the pipeline is working. If the pipeline is set up to return a single result and terminate, continue toUsing catch to handle errors in a one-shot pipeline. If the pipeline is set up to continually update, the error handling needs to be a little more complex. Jump ahead toUsing flatMap with catch to handle errors.

Verifying a failure hasn’t happened using assertNoFailure

Goal
  • Verify no error has occured within a pipeline

References
See also
  • >

Code and explanation

Useful in testing invariants in pipelines, the assertNoFailure operator also converts the failure type to. The operator will cause the application to terminate (and tests to crash to a debugger) if the assertion is triggered.

This is useful for verifying the invariant of having dealt with an error. If you are sure you handled the errors and need to map a pipeline which technically can generate a failure type ofto a subscriber that requires a failure type of.


Using catch to handle errors in a one-shot pipeline

Goal
  • If you need to handle a failure within a pipeline, for example before using theassignoperator or another operator that requires the failure type to be, you can usecatchto provide the appropriate logic.

References
See also
Code and explanation

(catch) handles errors by replacing the upstream publisher with another publisher that you provide as a return in a closure.

Be aware that this effectively terminates the earlier portion of the pipeline. If you’re using a one-shot publisher (one that doesn’t create more than a single event), then this is fine.

For example,URLSession.dataTaskPublisheris a one-shot publisher and you might use catch with it to ensure that you get a response, returning a placeholder in the event of an error. Extending our previous example to provide a default response:

struct(IPInfo):Codable{    / / matching the data structure returned from ip.jsontest.com    varIP:String}letmyURL=URL(string:" http://ip.jsontest.com ")/ / NOTE (heckj): you'll need to enable insecure downloads in your Info.plist for this example/ / since the URL scheme is 'http'letremoteDataPublisher=URLSession.shared.dataTaskPublisher(for:myURL!)    / / the dataTaskPublisher output combination is (data: Data, response: URLResponse)    .map( {(inputTuple)->(Data)   (in)          returninputTuple.data    } )    .decode(type:IPInfo.self,decoder:JSONDecoder())(1)    .catch{errin(2)        returnPublishers.Just(IPInfo(IP:" 8.8.8.8``))(3)    }    .eraseToAnyPublisher()
1) Often, a catch operator will be placed after several operators that could fail, in order to provide a fallback or placeholder in the event that any of the possible previous operations failed.
(2) When using catch, you get the error type in and can inspect it to choose how you provide a response. (3) The Just publisher is frequently used to either start another one-shot pipeline or to directly provide a placeholder response in the event of failure.

A possible problem with this technique is that the if the original publisher generates more values ​​to which you wish to react, the original pipeline has been ended. If you are creating a pipeline that reacts to aPublishedproperty, then after any failed value that activates the catch operator, the pipeline will cease to react further. Seecatchfor more illustration and examples of how this works.


Retrying in the event of a temporary failure

Goal
References
See also
Code and explanation

When you specify this operator in a pipeline and it receives a subscription, it first tries to request a subscription from it’s upstream publisher. If the response to that subscription fails, then it will retry the subscription to the same publisher.

The retry operator can be specified with a number of retries to attempt. If no number of retries is specified, it will attempt to retry indefinitely until it receives a .finished completion from it’s subscriber. If the number of retries is specified and all requests fail, then the. failurecompletion is passed down to the subscriber of this operator.

In practice, this is mostly commonly desired when attempting to request network resources with an unstable connection. If you use a retry operator, you should add a specific number of retries so that the subscription doesn’t effectively get into an infinite loop.

An example of the above example using retry in combination with a delay:

letremoteDataPublisher=urlSession.dataTaskPublisher(for(***********************************************: (self).(mockURL)  (***********************************************!)    .delay(for:DispatchQueue.SchedulerTimeType.Stride(integerLiteral(***********************************************:Int.Random(in:(1)(5))),Scheduler:backgroundQueue)(1)    .retry(3)(2)    .tryMap{data,Response->Datain(3)        guardlethttpResponse=ResponseasHTTPURLResponse,            httpResponse.statusCode==200else{                throwtestFailureCondition.invalidServerResponse        }        returndata    }    .decode(type:PostmanEchoTimeStampCheckResponse.self,decod er:JSONDecoder())    .subscribe(on:backgroundQueue)    .eraseToAnyPublisher()
1) the delay operator will delay further processing on the pipeline, in this case for a random selection of 1 to 5 seconds. By adding delay here in the pipeline, it will always occur, even if the original request is successful.
(2) retry is specified as trying 3 times. If you specify retry without any options, it will retry infinitely, and may cause your pipeline to never resolve any values ​​or completions. (3) tryMap is being used to investigate errors after the retry so that retry will only re-attempt the request when the site didn’t respond .

When using theretryoperator withURLSession.dataTaskPublisher, verify that the URL you are requesting isn’t going to have negative side effects if requested repeatedly or with a retry. Ideally such requests are be expected to be idempotent. If they are not, theretryoperator may make multiple requests, with very unexpected side effects.


Using flatMap with catch to handle errors

Goal
  • TheflatMapoperator can be used withcatchto continue to handle errors on new published values.

References
See also
Code and explanation

TheflatMapoperator is the operator to use in handling errors on a continual flow of events.

You provide a closure to flatMap that can read in the value that was provided, and creates a one-shot closure that does the possibly failing work. An example of this is requesting data from a network and then decoding the returned data. You can include acatchoperator to capture any errors and provide any appropriate value.

This is a perfect mechanism for when you want to maintain updates up an upstream publisher, as it creates one-shot publisher or short pipelines that send a single value and then complete for every incoming value. The completion from the created one-shot publishers terminates in the flatMap and isn’t passed to downstream subscribers.

An example of this with a dataTaskPublisher:

letremoteDataPublisher=Just(Self.testURL!)(1)    .flatMap{urlin(2)        URLSession.shared.dataTaskPublisher(for:url)(3)        .tryMap{data,Response->Datain(4)            guardlethttpResponse=ResponseasHTTPURLResponse,                httpResponse.statusCode==200else{                    throwtestFailureCondition.invalidServerResponse            }            returndata        }        .decode(type:PostmanEchoTimeStampCheckResponse.self,decod er:JSONDecoder())(5)        .catch{_in(6 )            returnJust(PostmanEchoTimeStampCheckResponse(valid:false) )        }    }    .subscribe(on:self.myBackgroundQueue!)    .eraseToAnyPublisher()
(1) Just starts this publisher as an example by passing in a URL.
(2) flatMap takes the URL as input and the closure goes on to create a one-shot publisher chain. (3) dataTaskPublisher uses the input url (4) which flows to tryMap to parse for additional errors (5) and finally decode to attempt to refine the returned data into a local type (6) if any of these have failed, catch will convert the error into a placeholder sample, in this case an object with a presetvalid=falseproperty.

Requesting data from an alternate URL when the network is constrained

Goal

    From Apple’s WWDC 19 presentationAdvances in Networking, Part 1, a sample pattern was provided usingtryCatchandtryMapoperators to react to the specific error of having the network be constrained.

References
See also
Code and explanation

This sample is originally from the WWDC session. The API and example is evolving with the beta releases of Combine since that presentation.tryCatchwas missing in the beta2 release, and has returned in beta3.

// Generalized Publisher for Adaptive URL LoadingFuncadaptiveLoader(regularURL:URL,lowDataURL:URL)->AnyPublisherData,Error>{    varrequest=URLRequest(url:regularURL)(1)    request.allowsConstrainedNetworkAccess=false(2)    returnURLSession.shared.dataTaskPublisher(for:request)(3)        .tryCatch{error->URLSession.DataTaskPublisherin(4)            guarderror.networkUnavailableReason==.constrainedelse{               throwerror            }            returnURLSession.shared.dataTaskPublisher(for:lowDataURL)(5)        .tryMap{data,Response->Datain            guardlethttpResponse=ResponseasHTTPUrlResponse,( (6)                     httpResponse.status_code==200else{                       throwMyNetworkingError.invalidServerResponse            }            returndata}.eraseToAnyPublisher()(7)

This example, from Apple’s WWDC, provides a function that takes two URLs – a primary and a fallback. It returns a publisher that will request data and fall back requesting a secondary URL when the network is constrained.

(1) The request starts with an attempt requesting data.
(2) Settingrequest.allowsConstrainedNetworkAccesswill cause the dataTaskPublisher to error if the network is constrained. (3) Invoke the dataTaskPublisher to make the request. (4) tryCatch is used to capture the immediate error condition and check for a specific error (the constrained network). (5) If it finds an error, it creates a new one-shot publisher with the fall-back URL. (6) The resulting publisher can still fail, and tryMap can map this a failure by throwing an error on HTTP response codes that map to error conditions (7) eraseToAnyPublisher will do type erasure on the chain of operators so the resulting signature of the adaptiveLoader function is of type (AnyPublisher)

In the sample, if the error returned from the original request wasn’t an issue of the network being constrained, it passes on the .failure completion down the pipeline. If the error is that the network is constrained, then the tryCatch operator creates a new request to an alternate URL.


UIKit (or AppKit) Integration

Declarative UI updates from user input

Goal
  • Querying a web based API and returning the data to be displayed in your UI

References
See also
Code and explanation

One of the primary benefits of a framework like Combine is setting up a declarative structure that defines how an interface will update to user input.

A pattern for integrating UIKit is setting up a variable which will hold a reference to the updated state, and then linking that with existing UIKit or AppKit controls within IBAction.

The sample here is a portion of the code at in a larger ViewController implementation.

import(UIKit)importCombineclassViewController:UIViewController{    @ IBOutletweakvargithub_id_entry:UITextField(1)    varusernameSubscriber:AnyCancellable?    / / username from the github_id_entry field, updated via IBAction    @ Publishedvarusername:String=""(2)    / / github user retrieved from the API publisher. As it's updated, it    / / is "wired" to update UI elements    @ PublishedprivatevargithubUserData:[GithubAPIUser]=[]    / / MARK - Actions    @ IBActionFuncgithubIdChanged(_sender:UITextField){        username=sender.text?""(3)        print("Set username to",(username))    }    OverrideFuncviewDidLoad(){        super.viewDidLoad()        / / Do any additional setup after loading the view.        usernameSubscriber=$username(4)            .throttle(for:0.5,scheduler:myBackgroundQueue,latest:(true))(5)            / / ^^ scheduler myBackGroundQueue publishes resulting elements            / / into that queue, resulting on this processing moving off the            / / main runloop.            .removeDuplicates()( 6)            .print(" username pipeline: ")// debugging output for pipeline            .map{username->AnyPublisher[GithubAPIUser],Never>in(7)                returnGithubAPI.retrieveGithubUser(username:username)            }            / / ^^ type returned in the pipeline is a Publisher, so we use            / / switchToLatest to flatten the values ​​out of that            / / pipeline to return down the chain, rather than returning a            / / publisher down the pipeline.            .switchToLatest()(8)            / / using a sink to get the results from the API search lets us            / / get not only the user, but also any errors attempting to get it.            .receive(on:RunLoop.main)            .assign(to:.githubUserData,on:Self)(9)
(1) The UITextField is the interface element which is driving the updates from user interaction.
(2) We defined aPublishedproperty to both hold the updates. Because its a Published property, it provides a publisher reference that we can use to attach additional combine pipelines to update other variables or elements of the interface. (3) We set the variableusernamefrom within an IBAction, which in turn triggers a data flow if the publisher$ usernamehas any subscribers. (4) We in turn set up a subscriber on the publisher$ usernamethat does further actions . In this case the overall flow retrives an instance of a GithubAPIUser from Github’s REST API. (5) Thethrottleis there to keep from triggering a network request on ever request. The throttle keeps it to a maximum of 1 request every half-second. (6) removeDuplicatesis there to collapse events from the changing username so that API requests aren’t made on rapidly changing values. The removeDuplicates prevents redundant requests from being made, should the user edit and the return the previous value. (7) map is used similiarly to flatMap in error handling here, returning an instance of a publisher. The API object returns a publisher, which this map is invoking. This doesn’t return the value from the call, but the calling publisher itself. (8) switchToLatestoperator takes the instance of the publisher that is the element passed down the pipeline, and pulls out the data to push the elements further down the pipeline. switchToLatest resolves that publisher into a value and passes that value down the pipeline, in this case an instance of[GithubAPIUser]. (9) And assign at the end up the pipeline is the subscriber, which assigns the value into another variable

Cascading UI updates including a network request

Goal
  • Have multiple UI elements update triggered by an upstream subscriber

References
See also
Code and explanation

The example provided expands on a publisher updating fromDeclarative UI updates from user input, adding additional combine pipelines to update multiple UI elements.

The general pattern of this view starts with a textfield that accepts user input:

  1. We using an IBAction to update thePublishedusername variable.

  2. We have a subscriber (usernameSubscriber) attached ($ username) publisher reference, which attempts to retrieve the GitHub user from the API. The resulting variablegithubUserData(alsoPublished) is a list of GitHub user objects. Even though we only expect a single value here, we use a list because we can conveniently return an empty list on failure scenarios: unable to access the API or the username isn’t registered at GitHub.

  3. We have a “secondary” subscriberapiNetworkActivitySubscriberwhich another publisher from the GithubAPI object that provides values ​​when the GithubAPI object starts or finishes making network requests.

  4. We have a another subscriberrepositoryCountSubscriberattached to$ githubUserDatathat pulls the repository count off the github user data object and assigns it as the count to be displayed.

  5. We have a final subscriberavatarViewSubscriberattached to$ githubUserDatathat attempts to retrieve the image associated with the user’s avatar for display.

The empty list is useful to return because when a username is provided that doesn’t resolve, we want to explicitly remove any avatar image that was previously displayed . To do this, we need the pipelines to fully resolve to some value, so that further pipelines are triggered and the relevant UI interfaces updated.

The subscribers (created with (assign) and (sink) ) are stored as AnyCancellable variables on the ViewController instance . Because they are defined on the class instance, the swift compiler creates initializers and deinitializers, which will cancel and clean up the publishers when the class is torn down.

The pipelines have been explicitly configured to work on a background queue using thesubscribeoperator. Without that additional configured, the pipelines would be invoked and run on the main runloop since they were invoked from the UI, which causes a noticable slow-down in responsiveness in the simulator. Likewise when the resulting pipelines assign or update UI elements, thereceiveoperator is used to transfer that work back onto the main runloop.

If you want to have the UI continuously updated from changes propogating throughPublishedproperties, make sure that any configured pipelines have afailure type. This is required for the (assign) operator. But more importantly, it’s a source of bugs when using asinkoperator. If the pipeline from aPublished (variable terminates in a) ************************************** (sink) that accepts an Error failure type, the sink will send a termination signal if an error occures, which stops the pipeline from further processing even when the variable is updated.

importFounationimportCombineenumAPIFailureCondition:Error{    caseinvalidServerResponse}structGithubAPIUser:Decodable{(1)     / / A very * small * subset of the content available about    / / a github API user for example:    / / https://api.github.com/users/heckj    letlogin:String    letpublic_repos:Int    letavatar_url:String}structGithubAPI{(2)    / / NOTE (heckj): I've also seen this kind of API access    / / object set up with with a class and static methods on the class.    / / I don't know that there's a specific benefit to make this a value    / / type / struct with a function on it.    / // externally accessible publsher that indicates that network activity is happening in the API proxy    staticletnetworkActivityPublisher=PassthroughSubjectBool,Never>()(3)    / // creates a one-shot publisher that provides a GithubAPI User    / // object as the end result. This method was specifically designed to    / // return a list of 1 object, as opposed to the object itself to make    / // it easier to distinguish a "no user" result (empty list)    / // representation that could be dealt with more easily in a Combine    / // pipeline than an optional value. The expected return types is a    / // Publisher that returns either an empty list, or a list of one    / // GithubAPUser, and with a failure return type of Never, so it's    / // suitable for recurring pipeline updates working with a @Published    / // data source.    / // - Parameter username: username to be retrieved from the Github API    staticFuncretrieveGithubUser(username:String)->AnyPubl isher[GithubAPIUser],Never>{(4)        ifusername.count3{(5)            returnJust( []).eraseToAnyPublisher()            / / return Publishers.Empty()            / / .eraseToAnyPublisher ()        }        letAssembledURL=String(" https://api.github.com/users/ (username)")        letpublisher=URLSession.shared.dataTaskPublisher(for:URL(string:AssembledURL)!)            .handleEvents(receiveSubscription:{_in(6 )                networkActivityPublisher.send(true)            } ,receiveCompletion:{_in                networkActivityPublisher.send(false)            } ,receiveCancel:{                networkActivityPublisher.send(false)            } )            .tryMap{data,Response->Datain(7)                guardlethttpResponse=ResponseasHTTPURLResponse,                    httpResponse.statusCode==200else{                        throwAPIFailureCondition.invalidServerResponse                }                returndata        }        .decode(type:GithubAPIUser.self,decoder:JSONDecoder())(8)        .map{                [$0](9)        }        .catch{errin(10)            / / return Publishers.Empty()            / / ^^ when I originally wrote this method, I was returning            / / a GithubAPIUser? optional, and then a GithubAPIUser without            / / optional. I ended up converting this to return an empty            / / list as the "error output replacement" so that I could            / / represent that the current value requested didn't * have * a            / / correct github API response. When I was returing a single            / / specific type, using Publishers.Empty was a good way to do a            / / "no data on failure" error capture scenario.            returnJust( [])        }        .eraseToAnyPublisher()(11)        returnpublisher    }}
(1) The decodable struct created here is a subset of what’s returned from the GitHub API. Any pieces not defined in the struct are simply ignored when processed by thedecode (operator.)
(2) The code to interact with the GitHub API was broken out into its own object, which I would normally have in a separate file. The functions on the API struct return publishers, and are then mixed and merged with other pipelines in the ViewController. (3) This struct also exposes a publisher usingpassthroughSubjectthat have set up to trigger Boolean values ​​when it is actively making network requests. (4) I first created the pipelines to return an optional GithubAPIUser instance, but found that there was not a convenient way to propogate “nil” or empty objects on failure conditions. The code was then recreated to return a list, even though only a single instance was ever expected, to conveniently represent an “empty” object. This was important for the use case of wanting to erase existing values ​​in following pipelines reacting to the GithubAPIUser object “disappearing” – removing the repository count and avatar images in this case. (5) The logic here is simply to prevent extraneous network requests, returning an empty result if the username being requested has less than 3 characters. The commented out code is a bit of legacy from when I wanted to return nothing instead of an empty list. (6) the (handleEvents) operator here is how we are triggering updates for the network activity publisher. We define closures that trigger on subscription and finalization (both completion and cancel) that invokesend ()on thepassthroughSubject. This is an example of how we can provide metadata about a pipeline’s operation as a separate publisher. (7) tryMapadds additional checking on the API response from github to convert correct responses from the API that aren’t valid User instances into a pipeline failure condition. (8) Decodetakes the Data from the response and decodes it into a single instance ofGithubAPIUser (9) mapis used to take the single instance and convert it into a list of 1 item, changing the type to a list ofGithubAPIUser:[GithubAPIUser]. 10 (catch) operator captures the error conditions within this pipeline, and returns an empty list on failure while also converting the failure type to (Never) . 11 eraseToAnyPublishercollapses the complex types of all the chained operators and exposes the whole pipeline as an instance ofAnyPublisher.
import(UIKit)importCombineclassViewController:UIViewController{    @ IBOutletweakvargithub_id_entry:UITextField    @ IBOutletweakvaractivityIndicator:UIActivityIndicatorView    @ IBOutletweakvarrepositoryCountLabel:UILabel    @ IBOutletweakvargithubAvatarImageView:UIImageView    varrepositoryCountSubscriber:AnyCancellable?    varavatarViewSubscriber:AnyCancellable?    varusernameSubscriber:AnyCancellable?    varheadingSubscriber:AnyCancellable?    varapiNetworkActivitySubscriber:AnyCancellable?    / / username from the github_id_entry field, updated via IBAction    @ Publishedvarusername:String=""    / / github user retrieved from the API publisher. As it's updated, it    / / is "wired" to update UI elements    @ PublishedprivatevargithubUserData:[GithubAPIUser]=[]    / / publisher reference for this is $ username, of type    varmyBackgroundQueue:DispatchQueue=DispatchQueue(Label:"viewControllerBackgroundQueue")    letcoreLocationProxy=LocationHeadingProxy()    / / MARK - Actions    @ IBActionFuncgithubIdChanged(_sender:UITextField){        username=sender.text?""        print(" Set username to ",username)    }    / / MARK - lifecycle methods    OverrideFuncviewDidLoad(){        super.viewDidLoad()        / / Do any additional setup after loading the view.        letapiActivitySub=GithubAPI.networkActivityPublisher(1)         .receive(on:RunLoop.main)            .sink{doingSomethingNowin                if(doingSomethingNow){                    self.activityIndicator.startAnimating()                }else{                    self.activityIndicator.stopAnimating()                }        }        apiNetworkActivitySubscriber=AnyCancellable(apiActivitySub)        usernameSubscriber=$username(2)            .throttle(for:0.5,scheduler:myBackgroundQueue,latest:(true))            / / ^^ scheduler myBackGroundQueue publishes resulting elements            / / into that queue, resulting on this processing moving off the            / / main runloop.            .removeDuplicates()            .print(" username pipeline: ")// debugging output for pipeline            .map{username->AnyPublisher[GithubAPIUser],Never>in                returnGithubAPI.retrieveGithubUser(username:username)            }            / / ^^ type returned in the pipeline is a Publisher, so we use            / / switchToLatest to flatten the values ​​out of that            / / pipeline to return down the chain, rather than returning a            / / publisher down the pipeline.            .switchToLatest()            / / using a sink to get the results from the API search lets us            / / get not only the user, but also any errors attempting to get it.            .receive(on:RunLoop.main)            .assign(to:.githubUserData,on:Self)        / / using .assign () on the other hand (which returns an        / / AnyCancellable) * DOES * require a Failure type of        repositoryCountSubscriber=$githubUserData(3)            .print(" github user data: ")            .map{userData->Stringin                ifletfirstUser=userData.first{                    returnString(firstUser.public_repos)                }                return" unknown "            }            .receive(on:RunLoop.main)            .assign(to:.text,on:repositoryCountLabel)        letavatarViewSub=$githubUserData(4)            / / When I first wrote this publisher pipeline, the type I was            / / aiming for was, where the value was an            / / optional. The commented out .filter below was to prevent a `nil` // GithubAPIUser object from propogating further and attempting to            / / invoke the dataTaskPublisher which retrieves the avatar image.            / /            / / When I updated the type to be non-optional (            / / Never>) the filter expression was no longer needed, but possibly            / / interesting.            / / .filter ({possibleUser ->Bool in            / / possibleUser!=nil            / /})            / / .print ("avatar image for user") // debugging output            .map{userData->AnyPublisherUIImage,Never>in                guardletfirstUser=userData.firstelse{                    / / my placeholder data being returned below is an empty                    / / UIImage () instance, which simply clears the display.                    / / Your use case may be better served with an explicit                    / / placeholder image in the event of this error condition.                    returnJust(UIImage())eraseToAnyPublisher()                }                returnURLSession.shared.dataTaskPublisher(for:URL(string:firstUser.avatar_url)!)                    / / ^^ this hands back (Data, response) objects                    .handleEvents(receiveSubscription:{_in                        DispatchQueue.main.Async{                            self.activityIndicator.startAnimating()                        }                    } ,receiveCompletion:{_in                        DispatchQueue.main.Async{                            self.activityIndicator.stopAnimating()                        }                    } ,receiveCancel:{                        DispatchQueue.main.Async{                            self.activityIndicator.stopAnimating()                        }                    } )                    .map{$ 0.data}                    / / ^^ pare down to just the Data object                    .map{UIImage(data:$ 0)!}}                    / / ^^ convert Data into a UIImage with its initializer                    .subscribe(on:self.myBackgroundQueue)                    / / ^^ do this work on a background Queue so we don't screw                    / / with the UI responsiveness                    .catch{errin                        returnJust(UIImage())                    }                    / / ^^ deal the failure scenario and return my "replacement"                    / / image for when an avatar image either isn't available or                    / / fails somewhere in the pipeline here.                    .eraseToAnyPublisher()                    / / ^^ match the return type here to the return type defined                    / / in the .map () wrapping this because otherwise the return                    / / type would be terribly complex nested set of generics.            }            .switchToLatest()            / / ^^ Take the returned publisher that's been passed down the chain            / / and "subscribe it out" to the value within in, and then pass            / / that further down.            .subscribe(on:myBackgroundQueue)            / / ^^ do the above processing as well on a background Queue rather            / / than potentially impacting the UI responsiveness            .receive(on:RunLoop.main)            / / ^^ and then switch to receive and process the data on the main            / / queue since we're messin with the UI            .map{image->UIImagein                image            }            / / ^^ this converts from the type UIImage to the type UIImage?            / / which is key to making it work correctly with the .assign ()            / / operator, which must map the type * exactly *            .assign(to:.image,on:Self.githubAvatarImageView)        / / convert the .sink to an `AnyCancellable` object that we have        / / referenced from the implied initializers        avatarViewSubscriber=AnyCancellable(avatarViewSub)        / / KVO publisher of UIKit interface element        let_=repositoryCountLabel.publisher(for:.text)(5)            .sink{someValuein                print(" repositoryCountLabel Updated to (String(describing:someValue))")        }    }}
1) We add a subscriber to our previous controller from that connects notifications of activity from the GithubAPI object to our activity indicator.
(2) Where the username is updated from the IBAction (from our earlier exampleDeclarative UI updates from user input) we have the subscriber make the network request and put the results in a new variable (alsopublished) on our ViewController. (3) The first of two subscribers on the publisher$ githubUserData, this pipeline extracts the count of repositories and updates the a UI label instance. There is a bit of logic in the middle of the pipeline to return the string “unknown” when the list is empty. (4) The second subscriber to the publisher$ githubUserData, this triggers a follow on network request to request the image data for the github avatar. This is a more complex pipeline, extracting the data from the githubUser, assembling a URL, and then requesting it. As this code is in the ViewController, we can also usehandleEventsoperator to trigger updates to the activityIndicator in our view. We usesubscribeto make the requests on a background queue, and laterreceivethe results back onto the main thread to update the UI elements. Thecatchand failure handling returns an emptyUIImageinstance in the event of failure. (5) A final subscriber that doesn’t do anything is attached to the UILabel itself. Any Key-Value Observable object from Foundation can also produce a publisher. In this example, we attach a publisher that triggers a print statement that the UI element was updated.

While we could simply attach pipelines to UI elements as we’re updating them, it more closely couples interactions to the actual UI elements themselves. While easy and direct, it is often a good idea to make explicit state and updates to seperate out actions and data for debugging and understandability. In the example above, we use twopublishedproperties to hold the state associated with the current view. One of which is updated by an IBAction, and the second updated declaratively using a Combine publisher pipeline. All other UI elements are updated publishers hanging from those properties getting updated.


Merging multiple pipelines to update UI elements

Goal
  • Watch and react to multiple UI elements publishing values, and updating the interface based on the combination of values ​​updated.

References
See also
Code and explanation

This example intentionally mimics a lot of web form style validation scenarios, but within UIKit and using Comine.

A view controller is set up with multiple elements to declaratively update. The view controller hosts 3 primary text input fields: * value1 * value2 * value2_repeat a button to submit the combined values, and two labels to provide feedback messages.

The rules of these update that are implemented: * the entry in value1 has to be at least 3 characters * the entry in value2 has to be at least 5 characters * the entry in value2_repeat has to be the same as value2

If any of these rules aren’t met, then we want the submit button to be disabled and relevant messages displayed explaining what needs to be done.

This is achieved by setting up a cascade of pipelines that link and merge together.

  • At the base, there arePublishedproperty matching each of the user input fields.combineLatestis used to take the continually published updates from the value2 properties and merge them into a single pipeline. A (map) operator enforces the rule about characters required and that the values ​​need to be the same. If the values ​​don’t match the required output, we pass a nil value down the pipeline.

  • Another validation pipeline is set up for value1, just using amapoperator to validate the value, or return nil .

  • The logic within the map operators doing the validation is also used to update the label messages in the user interface.

  • A final pipeline usescombineLatestto merge the two validation pipelines into a single pipeline. A subscriber is attached to this combined pipeline to determine if the submission button should be enabled.

The example below shows the various pieces all connected.

import(UIKit)importCombineclassFormViewController:UIViewController{    @ IBOutletweakvarvalue1_input:UITextField    @ IBOutletweakvarvalue2_input:UITextField    @ IBOutletweakvarvalue2_repeat_input:UITextField    @ IBOutletweakvarsubmission_button:UIButton    @ IBOutletweakvarvalue1_message_label:UILabel    @ IBOutletweakvarvalue2_message_label:UILabel    @ IBActionFuncvalue1_updated(_sender:UITextField){(1)        value1=sender.text?""    }    @ IBActionFuncvalue2_updated(_sender:UITextField){        value2=sender.text?""    }    @ IBActionFuncvalue2_repeat_updated(_sender:UITextField){        value2_repeat=sender.text?""    }    @ Publishedvarvalue1:String=""    @ Publishedvarvalue2:String=""    @ Publishedvarvalue2_repeat:String=""    varvalidatedValue1:AnyPublisherString? ,Never>{(2)        return$value1.map{value1in            guardvalue1.count>2else{                DispatchQueue.main.Async{(3)                    self.value1_message_label.text=" minimum of 3 characters required "                }                returnnil            }            DispatchQueue.main.Async{                self.value1_message_label.text=" "            }            returnvalue1        }.eraseToAnyPublisher()    }    varvalidatedValue2:AnyPublisherString? ,Never>{(4)        returnPublishers.CombineLatest($value2,$value2_repeat)            .receive(on:RunLoop.main)(5)            .map{value2,value2_repeatin                guardvalue2_repeat==value2,value2.count>4else{                    self.value2_message_label.text=" values ​​must match and have at least 5 characters "                    returnnil                }                self.value2_message_label.text=" "                returnvalue2            }.eraseToAnyPublisher()    }    varreadyToSubmit:AnyPublisher(String,String)?,Never>{(6)        returnPublishers.CombineLatest(validatedValue2,validatedValue1)            .map{value2,value1in                guardletrealValue2=value2,letrealValue1=value1else{                    returnnil                }                return(realValue2,realValue1)            }            .eraseToAnyPublisher()    }    privatevarcancellableSet:SetAnyCancellable>=[](7)     OverrideFuncviewDidLoad(){        super.viewDidLoad()        self.readyToSubmit            .map{$ 0!=nil}(8)            .receive(on:RunLoop.main)            .assign(to:.isEnabled,on:submission_button)            .store(in:&cancellableSet)(9)    }}
(1) The start of this code follows the same patterns laid out inDeclarative UI updates from user input. IBAction messages are used to update thePublishedproperties, triggering updates to any subscribers attached.
(2) The first of the validation pipelines uses amapoperator to take the string value intput and convert it to nil if it doesn’t match the validation rules. This is also converting the output type from the published property ofto the optional. The same logic is also used to trigger updates to the messages label to provide information about what is required. (3) Since we are updating user interface elements, we explicitly make those updates wrapped inDispatchQueue.main.asyncto invoke on the main thread. (4) combineLatetstakes two publishers and merges them into a single pipeline with an output type that is the combined values ​​of each of the upstream publishers. In this case, the output type is a tuple of(,). (5) Rather than useDispatchQueue.main.async, we can use thereceiveoperator to explicitly run the next operator on the main thread, since it will be doing UI updates. (6) The two vaildation pipelines are combined withcombineLatest, and the output of those checked and merged into a single tuple output. (7) We could store the assignment pipeline as an AnyCancellable? reference to map it to the life of the viewcontroller, but another option is to create something to collect all the cancellable references. This starts as an empty set, and any sinks or assignment subscribers can be added to it to keep a reference to them so that they operate over the full lifetime of the view controller. (8) If any of the values ​​are nil, themapoperator returns nil down the pipeline. Checking against a nil value provides the boolean used to enable (or disable) the submission button. (9) thestoremethod is available on theCancellableprotocol, which is explicitly set up to support saving off references that can be used to cancel a pipeline.

(Creating a repeating publisher by wrapping a delegate based API)

Goal
  • To use one of the Apple delegate APIs to provide values ​​to be used in a combine pipeline.

References
See also
Code and explanation

Where aFuturepublisher is great for wrapping existing code to make a single request, it doesn’t serve as well to make a publisher that produces lengthy, or potentially unbounded, amount of output.

Apple’s UIKit and AppKit APIs have tended to have a object / delegate pattern, where you can opt in to receive any number of different callbacks (often with data ). One such example of that is included within the CoreLocation library, which offers a number of different data sources.

If you want to consume data provided by one of these kinds of APIs within a pipeline, you can wrap the object and usepassthroughSubjectto expose a publisher. The sample code belows shows an example of wrapping CoreLocation’s CLManager object and consuming the data from it through a UIKit view controller.

importFoundationimportCombineimportCoreLocationfinalclassLocationHeadingProxy:NSObject,CLLocationManagerDelegate{    letmgr:CLLocationManager(1)     privateletheadingPublisher:PassthroughSubjectCLHeading,Error>(2)    varpublisher:AnyPublisherCLHeading,Error>(3)    Overrideinit(){        mgr=CLLocationManager()        headingPublisher=PassthroughSubjectCLHeading,Error>()        publisher=headingPublisher.eraseToAnyPublisher()        super.init()        mgr.delegate=self(4)    }    Funcenable(){        mgr.startUpdatingHeading()(5 )    }    Funcdisable(){        mgr.stopUpdatingHeading()    }    / / MARK - delegate methods    / *      * locationManager: didUpdateHeading:      *      * Discussion:      * Invoked when a new heading is available.      * /    FunclocationManager(_Manager:CLLocationManager,didUpdateHeadingnewHeading:CLHeading){        headingPublisher.send(newHeading)(6 )    }    / *      * locationManager: didFailWithError:      * Discussion:      * Invoked when an error has occurred. Error types are defined in "CLError.h".      * /    FunclocationManager(_Manager:CLLocationManager,didFailWithErrorerror:Error){        headingPublisher.send(completion:Subscribers.Completion.(failure)   ((error)))(7 )    }}

(1) CLLocationManageris the heart of what is being wrapped, part of CoreLocation. Because it has additional methods that need to be called for using the framework, I exposed it as a read-only, but public, property. An example of this need is for requesting user permission to use the location API, which the framework exposes as a method on CLLocationManager.
(2) A private instance of PassthroughSubject with the data type we want to publish provides our inside-the-class access to forward data. (3) An the public propertypublisher (exposes the publisher from that subject for external subscriptions.)

(4) The heart of this works by assigning this class as the delegate to the CLLocationManager instance, which is set up at the tail end of initialization . (5) The CoreLocation API doesn’t immediately start sending information. There are methods that need to be called to start (and stop) the data flow, and these are wrapped and exposed on this proxy object. Most publishers are set up to subscribe and drive consumption based on subscription, so this is a bit out of the norm for how a publisher starts generating data. (6) With the delegate defined and the CLLocationManager activated, the data will be provided via callbacks defined on theCLLocationManagerDelegate. We implement the callbacks we want for this wrapped object, and within them we use (passthroughSubject).Send ()to forward the information to any existing subscribers. (7) While not strictly required, the delegate provided an Error reporting callback, so we included that as an example of forwarding an error throughpassthroughSubject.
import(UIKit)importCombineimportCoreLocationclassHeadingViewController:UIViewController{    varheadingSubscriber:AnyCancellable?    letcoreLocationProxy=LocationHeadingProxy()    varheadingBackgroundQueue:DispatchQueue=DispatchQueue(Label:"headingBackgroundQueue")    / / MARK - lifecycle methods    @ IBOutletweakvarpermissionButton:UIButton    @ IBOutletweakvaractivateTrackingSwitch:UISwitch    @ IBOutletweakvarheadingLabel:UILabel    @ IBOutletweakvarlocationPermissionLabel:UILabel    @ IBActionFuncrequestPermission(_sender:UIButton){        print(" requesting corelocation permission ")        let_=FutureInt,Never>{(promise)in(1)            self.coreLocationProxy.mgr.requestWhenInUseAuthorization()            returnpromise(.success(1) )        }        .delay(for:2.0,scheduler:headingBackgroundQueue)(2)        .receive(on:RunLoop.main)        .sink{_in            print(" updating corel ocation permission label ")            self.updatePermissionStatus()(3)        }    }    @ IBActionFunctrackingToggled(_sender:UISwitch){        switchsender.isOn{        casetrue:            self.coreLocationProxy.enable()(4)            print(" Enabling heading tracking ")        casefalse:            self.coreLocationProxy.disable()            print(" Disabling heading tracking ")        }    }    FuncupdatePermissionStatus(){        letx=CLLocationManager.authorizationStatus()        switchx{        case.authorizedWhenInUse:            locationPermissionLabel.text=" Allowed when in use "        case.notDetermined:            locationPermissionLabel.text=" notDetermined "        case.restricted:            locationPermissionLabel.text=" restricted "        case.denied:            locationPermissionLabel.text=" denied "        case.authorizedAlways:            locationPermissionLabel.text=" authorizedAlways "        @ unknowndefault:            locationPermissionLabel.text=" unknown default "        }    }    OverrideFuncviewDidLoad(){        super.viewDidLoad()        / / Do any additional setup after loading the view.        / / request authorization for the corelocation data        self.updatePermissionStatus()        letcorelocationsub=coreLocationProxy            .publisher            .print(" headingSubscriber ")            .receive(on:RunLoop.main)            .sink{someValuein(5)                self.headingLabel.text=String(someValue.trueHeading)        }        headingSubscriber=AnyCancellable(corelocationsub)    }}
1) One of the quirks of CoreLocation is the requirement to ask for permission from the user to access the data. The API provided to initiate this request returns immediately, but provides no detail if the user allowed or denied the request. The CLLocationManager class includes the information, and exposes it as a class method when you want to retrieve it, but there is no information provided to know when, or if, the user has responded to the request. Since the operation doesn’t provide any return, we provide an integer as the pipeline data, primarily to represent that the request has been made.
(2) Since there isn’t a clear way to judge, but the permission is persistent, we simply use adelayoperator before attempting to retrieve the data. This use simply delays the propogation of the value for two seconds. (3) After that delay, we invoke the class method and attempt to update informtion in the interface with the results of the current provided status. (4) Since CoreLocation requires methods to be explicitly enabled or disabled to provide the data, this connects a UISwitch toggle IBAction to the methods exposed on our publisher proxy. (5) The heading data is received in thissinksubscriber, where in this example we simply write it to a text label.

(Responding to updates from NotificationCenter)

Goal
  • Receiving notifications from NotificationCenter as a publisher to declaratively react to the information provided.

References
See also
Code and explanation

A large number of frameworks and user interface components provide information about their state and interactions via Notifications from NotificationCenter. Apple’s documentation includes an article onreceiving and handling events with combinespecifically referencing NotificationCenter .

You can also add your own notifications to your application, and upon sending them may include an additional dictionary in their (userInfo) property. An example of defining your own notification. myExampleNotification:

extensionNotification.Name{    staticletmyExampleNotification=Notification.Name(" an-example-notification ")}

Notification names are structured, and based on Strings. Object references can be passed when a notification is posted to the NotificationCenter, indicating which object sent the notification. Additionally, Notifications may include a (userInfo) , which has a type of[AnyHashable : Any]?. This allows for arbitrary dictionaries, either reference or value typed, to be included with a notification.

letmyUserInfo=["foo":"bar"]letnote=Notification((name):.myExampleNotification,userInfo:myUserInfo)NotificationCenter.default.post(note)

While commonly in use within AppKit and MacOS applications, not all developers are comfortable with heavily using NotificationCenter. Originating within the more dynamic Objective-C runtime, Notifications leverage Any and optional types quite extensively. Using them within swift code, or a pipeline, implies that the pipeline will have to provide the type assertions and deal with any possible errors related to data that may or may not be expected.

When creating the NotificationCenter publisher, you provide the name of the notification for which you want to receive, and optionally an object reference to filter to specific types of objects. A number of AppKit components that are subclasses ofNSControlshare a set of notifications, and filtering can be critical to getting the right notification.

An example of subscribing to AppKit generated notifications:

let(sub)=NotificationCenter.default.publisher(for:NSControl.textDidChangeNotification,(1)                                               object:filterField)(2)    .map{($ 0.objectasNSTextField).stringValue}(3)    .assign(to:MyViewModel.filterString,on:myViewModel)(4)
1) TextFields within AppKit generate textDidChangeNotifications when the values ​​are updated.
(2) A AppKit application can frequently have a large number of text fields that may be changed. Including a reference to the sending control can be used to filter to text changed notifications to which you are specifically interested in responding. (3) themapoperator can be used to get into the object references included with the notification, in this case the.stringValueproperty of the text field that sent the notification, providing it’s updated value (4) The resulting string can be assigned using a writable KeyValue path.

An example of subscribing to your own notifications:

letCancellable=NotificationCenter.default.publisher(for(***********************************************:.myExampleNotification,object(***********************************************:nil)    / / can't use the object parameter to filter on a value reference, only class references, but    / / filtering on 'nil' only constrains to notification name, so value objects * can * be passed    / / in the notification itself.    .sink{receivedNotificationin        print(" passed through: ",receivedNotification)        / / receivedNotification.name        / / receivedNotification.object - object sending the notification (sometimes nil)        / / receivedNotification.userInfo - often nil    }

(SwiftUI Integration)

Using BindableObject with SwiftUI models as a publisher source

Goal
  • SwiftUI includes @Binding and the BindableObject protocol, which provides a publishing source to alerts to model objects changing.

References
  • >

See also
  • >

Code and explanation

Testing and Debugging

The Publisher / Subscriber interface in combine is beautifully suited to be an easily testable interface.

With the composability of Combine, you can use this to your advantage, creating APIs that present, or consume, code that conforms to (Publisher) .

With thepublisher protocolas the key interface, you can replace either side to validate your code in isolation.

For example, if your code was focused on providing it’s data from external web services through Combine, you might make the interface to this conform toAnyPublisher. You could then use that interface to test either side of that pipeline independently.

  • You could mock data responses that emulate the underlying API calls and possible responses, including various error conditions. This might include returning data from a publisher created with (Just) or (Fail) , or something more complex usingFuture. None of these options require you to make actual network interface calls.

  • Likewise you can isolate the testing of making the publisher do the API calls and verify the various success and failure conditions expected.

Testing a publisher with XCTestExpectation

Goal
  • For testing a publisher (and any pipeline attached)

References
See also
Code and explanation

When you are testing a publisher, or something that creates a publisher, you may not have the option of controlling when the publisher returns data for your tests. Combine, being driven by its subscribers, can set up a sync that initiates the data flow. You can use anXCTestExpectationto wait an explicit amount of time for the test to run to completion.

A general pattern for using this with combine includes:

  1. set up the expectation within the test

  2. establish the code you are going to test

  3. set up the code to be invoked such that on the success path you call the expectation’s. ************************************************** (function

  4. set up await ()function with an explicit timeout that will fail the test if the expectation isn ‘t fulfilled within that time window.

If you are testing the data results from a pipeline, then triggering thefulfill ()function within thesinkoperatorreceiveValueclosure can be very convenient . If you are testing a failure condition from the pipeline, then often including (fulfill ()within thesinkoperatorreceiveCompletionclosure is effective.

The following example shows testing a one-shot publisher (URLSession.dataTaskPublisherin this case) using expectation, and expecting the data to flow without an error.

FUNCtestDataTaskPublisher(){        / / setup        letexpectation=XCTestExpectation(description:" Download from (String(describing:testURL))")(1)        letremoteDataPublisher=URLSession.shared.dataTaskPublisher(for:Self.testURL!)            / / validate            .sink(receiveCompletion:{finiin                print(" .sink () received the completion ",String(describing:(FINI)))                switchfini{                case.finished:expectation.fulfill()(2)                case.failure:XCTFail()(3)                }            } ,receiveValue:{(data,response)in                guardlethttpResponse=ResponseasHTTPURLResponseelse{                    XCTFail(" Unable to parse response an HTTPURLResponse ")                    return                }                XCTAssertNotNil(data)                / / print (". sink () data received  (data)")                XCTAssertNotNil(httpResponse)                XCTAssertEqual(httpResponse.statusCode,200)(4)                / / print (". sink () httpResponse received  (httpResponse)")            } )        XCTAssertNotNil(remoteDataPublisher)        wait(for:[expectation],timeout:(5.0))(5)    }
1) The expectation is set up with a string that makes debugging in the event of failure a bit easier. This string is really only seen when a test failure occurs. The code we are testing here is dataTaskPublisher retrieving data from a preset test URL, defined earlier in the test. The publisher is invoked by attaching thesinksubscriber to it. Without the expectation, the code will still run, but the test running structure wouldn’t wait to see if there were any exceptions. The expectation within the test “holds the test” waiting for a response to let the operators do their work.
(2) In this case, the test is expected to complete successfully and terminate normally, therefore thetheexpectation.fulfill ()invocation is set within the receiveCompletion closure, specifically linked to a received.FinishedCompletion. (3) Since we don’t expect a failure, we also have an explicit XCTFail () invocation if we receive a.FailureCompletion. (4) We have a few additional assertions within the receiveValue. Since this publisher set returns a single value and then terminates, we can make easily make inline assertions about the data received. If we received multiple values, then we could collect those and make assertions on what was received after the fact. (5) This test uses a single expectation, but you can include multiple independent expectations to require fulfillment. It also sets that maximum time that this test can run to five seconds. The test will not always take five seconds, as it will complete the test as soon as the fulfill is received.

Testing a subscriber with a PassthroughSubject

Goal
  • For testing a subscriber, or something that includes a subscriber, we can emulate the publishing source with PassthroughSubject to provide explicit control of what data gets sent and when .

References
See also
Code and explanation

When you are testing a subscriber in isolation, you can often get more fine-grained control of your tests by emulating the publisher with apassthroughSubjectand using the associated.Send ()method to trigger controlled updates.

This pattern relies on the subscriber setting up the initial part of the publisher-subscriber lifecycle upon construction, and leaving the code to stand waiting until data is provided . With a PassthroughSubject, sending the data to trigger the pipeline and subscriber closures, or following state changes that can be verified, is at the control of the test code itself.

This kind of testing pattern also works well when you are testing the response of the subscriber to a failure, which might otherwise terminate a subscription.

A general pattern for using this kind of test construct is:

  1. set up your subscriber and any pipeline leading to it that you want to include within the test

  2. create a PassthroughSubject in the test that produces a output type and failure type to match with your subscriber.

  3. assert any initial values ​​or preconditions

  4. send a the data through the subject

  5. test the results of having sent the data – either directly or asserting on state changes that were expected

  6. send additional data if desired

  7. test further evolution of state or other changes.

An example of this pattern follows:

FUNCtestSinkReceiveDataThenError(){    / / setup - preconditions(1)    letexpectedValues=["firstStringValue","secondStringValue"]    enumtestFailureCondition:Error{        caseanErrorExample    }    varcountValuesReceived=0    varcountCompletionsReceived=0    / / setup    letsimplePublisher=PassthroughSubjectString,Error>( ()(2)
    let_=simplePublisher(3)        .sink(receiveCompletion:{completionin            countCompletionsReceived=1            switchcompletion{(4)            case.finished:                print(" .sink () received the completion: ",String(describing:completion))                / / no associated data, but you can react to knowing the                / / request has been completed                XCTFail(" We should never receive the completion, the error should happen first ")                break            case.failure(letanError) :                / / do what you want with the error details, presenting,                / / logging, or hiding as appropriate                print(" received the error: ",anError)                XCTAssertEqual(anError.localizedDescription,                               testFailureCondition.anErrorExample.localizedDescription)(5)                break            }        } ,receiveValue:{someValuein( 6)            / / do what you want with the resulting value passed down            / / be aware that depending on the data type being returned,            / / you may get this closure invoked multiple times.            XCTAssertNotNil(someValue)            XCTAssertTrue(expectedValues.contains(someValue) )            countValuesReceived=1            print(" .sink () received (someValue)")        } )    / / validate    XCTAssertEqual(countValuesReceived,0)( 7)    XCTAssertEqual(countCompletionsReceived,0)    simplePublisher.send(" firstStringValue ")(8)    XCTAssertEqual(countValuesReceived,1)    XCTAssertEqual(countCompletionsReceived,0)    simplePublisher.send(" secondStringValue ")    XCTAssertEqual(countValuesReceived,2)    XCTAssertEqual(countCompletionsReceived,0)    simplePublisher.send(completion:Subscribers.Completion.failure(testFailureCondition.anErrorExample))(9)    XCTAssertEqual(countValuesReceived,2)    XCTAssertEqual(countCompletionsReceived,1)    / / this data will never be seen by anything in the pipeline above because we've already sent a completion    simplePublisher.send(completion:Subscribers.Completion.finished)(10)    XCTAssertEqual(countValuesReceived,2)    XCTAssertEqual(countCompletionsReceived,1)}
(1) This test sets up some variables to capture and modify during test execution that we use to validate when and how the sink code operates. Additionally, we have an error definition defined here because it’s not coming from other code elsewhere.
(2) The setup for this code uses thepassthroughSubjectto drive the test, but the code we’re interested in testing is really the subscriber. (3) The subscriber setup under test (in this case, a standard (sink) ). We have code paths that trigger on receiving data and completions. (4) Within the completion path, we switch on the type of completion, adding an assertion that will fail the test if a finish is called, as we expect to only generate a. failureCompletion. (5) I find testing error equality in swift to be awkward, but if the error is code you are controller, you can sometimes use thelocalizedDescriptionas a convenient way to test the type of error received. (6) ThereceiveValueclosure is more complex in how it asserts its values. Since we are receiving multiple values ​​in the process of this test, we have some additional logic to simply check that the values ​​are within the set that we send. Like the completion handler, We also increment test specific variables that we will assert on later to validate state and order of operation. (7) The count variables are validated as preconditions before we send any data to double check our assumptions. (8) In the test, thesend ()triggers the actions, and immediately after we can test the side effects through the test variables we are updating. In your own code, you may not be able to (or want to) modify your subscriber, but you may be able to provide private / testable properties or windows into the objects to validate them in a similiar fashion. (9) We also usesend ()to trigger a completion, in this case a failure completion. 10 And the finalsend ()is simply validating the operation of the failure that just happened – that it wasn’t processed, and no further state updates happened.

Testing a subscriber with scheduled sends from PassthroughSubject

Goal
  • For testing a pipeline, or subscriber, when part of what you want to test is the timing of the pipeline.

References
See also
Code and explanation

There are a number of operators in Combine that are specific to the timing of data, including (debounce) , (throttle, anddelay. You may want to test that your pipeline timing is having the desired impact, indepedently of doing UI testing.

An example of this:

FUNCtestKVOPublisher(){    letexpectation=XCTestExpectation(description:self.debug Description)    letfoo=KVOAbleNSObject()    letq=DispatchQueue(Label:self.debugDescription)(1)    let_=foo.publisher(for:.intValue)        .print()        .sink{someValuein            print(" value of intValue updated to:>> (someValue))        }    q.asyncAfter(Deadline:.now()(0.5),(execute):{(2)        print(" Updating to foo.intValue on background queue ")        foo.intValue=5        expectation.fulfill()(3)    } )    wait(for:[expectation],timeout:(5.0))(4)}
1) This adds a DispatchQueue to your test, conveniently naming the queue after the test itself. This really only shows when debugging test failures, and is convenient as a reminder of what’s happening in the test code vs. any other background queues that might be in use.
(2) .asyncAfteris used along with the deadline parameter to define when a call gets made . (3) The simplest form embeds any relevant assertions into the subscriber or around the subscriber. Additionally, invoking the.Fulfill ()on your expectation as the last queued entry you send lets the test know that it is now complete. (4) Make sure that when you set up the wait that allow for sufficient time for your queue’d calls to be invoked.

A definite downside to this technique is that it forces the test to take a minimum amount of time matching the maximum queue delay in the test.

Another option is a 3rd party library named EntwineTest, which was inspired by the RxTest library. EntwineTest is part of Entwine, a swift library that expands on Combine with some helpers. The library can be found on Github athttps://github.com/tcldr/Entwine.git, available under the MIT license.

One of the key elements included in EnwtineTest is a virtual time scheduler, as well as additional classes that schedule (TestablePublisher) and collect and record (TestableSubscriber) the timing of results while using this scheduler.

An example of this from the EntwineTest project README is included:

FUNCtestExampleUsingVirtualTimeScheduler(){    letscheduler=TestScheduler(initialClock:0)(1)    vardidSink=false    letCancellable=Just(1)(2)        .delay(for:1,scheduler:scheduler)        .sink{_in            didSink=true        }    XCTAssertNotNil(Cancellable)    / / where a real scheduler would have triggered when .sink () was invoked    / / the virtual time scheduler requires resume () to commence and runs to    / / completion.    scheduler.resume()(3)    XCTAssertTrue(didSink)(4)}
1) Using the virtual time scheduler requires you create one at the start of the test, initializing it’s clock to a starting value. The virtual time scheduler in EntwineTest will commence subscription at the value200and times out at900if the pipeline isn’t complete by that time.
(2) You create your pipeline, along with any publishers or subscribers, as normal. EntwineTest also offers a testable publisher and a testable subscriber that could be used as well. For more details on these parts of EntwineTest, seeUsing EntwineTest to create a testable publisher and subscriber. (3) .Resume ()needs to be invoked on the virtual time scheduler to commence its operation and run the pipeline. (4) Assert against expected end results after the pipeline has run to completion.

Using EntwineTest to create a testable publisher and subscriber

Goal
  • For testing a pipeline, or subscriber, when part of what you want to test is the timing of the pipeline.

References
See also
Code and explanation

The EntwineTest library, available from Gitub athttps://github.com/tcldr/Entwine.git, provides some additional options for making your pipelines testable. In addition to a virtual time scheduler, EntwineTest has a TestablePublisher and a TestableSubscriber. These work in coordination with the virtual time scheduler to allow you to specify the timing of the publisher generating data, and to valid the data received by the subscriber.

An example of this from the EntwineTest project is included:

importXCTestimportEntwineTest/ / library loaded from https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md/ / as a swift package https://github.com/tcldr/Entwine.git: 0.6.0, Next Major VersionclassEntwineTestExampleTests:XCTestCase{    FunctestMap(){        lettestScheduler=TestScheduler(initialClock:0)        / / creates a publisher that will schedule it's elements relatively, at the point of subscription        lettestablePublisher:TestablePublisherString,Never>=(testScheduler.createRelativeTestablePublisher([(1)         (100,.input("a")),         (200,.input("b")),         (300,.input("c")),     ])        / / a publisher that maps strings to uppercase        letsubjectUnderTest=testablePublisher.map{$ 0.uppercased()}        / / uses the method described above (schedules a subscription at 200, to be cancelled at 900)        letresults=testScheduler.start{subjectUnderTest}(2)        XCTAssertEqual(results.recordedOutput,[(3)         (200,.subscription),       // subscribed at 200         (300,.input("A")),         // received uppercased input @ 100   subscription time         (400,.input("B")),         // received uppercased input @ 200   subscription time         (500,.input("C")),         // received uppercased input @ 300   subscription time     ])    }}
1) The TestablePublisher lets you set up a publisher that returns specific values ​​at specific times. In this case, it’s returning 3 items at consistent intervals.
(2) When you use the virtual time scheduler, it is important to make sure to invoke it with start. This runs the virtual time scheduler, which can run faster than a clock since it only needs to increment the virtual time and not wait for elapsed time. (3) resultsis a TestableSubscriber object, and includes arecordedOutputproperty which provides an ordered list of all the data and combine control path interactions with their timing.

If this test sequence had been done with asyncAfter, then the test would have taken a minimum of 500 ms to complete. When I ran this test on my laptop, it was recording 0. (seconds to complete the test) 12. 1ms).


Debugging pipelines with the print operator

Goal
  • To gain understanding of what is happening in a pipeline, seeing all control and data interactions.

References
See also
Code and explanation

I have found the greatest detail of information comes from selectively using the (print) operator. The downside is that it prints quite a lot of information, so the output can become quickly overwhelming. For understanding a simple pipeline, using the. print ()as an operator without any parameters is very straightforward. As soon as you want to add more than one print operator, you will likely want to use the string parameter, which is puts in as a prefix to the output.

The two pipelines cascade together by connecting through a private published variable – the github user data. The two relevant pipelines from that example code:

usernameSubscriber=$username    .throttle(for:0.5,scheduler:myBackgroundQueue,latest:(true))    / / ^^ scheduler myBackGroundQueue publishes resulting elements    / / into that queue, resulting on this processing moving off the    / / main runloop.    .removeDuplicates()    .print(" username pipeline: ")// debugging output for pipeline    .map{username->AnyPublisher[GithubAPIUser],Never>in        returnGithubAPI.retrieveGithubUser(username:username)    }    / / ^^ type returned in the pipeline is a Publisher, so we use    / / switchToLatest to flatten the values ​​out of that    / / pipeline to return down the chain, rather than returning a    / / publisher down the pipeline.    .switchToLatest()    / / using a sink to get the results from the API search lets us    / / get not only the user, but also any errors attempting to get it.    .receive(on:RunLoop.main)    .assign(to:.githubUserData,on:Self)/ / using .assign () on the other hand (which returns an/ / AnyCancellable) * DOES * require a Failure type ofrepositoryCountSubscriber=$githubUserData    .print(" github user data: ")    .map{userData->Stringin        ifletfirstUser=userData.first{            returnString(firstUser.public_repos)        }        return" unknown "    }    .receive(on:RunLoop.main)    .assign(to:.text,on:repositoryCountLabel)

When you run the UIKit-Combine example code, the terminal shows the following output as I slowly enter the username (heckj) . In the course of doing these lookups, two other github accounts are found and retrieved (HECandheck) before the final one.

interactive output from simulator

username pipeline:: receive subscription: (RemoveDuplicates) username pipeline :: request unlimited github user data:: receive subscription: (CurrentValueSubject) github user data:: request unlimited github user data:: receive value: ([]) username pipeline:: receive value: () github user data:: receive value: ([])  Set username to h username pipeline:: receive value: (h) github user data:: receive value: ([])  Set username to he username pipeline :: receive value: (he) github user data:: receive value: ([])  Set username to hec username pipeline:: receive value: (hec)  Set username to heck github user data:: receive value: ([UIKit_Combine.GithubAPIUser(login: "hec", public_repos: 3, avatar_url: "https://avatars3.githubusercontent.com/u/53656?v=4")])  username pipeline:: receive value: (heck) github user data:: receive value: ([UIKit_Combine.GithubAPIUser(login: "heck", public_repos: 6, avatar_url: "https://avatars3.githubusercontent.com/u/138508?v=4")])  Set username to heckj username pipeline:: receive value: (heckj) github user data:: receive value: ([UIKit_Combine.GithubAPIUser(login: "heckj", public_repos: 69, avatar_url: "https://avatars0.githubusercontent.com/u/43388?v=4")])

I have removed some of the other extraneous print statements, which I also placed in (sink) closures to see final results.

As you can see, you see the initial subscription setup at the very beginning, and then notifications, including the debug representation of the value passed through the print operator. Although it is not shown in the example content above, you will also see cancellations when an error occurs, or completions when they emit from a publisher reporting no further data is available.

It can also be beneficial to use a print operator on either side of an operator to understand how it is operating.

An example of doing this, leveraging the prefix to show theRetry operator and how it works:

FUNCtestRetryWithOneShotFailPublisher(){    / / setup    let_=Fail(outputType:String.self,failure:testFailureCondition.invalidServerResponse)        .print(" (1)>")(1)        .retry(3)        .print(" (2)>")(2)        .sink(receiveCompletion:{finiin            print(" ** .sink () received the completion: ",String(describing:FINI))        } ,receiveValue:{stringValuein            XCTAssertNotNil(stringValue)            print(" ** .sink () received (stringValue)")        } )}
1) The(1)prefix is ​​to show the interactions above the retry operator
(2) The(2)prefix shows the interactions after the retry operator

output from unit test

Test Suite 'Selected tests' started at 2019 - 07 - 26 (*****************************************************************************************************************************************************************************************************: 59: 48. 042 Test Suite 'UsingCombineTests.xctest' started at 2019 - 07 - 26 (*****************************************************************************************************************************************************************************************************: 59: 48. 043 Test Suite 'RetryPublisherTests' started at 2019 - 07 - 26 (*****************************************************************************************************************************************************************************************************: 59: 48. 043 Test Case '- [UsingCombineTests.RetryPublisherTests testRetryWithOneShotFailPublisher]' started. (1)>: receive subscription: (Empty)(1)(1)>: receive error: (invalidServerResponse) (1)>: receive subscription: (Empty) (1)>: receive error: (invalidServerResponse) (1)>: receive subscription: (Empty) (1)>: receive error: (invalidServerResponse) (1)>: receive subscription: (Empty) (1)>: receive error: (invalidServerResponse) (2)>: receive error: (invalidServerResponse)(2) ** .sink () received the completion: failure (UsingCombineTests.RetryPublisherTests.testFailureCondition.invalidServerResponse) (2)>: receive subscription: (Retry) (2)>: request unlimited (2)>: receive cancel Test Case '- [UsingCombineTests.RetryPublisherTests testRetryWithOneShotFailPublisher]' passed (0.0  (seconds). Test Suite 'RetryPublisherTests' passed at 2019 - 07 - 26 (*****************************************************************************************************************************************************************************************************: 59: 48. 054. Executed 1 test, with 0 failures (0 unexpected) in 0.0 10 (0.0 11) seconds Test Suite 'UsingCombineTests.xctest' passed at 2019 - 07 - 26 (*****************************************************************************************************************************************************************************************************: 59: 48. 054. Executed 1 test, with 0 failures (0 unexpected) in 0.0 10 (0.0 11) seconds Test Suite 'Selected tests' passed at 2019 - 07 - 26 15: 59: 48. 057. Executed 1 test, with 0 failures (0 unexpected) in 0.0 10 (0.0 15) seconds
1) In the test sample, the publisher always reports a failure, resulting in seeing the prefix(1)receiving the error, and then the resubscription from the retry operator.
(2) And after 4 of those attempts (3 “retries”), then you see the error falling through the pipeline. After the error hits the sink, you see thecancelsignal propogated back up, which stops at the retry operator.

While very effective, the print operator can be a blunt tool, generating a lot of output that you have to parse and review. If you want to be more selective with what you identify and print, or if you need to process the data passing through for it to be used more meaningfully, then you look at thehandleEventsoperator. More detail on how to use this opeartor for debugging is inDebugging pipelines with the handleEvents operator.


Debugging pipelines with the handleEvents operator

Goal
  • To get more targettted understanding of what is happening within a pipeline, employing breakpoints, print or logging statements, or additional logic.

References
See also
Code and explanation

handleEventspasses data through, making no modifications to the output and failure types, or the data. When you put in the operator, you can specify a number of optional closures, allowing you to focus on the aspect of what you want to see. ThehandleEventsoperator with specific closures can be a great way to get a window to see what is happening when a pipeline is cancelling, erroring, or otherwise terminating expectedly.

The closures you can provide include:

  • receiveSubscription

  • receiveRequest

  • receiveCancel

  • receiveOutput

  • receiveCompletion

The power of handleEvents for debugging is in selecting what you want to view, reducing the amount of output, or manipulating the data to get a better understanding of it.

In the example viewcontroller atUIKit-Combine / GithubViewController.swift, the subscription, cancellation, and completion handlers are used to provide a side effect of starting, or stopping, an activity indicator.

If you only wanted to see the data being pass ed on the pipeline, and didn’t care about the control messages, then providing a single closure for receiveOutput and ignoring the other closures can let you focus in on just that detail.

The unit test example showing handleEvents has each active with comments:

.handleEvents(receiveSubscription:{aValuein    print(" receiveSubscription event called with (String(describing:aValue))"")(2)} ,receiveOutput:{aValuein(3)    print(" receiveOutput was invoked with (String(describing:aValue))")} ,receiveCompletion:{aValuein(4)    print(" receiveCompletion event called with (String(describing:aValue))")} ,receiveCancel:{(5 )    print(" receiveCancel event invoked ")} ,receiveRequest:{aValuein(1)    print(" receiveRequest event called with (String(describing:aValue))")} )
1) The first closure called isreceiveRequest, which will have the demand value passed into it .
(2) The second closurereceiveSubscriptionis commonly the returning subscription from the publisher, which passes in a reference to the publisher. At this point, the pipeline is operational, and the publisher will provide data based on the amount of data requested in the original request. (3) This data is passed intoreceiveOutputas the publisher makes it available, invoking the closure for each value passed. This will repeat for as many values ​​as the publisher sends. (4) If the pipeline is closed – either normally or terminated due to a failure – thereceiveCompletionclosure will get the completion. Just the like thesinkclosure, you can switch on the completion provided, and if it is A.Failurecompletion, then you can inspect the enclosed error. (5) If the pipeline is cancelled, then thereceiveCancelclosure will be called. No data is passed into the cancellation closure.

While you can also useBreakpointandbreakpointOnErroroperators to break into a debugger (as shown inDebugging pipelines with the debugger), the handleEvents () operator with closures allows you to set breakpoints within Xcode. This allows you to immediately jump into the debugger to inspect the data flowing through the pipeline, or to get references to the subscriber, or the error in the case of a failed completion.


Debugging pipelines with the debugger

Goal
  • To force the pipeline to trap into a debugger on specific scenarios or conditions.

References
See also
Code and explanation

You can easily set a breakpoint within any closure to any operator within a pipeline, triggering the debugger to activate to inspect the data. Since themapoperator is frequently used for simple output type conversions, it is often an excellent candidate that has a closure you can use. If you want to see into the control messages, then a breakpoint within any of the closures provided tohandleEventsmakes a very convenient target.

You can also use theBreakpointoperator to trigger the debugger, which can be a very quick and convenient way to see what is happening in a pipeline. The breakpoint operator acts very much like handleEvents, taking a number of optional parameters, closures that are expected to return a boolean, and if true will invoke the debugger.

The optional closures include:

  • receiveSubscription

  • receiveOutput

  • receiveCompletion

.Breakpoint(receiveSubscription:{subscriptionin    returnfalse/ / return true to throw SIGTRAP and invoke the debugger} ,receiveOutput:{valuein    returnfalse/ / return true to throw SIGTRAP and invoke the debugger} ,receiveCompletion:{completionin    returnfalse/ / return true to throw SIGTRAP and invoke the debugger} )

This allows you to provide logic to evaluate the data being passed through, and only triggering a breakpoint when your specific conditions are met. With very active pipelines processing a lot of data, this can be a great tool to be more surgical in getting the debugger active when you need it, and letting the other data move on by.

If you are only interested in the breaking into the debugger on error conditions, then convenience operatorbreakPointOnErroris perfect. It takes no parameters or closures, simply invoking the debugger when an error condition of any form is passed through the pipeline.

The location of the breakpoint that is triggered by the breakpoint operator isn’t in your code, so getting to local frames and information can be a bit tricky . This does allow you to inspect global application state in highly specific instances (whenever the closure returns (true) , with logic you provide), but you may find it more effective to use regular breakpoints within closures . The breakpoint () and breakpointOnError () operators don’t immediately drop you into a closure where you can see the data being passed, error thrown, or control signals that may have triggered the breakpoint. You can often walk back up the stack trace within the debugging window to see the publisher.

When you trigger a breakpoint within an operator’s closure, the debugger immediately gets the context of that closure as well, so you can see / inspect the data being passed.



Brave Browser
(Read More)
Payeer

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

The history of celebrity: from Bernhardt to the Kardashians, Hacker News

The history of celebrity: from Bernhardt to the Kardashians, Hacker News

Modi’s goal of a $ 5 trillion economy by 2025 is at risk – Economic Times, The Times of India

Modi’s goal of a $ 5 trillion economy by 2025 is at risk – Economic Times, The Times of India