I would like to print the url of my Apollo iOS Client GraphQL queries to the Xcode console when the query is called.
You don't need to write swift code to extract the QueryName.
Use Proxyman, like Charles Proxy. It will display the QueryName on the column by default.
Ref: https://docs.proxyman.io/advanced-features/graphql
Per the Apollo iOS Client docs, a logging interceptor can be added in a custom Interceptor Provider.
I created a custom interceptor provider using the code from DefaultInterceptorProvider, and included the logging interceptor.
import Apollo
class InterceptorProviderWithLogging: InterceptorProvider {
private let client: URLSessionClient
private let store: ApolloStore
private let shouldInvalidateClientOnDeinit: Bool
public init(client: URLSessionClient = URLSessionClient(),
shouldInvalidateClientOnDeinit: Bool = true,
store: ApolloStore) {
self.client = client
self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit
self.store = store
}
deinit {
if self.shouldInvalidateClientOnDeinit {
self.client.invalidate()
}
}
open func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
CacheReadInterceptor(store: self.store),
RequestLoggingInterceptor(), // added logging interceptor
NetworkFetchInterceptor(client: self.client),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store),
]
}
open func additionalErrorInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> ApolloErrorInterceptor? {
return nil
}
}
class RequestLoggingInterceptor: ApolloInterceptor {
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: #escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
if let url = try? request.toURLRequest().url?.absoluteString.removingPercentEncoding {
if let variables = request.operation.variables {
print("\(request.operation.operationName) parameters: \(variables) \(url)")
} else {
print("\(request.operation.operationName) \(url)")
}
}
chain.proceedAsync(request: request, response: response, completion: completion)
}
}
I use the custom interceptor provider in the Request Chain Network Transport.
private(set) lazy var apolloClient: ApolloClient = {
let store = ApolloStore()
let interceptorProvider = InterceptorProviderWithLogging(store: store)
let requestChainTransport = RequestChainNetworkTransport(
interceptorProvider: interceptorProvider,
endpointURL: url,
additionalHeaders: [:],
autoPersistQueries: false,
requestBodyCreator: ApolloRequestBodyCreator(),
useGETForQueries: true,
useGETForPersistedQueryRetry: false
)
return ApolloClient(networkTransport: requestChainTransport, store: store)
}()
Extending GraphQLQuery provides access to the operation name, operation id, and variables, which can be used to build up the url. I also print out the operation name and variables for the query.
extension GraphQLQuery {
func printInfo() {
if let variables = self.variables?.JSONString {
let cleanedVariables = variables.replacingOccurrences(of: "\\", with: "")
print("GraphQL Query: \(self.operationName) \(variables))")
if let operationID = self.operationIdentifier {
let url = "\(GraphQLClient.shared.url)?extensions={\"persistedQuery\":{\"sha256Hash\":\"\(operationID)\",\"version\":1}}&id=\(operationID)&operationName=\(self.operationName)&variables=\(cleanedVariables)"
print("GraphQL URL", url)
}
} else {
print("GraphQL Query: \(self.operationName)")
if let operationID = self.operationIdentifier {
let url = "\(GraphQLClient.shared.url)?extensions={\"persistedQuery\":{\"sha256Hash\":\"\(operationID)\",\"version\":1}}&id=\(operationID)&operationName=\(self.operationName)"
print("GraphQL URL", url)
}
}
}
}
Usage:
let standingsQuery = GetStandingsForSportQuery(sportID: sportIDInt, season: season)
standingsQuery.printInfo()
Example output:
GraphQL Query: getStandingsForSport {"sportID":7,"season":"2020"})
GraphQL URL: https://api.company.com/graphql?extensions={"persistedQuery":{"sha256Hash":"932b414fdadb641f95659d6c61aa29d6d6b0ccf1fa704a0ace751187b90b8cac","version":1}}&id=932b414fdadb641f95659d6c61aa29d6d6b0ccf1fa704a0ace751187b90b8cac&operationName=getStandingsForSport&variables={"sportID":1,"season":"2020"}
The url format in this example may not be typical as we're using persisted queries. I used Charles proxy to see the actual url being sent so I'd know the format.
You could also extend GraphQLOperation instead of GraphQLQuery to get this same info, which would also support mutations and subscriptions.
Related
This code is used to show google drive contents in my iOS app. Now I could sign in and show contents by using below query "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'".
The problem is that it returns all the mp3 files even the ones inside sub-folders, that is not what I want. I want to show the same structure as google drive root. Then when a user enter any sub-folder, I would send another request to retrieve the mp3 files in that sub-folder.
So how could I reconstruct this query to achieve it?
// the code to filter/search google drive files.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(_ mimeType: String, onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
if you do mimeType = 'application/vnd.google-apps.folder' then you are telling it that you only want folders or a specific mime type.
if you do 'root' in parents" it will return everything with a parent folder of root.
so if you do 'root' in parents" and mimeType = 'application/vnd.google-apps.folder' you will get all of the folders that have a parent folder of root.
Per DalmTo's answer, I have changed my code and have some test. Now I get what I want.
In short, I split the list file into two steps, first to search with query = 'root' in parents", which will list all contents in root folder. Then I do a filter files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"} before passing that data source to another ViewController.
To do this, I get every folders in root and every mp3 files in root, other type of files are ignored. Then if a user enter one folder in root, I would do another http request(file list) to get its content.
Google drive API function.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
// query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
query.q = "'root' in parents"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
call api from a ViewController, and I put a bit context here to make it clear.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setViews()
// add an observer on notification "userDidSignInGoogle"
NotificationCenter.default.addObserver(self, selector: #selector(userDidSignInGoogle), name: .signInGoogleCompleted, object: nil)
setUpGoogleSignIn()
}
func setUpGoogleSignIn() {
GIDSignIn.sharedInstance().delegate = self
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().scopes = [kGTLRAuthScopeDrive]
GIDSignIn.sharedInstance().signInSilently()
}
// MARK: - Notification
#objc private func userDidSignInGoogle(_ notification: Notification) {
// Update screen after user successfully signed in
updateScreen()
print("userDidSignInGoogle")
listAudioFilesAndFolders()
}
func listAudioFilesAndFolders() {
self.googleAPIs?.search(onCompleted: { files, error in
guard error == nil, files != nil else {
print("Err: \(String(describing: error))")
return
}
self.dismiss(animated: true) {
let vc = GoogleDriveFilesViewController()
// filter the files before passing it.
vc.audioFilesAndFolders = files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"}
UIApplication.getTopMostViewController()?.present(vc, animated: true)
}
})
}
I am new in using Alamofire and iOS development in general. I am fetching data from server using the code below
func getDataFromServer(completion: #escaping ( _ dataString: String) -> Void) {
let url = "http://app10.com/index.php/Data"
let parameters : [String:Any] = ["someData": "xxx"]
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers:nil).responseJSON { (response) in
switch response.result {
case .failure(let errorResponse) :
let errorMessage = "error message here: \(errorResponse.localizedDescription)"
print(errorMessage)
completion("")
case .success(let value) :
let json = JSON(value)
let dataString = json["data"].stringValue
completion(dataString)
}
}
}
as you can see from the code above, I am trying to get dataString from server. but what I want to achieve is something like this
if meetSomeCondition {
// get 'dataString' from response cache from alamofire
} else {
// fetch 'dataString' from server, using the code above
}
can I do something like that using only alamofire?
I am sorry, I am newbie, but I can do something like that if I use Firebase Firestore, so maybe alamofire has feature something like that
I have tried to search but I can't find it in Stackoverflow
The underlying URLSession utilises a URLCache. Ensure your URLSession uses a URLCache and your server uses an appropriate response to instruct the client to cache the response.
In order to get the cached response, if any, the method according HTTP would be to set a cache control request header:
Cache-Control: only-if-cached (see RFC 5.2.1.7 only-if-cached)
That is, you would need to set this additional header when you create the request. Then execute the request as usual and you would either get the cached response or a status code 504 if there is none. Exactly what you would need.
Now, when you try this you realise, it unfortunately won't work as expected (bummer!). The reasons for this is manifold and it would be futile to go into detail.
The approach you may try next is to set the cache control policy of the URLRequest as follows:
urlRequest.cachePolicy = .returnCacheDataDontLoad
You can look up the meaning of it here: returnCacheDataDontLoad
Please read the documentation and the source documentation as well carefully, since it's not implemented in every iOS version!
That seems to be a working solution, however when using Alamofire you need to access the underlying URLRequest in order to set the cache policy (you may search on SO how to accomplish this) - and you need to be sure Alamofire does not alter your cache policy under the hood.
Alamofire does not provide cache directly.
It is easy to achieve it yourself.
use NSCache to keep our memory footprint low,
use time stamp, to invalidate stale data
here is an example:
// by writing a thin wrapper around NSCache, we can create a much more flexible Swift caching API
// — that enables us to store structs and other value types, and let us use any Hashable key type
final class Cache<Key: Hashable, Value> {
private let wrapped = NSCache<WrappedKey, Entry>()
private let dateProvider: () -> Date
private let entryLifetime: TimeInterval
init(dateProvider: #escaping () -> Date = Date.init,
entryLifetime: TimeInterval = 12 * 60 * 60) {
self.dateProvider = dateProvider
self.entryLifetime = entryLifetime
}
func insert(_ value: Value, forKey key: Key) {
let date = dateProvider().addingTimeInterval(entryLifetime)
let entry = Entry(value: value, expirationDate: date)
wrapped.setObject(entry, forKey: WrappedKey(key))
}
func value(forKey key: Key) -> Value? {
guard let entry = wrapped.object(forKey: WrappedKey(key)) else {
return nil
}
guard dateProvider() < entry.expirationDate else {
// Discard values that have expired
removeValue(forKey: key)
return nil
}
return entry.value
}
func removeValue(forKey key: Key) {
wrapped.removeObject(forKey: WrappedKey(key))
}
}
private extension Cache {
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) { self.key = key }
override var hash: Int { return key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else {
return false
}
return value.key == key
}
}
}
private extension Cache {
final class Entry {
let value: Value
let expirationDate: Date
init(value: Value, expirationDate: Date) {
self.value = value
self.expirationDate = expirationDate
}
}
}
extension Cache {
subscript(key: Key) -> Value? {
get { return value(forKey: key) }
set {
guard let value = newValue else {
// If nil was assigned using our subscript,
// then we remove any value for that key:
removeValue(forKey: key)
return
}
insert(value, forKey: key)
}
}
}
call like this:
// global singleton
let cache = Cache<Int, Int>()
// ...
override func viewDidLoad() {
super.viewDidLoad()
cache.insert(5, forKey: 1)
print(cache[1])
}
Persistent caching is also easy.
use Codable to do serialize the data.
use FileManager to persist the data file to disk
Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.
I'm working with apple pay on iOS 11
I have unit testing for our credit card tokenization. But I'm trying to tokenize an apple pay request.
Is it possible to unit test apple pay?
import XCTest
import PassKit
#testable import os_ios
extension apiTests{
func testBraintreeApplePay(){
let expect = expectation(description: "testBraintreeApplePay")
var responseReceived = false
let number = "4111111111111111"
let month = "12"
let year = "20"
let securityCode = "411"
let postalCode = "70433"
let payment = PKPayment()
// add the above info to this payment
Braintree.paymentAuthorizationViewController(
didAuthorizePayment: payment,
completion: {(status: PKPaymentAuthorizationStatus,nonce:String?) in
responseReceived = true
// Check to see if we've received a nonce
expect.fulfill()
})
waitForExpectations(timeout: expectationTimeoutDuration, handler: nil)
XCTAssert(responseReceived)
}
}
In order to initialize 'PKPaymentAuthorizationController', you need to inject the value that each specific test needs. In the case of Apple Pay, it usually involves the method 'canMakePayments(usingNetworks: [PKPaymentNetwork]) -> Bool' and the failable initializer for the 'PKPaymentAuthorizationController' or 'PKPaymentAuthorizationViewController'.
You can do it either by constructor injection (passing the values on the initializer) or by property injection.
Here's an example of a class that handles authorization controller initialization, and completes with the controller, or completes with an error.
public class PaymentAuthorizationHandler {
public typealias ApplePayControllerFactory = (PKPaymentRequest) -> PKPaymentAuthorizationViewController?
public typealias ApplePayCompletionHandler = (Result<PKPaymentAuthorizationViewController, Error>) -> Void
public typealias NetworkAuthorizationHandler = ([PKPaymentNetwork]) -> Bool
public enum Error: Swift.Error {
case networkNotSupported
case unableToInitialize
}
private let controllerFactory: ApplePayControllerFactory
private let networkAuthorizationHandler: NetworkAuthorizationHandler
public init(
controllerFactory: #escaping ApplePayControllerFactory = PKPaymentAuthorizationViewController.init,
networkAuthorizationHandler: #escaping NetworkAuthorizationHandler = PKPaymentAuthorizationViewController.canMakePayments
) {
self.controllerFactory = controllerFactory
self.networkAuthorizationHandler = networkAuthorizationHandler
}
private func makeRequest() -> PKPaymentRequest { //... }
public func requestAuthorization(completion: ApplePayCompletionHandler) {
let request = makeRequest()
guard networkAuthorizationHandler(request.supportedNetworks) else {
completion(.failure(.networkNotSupported))
return
}
guard let paymentVC = controllerFactory(request) else {
completion(.failure(.unableToInitialize))
return
}
completion(.success(paymentVC))
}
}
The view controller initializer and the 'canMakePayments(usingNetworks: [PKPaymentNetwork]) -> Bool' method signature are being injected via constructor injection, represented by the typealiases. With this change, it’s possible to pass, in production, the real method, and for test use, the value required for the test case can be provided by injecting it during the test setup.
Here's an example of the tests:
func test_requestAuthorization_failsWhenControllerInitializationFails() {
let authController = PKPaymentAuthorizationViewController(paymentRequest: .invalidRequest)
let sut = PaymentAuthorizationHandler(controllerFactory: { request in
authController
}, networkAuthorizationHandler: { networks in
true
})
let exp = expectation(description: "Wait for request permission")
var expectedError: PaymentAuthorizationHandler.Error?
sut.requestAuthorization { response in
switch response {
case let.success(controller):
XCTFail("Expected failure, received \(String(describing: controller)) instead")
case let .failure(receivedError):
expectedError = receivedError
}
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
XCTAssertEqual(expectedError, .unableToInitialize)
}
func test_requestAuthorization_succeedsWhenPaymentNetworkSupported() {
let authController = PKPaymentAuthorizationViewController(paymentRequest: .validRequest)
let sut = PaymentAuthorizationHandler(controllerFactory: { request in
authController
}, networkAuthorizationHandler: { networks in
true
})
let exp = expectation(description: "Wait for request permission")
sut.requestAuthorization { response in
switch response {
case let.success(receivedController):
XCTAssertNotNil(receivedController, "Request might be incomplete, or payment is not possible")
case let .failure(receivedError):
XCTFail("Expected success, received \(receivedError) instead")
}
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
}
You can check a more thorough explanation here.
I am working on an iOS App with Swift 3 using ReactiveSwift 1.1.1, the MVVM + Flow Coordinator pattern and Firebase as a backend. I only recently started to adapt to FRP and I am still trying to figure out how to integrate new functionalities into my existing code base.
For instance, my model uses a asynchronous method from Firebase to download thumbnails from the web and I want to provide a SignalProducer<Content, NoError> to subscribe from my ViewModel classes and observe, if thumbnails have been downloaded, which then updates the UI.
// field to be used from the view-models to observe
public let thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
// TODO: send next content via completion below
}
// thumbnail download method
public func findThumbnail(bucketId: String, contentId: String) {
guard let userId = userService.getCurrentUserId() else {
debugPring("Error id")
return
}
let ref = self.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
debugPrint("Error download")
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
// TODO: emit signal with content
// How to send the content via the SignalProducer above?
})
}
I have also tried something similar with Signal<Content, NoError>, whereas I used the Signal<Content, NoError>.pipe() method to receive a (observer, disposable) tuple and I saved the observer as a private global field to access it form the Firebase callback.
Questions:
Is this the right approach or am I missing something?
How do I emit the content object on completion?
UPDATE:
After some hours of pain, I found out how to design the SingalProducer to emit signals and to subscribe from the ViewModels.
Maybe the following code snippet will help also others:
// model protocol
import ReactiveSwift
import enum Result.NoError
public protocol ContentService {
func findThumbnail(bucketId: String, contentId: String)
var thumbnailContentProducer: SignalProducer<Content, NoError> { get }
}
// model implementation using firebase
import Firebase
import FirebaseStorage
import ReactiveSwift
public class FirebaseContentService: ContentService {
// other fields, etc.
// ...
private var thumbnailContentObserver: Observer<Content, NoError>?
private var thumbnailContentSignalProducer: SignalProducer<Content, NoError>?
var thumbnailContentProducer: SignalProducer<Content, NoError> {
return thumbnailContentSignalProducer!
}
init() {
thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
self.thumbnailContentObserver = observer
}
}
func findThumbnail(bucketId: String, contentId: String) {
guard let userId = userService.getCurrentUserId() else {
// TODO handle error
return
}
let ref = self.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
// TODO handle error
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
// emit signal
self.thumbnailContentObserver?.send(value: content)
})
}
}
// usage from a ViewModel
contentService.thumbnailContentProducer
.startWithValues { content in
self.contents.append(content)
}
Maybe someone can verify the code above and say that this is the right way to do it.
I think you were on the right path when you were looking at using Signal with pipe. The key point is that you need to create a new SignalProducer for each thumbnail request, and you need a way to combine all of those requests into one resulting signal. I was thinking something like this (note this is untested code, but it should get the idea across):
class FirebaseContentService {
// userService and storageThumbnail defined here
}
extension FirebaseContentService: ReactiveExtensionsProvider { }
extension Reactive where Base: FirebaseContentService {
private func getThumbnailContentSignalProducer(bucketId: String, contentId: String) -> SignalProducer<Content, ContentError> {
return SignalProducer<Content, ContentError> { (observer, disposable) in
guard let userId = self.base.userService.getCurrentUserId() else {
observer.send(error: ContentError.invalidUserLogin)
return
}
let ref = self.base.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
observer.send(error: ContentError.contentNotFound)
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
observer.send(value: content)
observer.sendCompleted()
})
}
}
}
class ThumbnailProvider {
public let thumbnailSignal: Signal<Content, NoError>
private let input: Observer<(bucketId: String, contentId: String), NoError>
init(contentService: FirebaseContentService) {
let (signal, observer) = Signal<(bucketId: String, contentId: String), NoError>.pipe()
self.input = observer
self.thumbnailSignal = signal
.flatMap(.merge) { param in
return contentService.reactive.getThumbnailContentSignalProducer(bucketId: param.bucketId, contentId: param.contentId)
.flatMapError { error in
debugPrint("Error download")
return SignalProducer.empty
}
}
}
public func findThumbnail(bucketId: String, contentId: String) {
input.send(value: (bucketId: bucketId, contentId: contentId))
}
}
Using ReactiveExtensionsProvider like this is the idiomatic way of adding reactive APIs to existing functionality via a reactive property.
The actual requesting code is confined to getThumbnailContentSignalProducer which creates a SignalProducer for each request. Note that errors are passed along here, and the handling and conversion to NoError happens later.
findThumbnails just takes a bucketId and contentId and sends it through the input observable.
The construction of thumbnailSignal in init is where the magic happens. Each input, which is a tuple containing a bucketId and contentId, is converted into a request via flatMap. Note that the .merge strategy means the thumbnails are sent as soon as possible in whatever order the requests complete. You can use .concat if you want to ensure that the thumbnails are returned in the same order they were requested.
The flatMapError is where the potential errors get handled. In this case it's just printing "Error download" and doing nothing else.