Skip to content

Instantly share code, notes, and snippets.

@gkostadinov
Created November 1, 2018 10:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gkostadinov/2c53453eda32c6730acc64e9784a9c95 to your computer and use it in GitHub Desktop.
Save gkostadinov/2c53453eda32c6730acc64e9784a9c95 to your computer and use it in GitHub Desktop.
Ray Wenderlich Alamofire Tutorial - Imagga API V2
//
// ImaggaRouter.swift
// PhotoTagger
//
import Foundation
import Alamofire
public enum ImaggaRouter: URLRequestConvertible {
static let baseURLPath = "https://api.imagga.com/v2"
static let authenticationToken = "<YOUR_AUTH_TOKEN_HERE>"
case content
case tags(String)
case colors(String)
var method: HTTPMethod {
switch self {
case .content:
return .post
case .tags, .colors:
return .get
}
}
var path: String {
switch self {
case .content:
return "/uploads"
case .tags:
return "/tags"
case .colors:
return "/colors"
}
}
public func asURLRequest() throws -> URLRequest {
let parameters: [String: Any] = {
switch self {
case .tags(let uploadId):
return ["image_upload_id": uploadId]
case .colors(let uploadId):
return ["image_upload_id": uploadId, "extract_object_colors": 0]
default:
return [:]
}
}()
let url = try ImaggaRouter.baseURLPath.asURL()
var request = URLRequest(url: url.appendingPathComponent(path))
request.httpMethod = method.rawValue
request.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
request.timeoutInterval = TimeInterval(10 * 1000)
return try URLEncoding.default.encode(request, with: parameters)
}
}
//
// ViewController.swift
// PhotoTagger
//
import UIKit
import Alamofire
class ViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet var takePictureButton: UIButton!
@IBOutlet var imageView: UIImageView!
@IBOutlet var progressView: UIProgressView!
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
// MARK: - Properties
fileprivate var tags: [String]?
fileprivate var colors: [PhotoColor]?
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
guard !UIImagePickerController.isSourceTypeAvailable(.camera) else { return }
takePictureButton.setTitle("Select Photo", for: .normal)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
imageView.image = nil
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
let controller = segue.destination as! TagsColorsViewController
controller.tags = tags
controller.colors = colors
}
}
// MARK: - IBActions
@IBAction func takePicture(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self
picker.allowsEditing = false
if UIImagePickerController.isSourceTypeAvailable(.camera) {
picker.sourceType = .camera
} else {
picker.sourceType = .photoLibrary
picker.modalPresentationStyle = .fullScreen
}
present(picker, animated: true)
}
}
// MARK: - UIImagePickerControllerDelegate
extension ViewController: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
print("Info did not have the required UIImage for the Original Image")
dismiss(animated: true)
return
}
imageView.image = image
// 1
takePictureButton.isHidden = true
progressView.progress = 0.0
progressView.isHidden = false
activityIndicatorView.startAnimating()
upload(
image: image,
progressCompletion: { [unowned self] percent in
// 2
self.progressView.setProgress(percent, animated: true)
},
completion: { [unowned self] tags, colors in
// 3
self.takePictureButton.isHidden = false
self.progressView.isHidden = true
self.activityIndicatorView.stopAnimating()
self.tags = tags
self.colors = colors
// 4
self.performSegue(withIdentifier: "ShowResults", sender: self)
})
dismiss(animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension ViewController: UINavigationControllerDelegate {
}
// Networking calls
extension ViewController {
func upload(image: UIImage,
progressCompletion: @escaping (_ percent: Float) -> Void,
completion: @escaping (_ tags: [String], _ colors: [PhotoColor]) -> Void) {
guard let imageData = UIImageJPEGRepresentation(image, 0.5) else {
print("Could not get JPEG representation of UIImage")
return
}
Alamofire.upload(
multipartFormData: { multipartFormData in
multipartFormData.append(imageData,
withName: "image",
fileName: "image.jpg",
mimeType: "image/jpeg")
},
with: ImaggaRouter.content,
encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.uploadProgress { progress in
progressCompletion(Float(progress.fractionCompleted))
}
upload.validate()
upload.responseJSON { response in
// 1.
guard response.result.isSuccess else {
print("Error while uploading file: \(String(describing: response.result.error))")
completion([String](), [PhotoColor]())
return
}
// 2.
guard let responseJSON = response.result.value as? [String: Any],
let uploadedFiles = responseJSON["result"] as? [String: Any],
let firstFileID = uploadedFiles["upload_id"] as? String else {
print("Invalid information received from service")
completion([String](), [PhotoColor]())
return
}
print("Content uploaded with ID: \(firstFileID)")
// 3.
self.downloadTags(uploadId: firstFileID) { tags in
self.downloadColors(uploadId: firstFileID) { colors in
completion(tags, colors)
}
}
}
case .failure(let encodingError):
print(encodingError)
}
}
)
}
func downloadTags(uploadId: String, completion: @escaping ([String]) -> Void) {
Alamofire.request(ImaggaRouter.tags(uploadId))
.responseJSON { response in
// 1.
guard response.result.isSuccess else {
print("Error while fetching tags: \(String(describing: response.result.error))")
completion([String]())
return
}
// 2.
guard let responseJSON = response.result.value as? [String: Any],
let result = responseJSON["result"] as? [String: Any],
let tagsAndConfidences = result["tags"] as? [[String: Any]] else {
print("Invalid tag information received from the service")
completion([String]())
return
}
// 3.
let tags = tagsAndConfidences.flatMap({ dict in
guard let tag = dict["tag"] as? [String: Any],
let tagName = tag["en"] as? String else {
return nil
}
return tagName
})
// 4.
completion(tags)
}
}
func downloadColors(uploadId: String, completion: @escaping ([PhotoColor]) -> Void) {
Alamofire.request(ImaggaRouter.colors(uploadId))
.responseJSON { response in
// 2.
guard response.result.isSuccess else {
print("Error while fetching colors: \(String(describing: response.result.error))")
completion([PhotoColor]())
return
}
// 3.
guard let responseJSON = response.result.value as? [String: Any],
let result = responseJSON["result"] as? [String: Any],
let info = result["colors"] as? [String: Any],
let imageColors = info["image_colors"] as? [[String: Any]] else {
print("Invalid color information received from service")
completion([PhotoColor]())
return
}
// 4.
let photoColors = imageColors.flatMap({ (dict) -> PhotoColor? in
guard let r = dict["r"] as? Int,
let g = dict["g"] as? Int,
let b = dict["b"] as? Int,
let closestPaletteColor = dict["closest_palette_color"] as? String else {
return nil
}
return PhotoColor(red: Int(r),
green: Int(g),
blue: Int(b),
colorName: closestPaletteColor)
})
// 5.
completion(photoColors)
}
}
}
@gkostadinov
Copy link
Author

