Moya Tutorial for iOS: Getting Started

Moya is a networking library inspired by the concept of encapsulating network requests in type-safe way, typically using enumerations, that provides confidence when working with your network layer. Become a networking superhero with Moya! By Shai Mishali.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Imgur – Sharing With Friends!

For this, you’ll create another Moya target named Imgur that will let you interact with two different endpoints for image handling: one for uploading and one for deleting.

Similar to the Marvel API, you’ll need to sign up for a free account with Imgur.

After that, you’ll need to create an Imgur Application. You may use any fake URL for the callback, as you won’t be using OAuth here. You can also simply choose **OAuth 2 authorization without a callback URL**.

Registering a new Imgur application

Registering a new Imgur application

Once you submit the form, Imgur will present you with your new Imgur Client ID and Client secret. Save these for the next step.

Creating the Imgur Target

Right-click the ComicCards/Network folder and select New File… Then create a new Swift file and name it Imgur.swift.

Add the following code to define the Imgur endpoints that you’ll implement and use:

import UIKit
import Moya

public enum Imgur {
  // 1
  static private let clientId = "YOUR CLIENT ID"

  // 2
  case upload(UIImage)
  case delete(String)
}

Similar to the Marvel API, you:

  1. Store your Imgur Client ID in clientId. Make sure to replace this with the Client ID generated in the previous step (you don’t need the secret).
  2. Define the two endpoints that you’ll be using: upload, used to upload an image, and delete, which takes a hash for a previously uploaded image and deletes it from Imgur. These are represented in the Imgur API as POST /image and DELETE /image/{imageDeleteHash}.

Next, you’ll conform to TargetType. Add the following code right below your new enum:

extension Imgur: TargetType {
  // 1
  public var baseURL: URL {
    return URL(string: "https://api.imgur.com/3")!
  }

  // 2
  public var path: String {
    switch self {
    case .upload: return "/image"
    case .delete(let deletehash): return "/image/\(deletehash)"
    }
  }

  // 3
  public var method: Moya.Method {
    switch self {
    case .upload: return .post
    case .delete: return .delete
    }
  }

  // 4
  public var sampleData: Data {
    return Data()
  }

  // 5
  public var task: Task {
    switch self {
    case .upload(let image):
      let imageData = image.jpegData(compressionQuality: 1.0)!

      return .uploadMultipart([MultipartFormData(provider: .data(imageData),
                                                 name: "image",
                                                 fileName: "card.jpg",
                                                 mimeType: "image/jpg")])
    case .delete:
      return .requestPlain
    }
  }

  // 6
  public var headers: [String: String]? {
    return [
      "Authorization": "Client-ID \(Imgur.clientId)",
      "Content-Type": "application/json"
    ]
  }

  // 7
  public var validationType: ValidationType {
    return .successCodes
  }
}

This should look familiar to you by now. Let’s go through the seven protocol properties of the new Imgur target.

To upload a file, you’ll use the .uploadMultipart task type, which takes an array of MultipartFormData structs. You then create an instance of MultipartFormData with the appropriate image data, field name, file name and image mime type.

  1. The base URL for the Imgur API is set to https://api.imgur.com/3.
  2. You return the appropriate endpoint path based on the case. /image for .upload, and /image/{deletehash} for .delete.
  3. The method differs based on the case as well: .post for .upload and .delete for .delete.
  4. Just like before, you return an empty Data struct for sampleData.
  5. The task is where things get interesting. You return a different Task for every endpoint. The .delete case doesn’t require any parameters or content since it’s a simple DELETE request, but the .upload case needs some more work.
  6. Like the Marvel API, the headers property returns a Content-Type: application/json header, and an additional header. The Imgur API uses Header authorization, so you’ll need to provide your Client ID in the header of every request, in the form of Authorization: Client-ID (YOUR CLIENT ID).
  7. The .validationType is the same as before — valid for any status codes between 200 and 299.

