Skip to Content
Personal Insights

How to use the new SAPFoundation Combine Publishers

The SCP SDK for iOS version 5.1.2 brings 2 new API on the SAPFoundation framework, combine publishers for URLSession.

I will show you how to use these new API to download images (also refer to media type or stream) from your backend.

Prerequisites:

  • A SAP Gateway service with a media entity exposing an image

 

Let’s start!

First, we will create an extension on SAPURLSession, this extension could be downloaded here:

let’s define our own API errors inside the extension

enum SAPURLError: Error {
    case requestFailed(URLError)
    case redirection(HTTPURLResponse)
    case client(HTTPURLResponse, ODataErrorPayload?)
    case server(HTTPURLResponse)
    case unknown
        
    init(_ error: Error) {
        switch error {
        case is URLError:
            self = .requestFailed(error as! URLError)
        case is SAPURLError:
            self = error as! SAPURLError
        default:
            self = .unknown
        }
     }
}

 

The requestFailed error are Apple URLSession API errors (examples include request with bad URI, request timeout, no network ….)

The redirection error are for all HTTP 3XX

The client error are for all HTTP 4XX

The server error are for all HTTP 5XX

and the initializer is to downcast a regular swift error to this error type, this is because when throwing an error in Swift we loose it’s type

 

 

Define our 2 combine publishers API for downloading an image

func getImage(for url: URL) -> AnyPublisher<UIImage, SAPURLError> {
    dataTaskPublisher(for: url)
        .tryMap(mapResponseToImage)
        .mapError(SAPURLError.init)
        .eraseToAnyPublisher()
}
    
func getImage(for request: URLRequest) -> AnyPublisher<UIImage, SAPURLError> {
    dataTaskPublisher(for: request)
        .tryMap(mapResponseToImage)
        .mapError(SAPURLError.init)
        .eraseToAnyPublisher()
}

 

 

Parse the response

func mapResponseToImage(transform: (data: Data, response: URLResponse)) throws -> UIImage {
    guard let httpResponse = transform.response as? HTTPURLResponse else {
        throw SAPURLError.unknown
    }

    // 2XX Success
    if 200...299 ~= httpResponse.statusCode {
        return UIImage(data: transform.data)!
         
    // 3XX Redirection
    } else if 300...399 ~= httpResponse.statusCode {
        throw SAPURLError.redirection(httpResponse)
        
    // 4XX Client Errors
    } else if 400...499 ~= httpResponse.statusCode {
        throw SAPURLError.client(httpResponse, decodeResponse(data: transform.data))
           
    // 5XX Server Errors
    } else if 500...599 ~= httpResponse.statusCode {
        throw SAPURLError.server(httpResponse)
            
    } else {
        throw SAPURLError.unknown
    }
 }

 

this is pretty straightforward error mapping, but notice for the 4XX client error, we are decoding the response …

SAP Gateway provides additional information about the error, usually for HTTP 400 or 404, your backend developer will raise an exception in case of an error and we need to be able to decode the response to get this text

how to raise exception in ABAP for returning an HTTP 404 (Not Found)

RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
  EXPORTING
    textid           = /iwbep/cx_mgw_busi_exception=>business_error
    http_status_code = /iwbep/cx_mgw_busi_exception=>gcs_http_status_codes-not_found
    message          = 'Image ABC not found'.

 

 

How to parse the response in case of an error (again mostly for HTTP 400 & 404)

func decodeResponse(data: Data) -> ODataErrorPayload? {
    try? JSONDecoder().decode(ODataErrorPayload.self, from: data)
}

The SAP Gateway error payload is always structured the same way, you will need this structure declaration added to your project, please download it from here

 

 

Where can you access the SAPURLSession?

The SAPURLSession could be access from your onboarding session which is hold by the session manager and the session manager could be access from the shared singleton or your app delegate.

Example:

 guard let onbardingSession = OnboardingSessionManager.shared.onboardingSession else { return }

 

 

How do you build your media stream URL ?

You need to retrieve the backend URL first then append your service destination name and your endpoint path to it

Example:

guard let settingsParameters = onboardingSession.settingsParameters else { fatalError() }

let imagePath = "MyMediaEntityset(\(myKey))/$value"

let url = settingsParameters.backendURL.appendingPathComponent("MyDestination").appendingPathComponent(imagePath)

 

 

Where should you encapsulate this code for reusability?

You can do it directly in your OnboardingSession or as an extension on the OnbardingSessionManager class itself

Example of downloading an article image from an extension on the OnbardingSessionManager class

func getArticleImage(for articleNumber: String) -> AnyPublisher<UIImage, SAPURLSession.SAPURLError> {
    guard let onboardingSession = onboardingSession,
        let settingsParameters = onboardingSession.settingsParameters else { fatalError() }

    let imagePath = "ArticleImageSet(\(articleNumber))/$value"

    let url = settingsParameters.backendURL.appendingPathComponent("MyDestination").appendingPathComponent(imagePath)

    return onboardingSession.sapURLSession.getImage(for: url)
}

If you are downloading multiple images from a table view or collection view controller, you’re probably not interested in the error of each individual download image, we can replace the error with a placeholder image and we can create a side effects based on what’s happening in the pipeline. We will used the handleEvents operator to log our request that have failed.

func getArticleImage1(for articleNumber: String) -> AnyPublisher<UIImage, Never> {
    guard let onboardingSession = onboardingSession,
        let settingsParameters = onboardingSession.settingsParameters else { fatalError() }

    let imagePath = "ArticleImageSet(\(articleNumber))/$value"

    let url = settingsParameters.backendURL.appendingPathComponent(“MyDestination”).appendingPathComponent(imagePath)

    return onboardingSession.sapURLSession.getImage(for: url)
        .replaceError(with: UIImage(named: "PlaceholderImage")!)
        .handleEvents(receiveCompletion: { completion in
            if case .failure(let error) = completion {
                self.logger.warn("An error occured while downloading an image for article \(articleNumber), error: ", error: error)
            }
          })
          .eraseToAnyPublisher()
}

 

you could access the session manager with the shared singleton and call your API like this:

OnboardingSessionManager.shared.getArticleImage(for: ... )
    .sink( ... )

 

 

How to cache your images?

If you need to cache your images for offline behaviour or you want to avoid re-downloading the same images frequently, you could save the images on disk and have your own purge mechanism, but I found it easier to control the cache on your SAP Gateway service.

Let say you would like to cache your images for 1 day on your iOS device …

In SAP Gateway, you will redefine your service /iwbep/if_mgw_appl_srv_runtime~get_stream method and set the cache policy on the response export parameter

es_response_context-max_age = 60 * 60 * 24. "24h cache
es_response_context-do_not_cache_on_client = /iwbep/if_mgw_appl_types=>gcs_cache_on_client-can_be_cached.

you could then call the same getImage API we just created in offline mode and it will returned you your image from the URLSession local cache.

 

 

Defining your own error description

Create an extension on SAPURLError, conform to the LocalizedError protocol and define your own error messages. There is an example on the first GIST link.

 

How to call this API from UIKit or SwiftUI?

For UIKit app development, you can subscribe to this API by calling the sink method, but don’t forget to keep a reference of the subscription.

For SwiftUI, you could encapsulate this publisher inside a view model class (conforming to the Observable protocol), download the image an assign it to a published property

or you can hold the publisher directly in your SwiftUI view, and with the view modifier OnReceive, subscribe and receive to the downloaded image

 

happy coding!

 

Alex

Be the first to leave a comment
You must be Logged on to comment or reply to a post.