Skip to content

Commit

Permalink
Add AvatarService and refactor accordingly (#122)
Browse files Browse the repository at this point in the history
* Add AvatarService and refactor

* Revert

* Rename method signature

* Update docs

* Add @discardableResult to image upload method

* Update Sources/Gravatar/Network/Services/AvatarService.swift

Co-authored-by: etoledom <[email protected]>

* Update Sources/Gravatar/Network/Services/AvatarService.swift

Co-authored-by: etoledom <[email protected]>

* Update Sources/Gravatar/Network/Services/AvatarService.swift

Co-authored-by: etoledom <[email protected]>

* Fix documentation warning

---------

Co-authored-by: etoledom <[email protected]>
  • Loading branch information
pinarol and etoledom authored Mar 21, 2024
1 parent 5f0b875 commit a1a0d9c
Show file tree
Hide file tree
Showing 22 changed files with 493 additions and 630 deletions.
20 changes: 7 additions & 13 deletions Demo/Demo/Gravatar-Demo/DemoAvatarDownloadViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ class DemoAvatarDownloadViewController: UIViewController {
return stack
}()

// private let imageRetriever = GravatarImageRetriever()
private let imageRetriever = Gravatar.ImageService()
private let imageRetriever = Gravatar.AvatarService()

override func viewDidLoad() {
super.viewDidLoad()
Expand Down Expand Up @@ -155,18 +154,13 @@ class DemoAvatarDownloadViewController: UIViewController {

avatarImageView.image = nil // Setting to nil to make the effect of `forceRefresh more visible

imageRetriever.fetchImage(with: emailInputField.text ?? "",
options: options) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let value):
self?.avatarImageView.image = value.image
print("Source URL: \(value.sourceURL)")
case .failure(let error):
print(error)
}
Task {
do {
let result = try await imageRetriever.fetch(with: emailInputField.text ?? "", options: options)
avatarImageView.image = result.image
} catch {
print(error)
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class DemoUIImageViewExtensionViewController: UIViewController {
return stack
}()

private let imageRetriever = ImageService()
private let imageRetriever = ImageDownloadService()

override func viewDidLoad() {
super.viewDidLoad()
Expand Down
13 changes: 7 additions & 6 deletions Demo/Demo/Gravatar-Demo/DemoUploadImageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,16 @@ class DemoUploadImageViewController: UIViewController {
else {
return
}

activityIndicator.startAnimating()
resultLabel.text = nil

// let service = GravatarService()
let service = Gravatar.ImageService()
service.uploadImage(image, accountEmail: email, accountToken: token) { [weak self] error in
DispatchQueue.main.async {
self?.uploadResult(with: error)
let service = Gravatar.AvatarService()
Task {
do {
try await service.upload(image, email: email, accessToken: token)
} catch {
uploadResult(with: error)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Gravatar/Gravatar.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ avatarImageView.gravatar.setImage(email: "[email protected]") { result in
}
```

You can also download the Gravatar image using the `ImageService` to download an image:
You can also download the Gravatar image using the `AvatarService` to download an image:

```swift
import Gravatar
Expand All @@ -81,7 +81,7 @@ Task {
}

func fetchAvatar(with email: String) async {
let service = ImageService()
let service = AvatarService()

do {
let result = try await service.fetchImage(with: email)
Expand Down
5 changes: 3 additions & 2 deletions Sources/Gravatar/Gravatar.docc/Gravatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ gravatarImageView.gravatar.setImage(email: "[email protected]")
```

For more info check:
- ``GravatarWrapper/setImage(email:placeholder:rating:preferredSize:defaultImage:options:completionHandler:)``
- ``GravatarWrapper/setImage(email:placeholder:rating:preferredSize:defaultImageOption:options:completionHandler:)``

## Featured

Expand All @@ -39,7 +39,8 @@ For more info check:

### Downloading images

- ``ImageService``
- ``ImageDownloadService``
- ``AvatarService``


### Get user Profile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ extension GravatarWrapper where Component: UIImageView {
let issuedIdentifier = SimpleCounter.next()
mutatingSelf.taskIdentifier = issuedIdentifier

let networkManager = options.imageDownloader ?? ImageService(cache: options.imageCache)
let networkManager = options.imageDownloader ?? ImageDownloadService(cache: options.imageCache)
mutatingSelf.imageDownloader = networkManager // Retain the network manager otherwise the completion tasks won't be done properly

let task = networkManager
Expand Down
7 changes: 4 additions & 3 deletions Sources/Gravatar/Network/CancellableDataTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import Foundation

/// Represents the task of a data downloading process, which can be cancelled while it's running.
///
/// We offer a default implementation of `CancellableDataTask` for `URLSessionTask` and `Task`.
/// We offer a default implementation of `CancellableDataTask` for `Task`.
public protocol CancellableDataTask {
/// Cancells a running task.
func cancel()
}

extension URLSessionTask: CancellableDataTask {}
/// Was the task cancelled?
var isCancelled: Bool { get }
}

extension Task: CancellableDataTask {}
2 changes: 1 addition & 1 deletion Sources/Gravatar/Network/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public enum RequestErrorReason {
case emptyURL
}

/// Errors thrown by `ImageService` when fetching an image.
/// Errors thrown by `ImageDownloadService` when fetching an image.
public enum ImageFetchingError: Error {
case requestError(reason: RequestErrorReason)
case responseError(reason: ResponseErrorReason)
Expand Down
51 changes: 51 additions & 0 deletions Sources/Gravatar/Network/Services/AvatarService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import UIKit

/// A service to perform uploading and downloading of avatars.
///
/// An avatar is a profile image of a Gravatar user. See [the avatar docs](https://support.gravatar.com/profiles/avatars/) for more info.
public struct AvatarService {
private let imageDownloader: ImageDownloader
private let imageUploader: ImageUploader

/// Creates a new `AvatarService`
///
/// Optionally, you can pass a custom type conforming to ``HTTPClient`` to gain control over networking tasks.
/// Similarly, you can pass a custom type conforming to ``ImageCaching`` to use your custom caching system.
/// - Parameters:
/// - client: A type which will perform basic networking operations.
/// - cache: A type which will perform image caching operations.
public init(client: HTTPClient? = nil, cache: ImageCaching? = nil) {
self.imageDownloader = ImageDownloadService(client: client, cache: cache)
self.imageUploader = ImageUploadService(client: client)
}

/// Fetches a Gravatar user profile image using the user account's email, and delivers the image asynchronously. See also: ``ImageDownloadService`` to
/// download the avatar via URL.
/// - Parameters:
/// - email: The user account email
/// - options: The options needed to perform the download.
/// - Returns: An asynchronously-delivered Result type containing the image and its URL.
public func fetch(
with email: String,
options: ImageDownloadOptions = ImageDownloadOptions()
) async throws -> ImageDownloadResult {
guard let gravatarURL = GravatarURL.gravatarUrl(with: email, options: options.imageQueryOptions) else {
throw ImageFetchingError.requestError(reason: .urlInitializationFailed)
}

return try await imageDownloader.fetchImage(with: gravatarURL, forceRefresh: options.forceRefresh, processingMethod: options.processingMethod)
}

/// Uploads an image to be used as the user's Gravatar profile image, and returns the `URLResponse` of the network tasks asynchronously. Throws
/// ``ImageUploadError``.
/// - Parameters:
/// - image: The image to be uploaded.
/// - email: The user email account.
/// - accessToken: The authentication token for the user. This is a WordPress.com OAuth2 access token.
/// - Returns: An asynchronously-delivered `URLResponse` instance, containing the response of the upload network task.
@discardableResult
public func upload(_ image: UIImage, email: String, accessToken: String) async throws -> URLResponse {
try await imageUploader.uploadImage(image, email: email, accessToken: accessToken)
}
}
2 changes: 1 addition & 1 deletion Sources/Gravatar/Network/Services/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// A `HTTPClient` is used to perform basic networking operations.
///
/// You can provide your own type conforming to this protocol to gain control over all networking operations performed internally by this SDK.
/// For more info, see ``ImageService/init(client:cache:)`` and ``ProfileService/init(client:)``.
/// For more info, see ``AvatarService/init(client:cache:)`` and ``ProfileService/init(client:)``.
public protocol HTTPClient {
/// Performs a data request using the information provided, and delivers the result asynchronously.
/// - Parameter request: A URL request object that provides request-specific information such as the URL and cache policy.
Expand Down
65 changes: 65 additions & 0 deletions Sources/Gravatar/Network/Services/ImageDownloadService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import UIKit

/// A service to perform image downloading.
///
/// This is the default type which implements ``ImageDownloader``..
/// Unless specified otherwise, `ImageDownloadService` will use a `URLSession` based `HTTPClient`, and a in-memory image cache.
public struct ImageDownloadService: ImageDownloader {
private let client: HTTPClient
let imageCache: ImageCaching

/// Creates a new `ImageDownloadService`
///
/// Optionally, you can pass a custom type conforming to ``HTTPClient`` to gain control over networking tasks.
/// Similarly, you can pass a custom type conforming to ``ImageCaching`` to use your custom caching system.
/// - Parameters:
/// - client: A type which will perform basic networking operations.
/// - cache: A type which will perform image caching operations.
public init(client: HTTPClient? = nil, cache: ImageCaching? = nil) {
self.client = client ?? URLSessionHTTPClient()
self.imageCache = cache ?? ImageCache()
}

private func fetchImage(from url: URL, forceRefresh: Bool, processor: ImageProcessor) async throws -> ImageDownloadResult {
let request = URLRequest.imageRequest(url: url, forceRefresh: forceRefresh)
do {
let (data, _) = try await client.fetchData(with: request)
guard let image = processor.process(data) else {
throw ImageFetchingError.imageProcessorFailed
}
imageCache.setImage(image, forKey: url.absoluteString)
return ImageDownloadResult(image: image, sourceURL: url)
} catch let error as HTTPClientError {
throw ImageFetchingError.responseError(reason: error.map())
} catch {
throw ImageFetchingError.responseError(reason: .unexpected(error))
}
}

public func fetchImage(
with url: URL,
forceRefresh: Bool = false,
processingMethod: ImageProcessingMethod = .common
) async throws -> ImageDownloadResult {
if !forceRefresh, let result = cachedImageResult(for: url) {
return result
}
return try await fetchImage(from: url, forceRefresh: forceRefresh, processor: processingMethod.processor)
}

func cachedImageResult(for url: URL) -> ImageDownloadResult? {
guard let cachedImage = imageCache.getImage(forKey: url.absoluteString) else {
return nil
}
return ImageDownloadResult(image: cachedImage, sourceURL: url)
}
}

extension URLRequest {
fileprivate static func imageRequest(url: URL, forceRefresh: Bool) -> URLRequest {
var request = forceRefresh ? URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) : URLRequest(url: url)
request.httpShouldHandleCookies = false
request.addValue("image/*", forHTTPHeaderField: "Accept")
return request
}
}
57 changes: 21 additions & 36 deletions Sources/Gravatar/Network/Services/ImageDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,36 @@ public typealias ImageDownloadCompletion = (Result<ImageDownloadResult, ImageFet

/// Represents a type which can be used by Gravatar to fetch images.
public protocol ImageDownloader {
/// Fetches a Gravatar user profile image using the user account's email.
/// - Parameters:
/// - email: The user account email
/// - options: The options needed to perform the download.
/// - completionHandler: A closure which is called when the task is completed.
/// - Returns: The task of an image downloading process.
func fetchImage(
with email: String,
options: ImageDownloadOptions,
completionHandler: ImageDownloadCompletion?
) -> CancellableDataTask

/// Fetches an image from the given `URL`.
/// Fetches an image from the given `URL`, and delivers the image asynchronously. Throws `ImageFetchingError`.
/// - Parameters:
/// - url: The URL from where to download the image.
/// - forceRefresh: Force the image to be downloaded, ignoring the cache.
/// - processingMethod: Method to use for processing the downloaded `Data`.
/// - completionHandler: A closure which is called when the task is completed.
/// - Returns: The task of an image downloading process.
/// - Returns: An asynchronously-delivered Result type containing the image and its URL.
func fetchImage(
with url: URL,
forceRefresh: Bool,
processingMethod: ImageProcessingMethod,
completionHandler: ImageDownloadCompletion?
) -> CancellableDataTask?

/// Fetches a Gravatar user profile image using the user account's email, and delivers the image asynchronously.
/// - Parameters:
/// - email: The user account email
/// - options: The options needed to perform the download.
/// - Returns: An asynchronously-delivered Result type containing the image and its URL.
func fetchImage(
with email: String,
options: ImageDownloadOptions
processingMethod: ImageProcessingMethod
) async throws -> ImageDownloadResult
}

/// Fetches an image from the given `URL`, and delivers the image asynchronously.
/// - Parameters:
/// - url: The URL from where to download the image.
/// - forceRefresh: Force the image to be downloaded, ignoring the cache.
/// - processingMethod: Method to use for processing the downloaded `Data`.
/// - Returns: An asynchronously-delivered Result type containing the image and its URL.
extension ImageDownloader {
@discardableResult
func fetchImage(
with url: URL,
forceRefresh: Bool,
processingMethod: ImageProcessingMethod
) async throws -> ImageDownloadResult
forceRefresh: Bool = false,
processingMethod: ImageProcessingMethod = .common,
completionHandler: ImageDownloadCompletion?
) -> CancellableDataTask? {
Task {
do {
let result = try await fetchImage(with: url, forceRefresh: forceRefresh, processingMethod: processingMethod)
completionHandler?(Result.success(result))
} catch let error as ImageFetchingError {
completionHandler?(Result.failure(error))
} catch {
completionHandler?(Result.failure(.responseError(reason: .unexpected(error))))
}
}
}
}
Loading

0 comments on commit a1a0d9c

Please sign in to comment.