Replace these two files with the ones in the tutorial to have a working Alamofire example with Imagga API v2.

Once you have replaced them, open ImaggaRouter.swift and replace "<YOUR_AUTH_TOKEN_HERE>" with your authorization token which can be found here:
https://imagga.com/profile/dashboard

If you don't have an authorization token and haven't sign up for free Imagga account, sign up from here:
https://imagga.com/auth/signup/hacker

@Afinque
Copy link

Afinque commented Nov 22, 2018

Hi, is there any chance that you could give reasons for the required changes in ViewController.swift? I see that you did some language handling, but I'm not sure if that's required to just get it working, or if it was extra functionality you wanted to add. A number of other things look like they changed, but I'm not sure if it's just style, or if it was required to address the updated API. Thanks so much for posting this, it's really helping me understand the tutorial!

2nd edit: the reason I ask is because I don't just want to paste the updated file in to get it working, I'd like to understand it, too, but it's a bit above me at the moment :)

@gkostadinov
Copy link
Author

Hi @Afinque, sorry for the late response, somehow I've missed the notification. All the changes that you see in the code above are mainly and only around the fact that Imagga API v2 has a changed interface compared to Imagga API v1. For example, regarding the language handling you are asking, in API v2 you are now required to explicitly provide the language key (in this case the default "en") in order to access the tag. The main reason for this interface change is for the API to have a unified response structure, even when you are using the language GET query parameter (e.g. &language=de, for German tags).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment