-
-
Save gkostadinov/2c53453eda32c6730acc64e9784a9c95 to your computer and use it in GitHub Desktop.
// | |
// 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) | |
} | |
} | |
} | |
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 :)
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).
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