Your Imgur target is done! This concludes the Moya-related code for the ComicCards app. Kudos to you!

The final step is completing CardViewController to have it use your newly created Moya target.

Wrapping Up CardViewController

Go back to CardViewController.swift and add the following lines at the beginning of your CardViewController class, below the comic property:

private let provider = MoyaProvider<Imgur>()
private var uploadResult: UploadResult?

Like before, you create a MoyaProvider instance, this time with the Imgur target. You also define uploadResult — an optional UploadResult property you’ll use to store the result of an upload, which you’ll need when deleting an image.

You have two methods to implement: uploadCard() and deleteCard().

At the end of uploadCard(), append the following code:

// 1
let card = snapCard()

// 2
provider.request(.upload(card),
  // 3
  callbackQueue: DispatchQueue.main,
  progress: { [weak self] progress in
    // 4
    self?.progressBar.setProgress(Float(progress.progress), animated: true)
  },
  completion: { [weak self] response in
    guard let self = self else { return }
    
    // 5
    UIView.animate(withDuration: 0.15) {
      self.viewUpload.alpha = 0.0
      self.btnShare.alpha = 0.0
    }
    
    // 6
    switch response {
    case .success(let result):
      do {
        let upload = try result.map(ImgurResponse<UploadResult>.self)
        
        self.uploadResult = upload.data
        self.btnDelete.alpha = 1.0
        
        self.presentShare(image: card, url: upload.data.link)
      } catch {
        self.presentError()
      }
    case .failure:
      self.presentError()
    }
})

This big chunk of code definitely needs some explanation, but worry not — most of it should be relatively familiar.

You’ll use this property later when finishing up the deleteCard() method. After storing the upload result, you trigger the presentShare method which will present a proper share alert with the URL to the uploaded image, and the image itself. A failure will trigger the presentError() method.

  1. You use a helper method called snapCard() to generate a UIImage from the presented card on screen.
  2. Like with the Marvel API, you use your provider to invoke the upload endpoint with an associated value of the card image.
  3. callbackQueue allows providing a queue on which you’ll receive upload progress updates in the next callback. You provide the main DispatchQueue to ensure progress updates happen on the main thread.
  4. You define a progress closure, which will be invoked as your image is uploaded to Imgur. This sets the progress bar’s progress and will be invoked on the main DispatchQueue provided in callbackQueue.
  5. When the request completes, you fade out the upload view and the share button.
  6. As before, you handle the success and failure options of the result. If successful, you try to map the response to an ImgurResponse and then store the mapped response in the instance property you defined before.

And for your final piece of code for the day: Add the following code inside deleteCard():

// 1
guard let uploadResult = uploadResult else { return }
btnDelete.isEnabled = false

// 2
provider.request(.delete(uploadResult.deletehash)) { [weak self] response in
  guard let self = self else { return }

  let message: String

  // 3
  switch response {
  case .success:
    message = "Deleted successfully!"
    self.btnDelete.alpha = 0.0
  case .failure:
    message = "Failed deleting card! Try again later."
    self.btnDelete.isEnabled = true
  }

  let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "Done", style: .cancel))

  self.present(alert, animated: true, completion: nil)
}

This method is rather simple and works as follows:

  1. You make sure the uploadResult is available and disable the delete button so the user doesn’t tap it again.
  2. You use the Imgur provider to invoke the delete endpoint with the associated value of the upload result’s deletehash. This hash uniquely identifies the uploaded image.
  3. In case of a successful or failed deletion, you show an appropriate message.

That is it! Build and run your app one final time. Select a comic and share your image to Imgur. After you’re done with it, you can tap the Delete from Imgur button to remove it.

Note: Something you might notice is that you can only delete the uploaded image as long as you’re in the card view controller. As soon as you leave it, the view controller’s uploadResult will be cleared and the deletehash will be lost. Persisting the hash for any generated images over different sessions is a nice challenge you might want to tackle :].