diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..120d061
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,11 @@
+### Expected behavior
+
+
+### Actual behavior
+
+
+### Steps to reproduce the behavior
+
+
+##### Tested on [device], iOS [version], WPiOS [version]
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..a6e0d83
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+Fixes #
+
+To test:
+
+PR submission checklist:
+
+- [ ] I have considered adding unit tests where possible.
+- [ ] I have considered adding accessibility improvements for my changes.
+- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..f560fef
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# How to Contribute
+
+First off, thank you for contributing! We're excited to collaborate with you! 🎉
+
+The following is a set of guidelines for the many ways you can join our collective effort.
+
+Before anything else, please take a moment to read our [Code of Conduct](https://github.com/wordpress-mobile/WordPress-iOS/blob/develop/CODE-OF-CONDUCT.md). We expect all participants, from full-timers to occasional tinkerers, to uphold it.
+
+## Reporting Bugs, Asking Questions, and Suggesting Features
+
+Have a suggestion or feedback? Please go to [Issues](https://github.com/wordpress-mobile/MediaEditor-iOS/issues) and [open a new issue](https://github.com/wordpress-mobile/MediaEditor-iOS/issues/new). Prefix the title with a category like _"Bug:"_, _"Question:"_, or _"Feature Request:"_. Screenshots help us resolve issues and answer questions faster, so thanks for including some if you can.
+
+## Submitting Code Changes
+
+If you're just getting started and want to familiarize yourself with the app’s code, we suggest looking at [these issues](https://github.com/wordpress-mobile/MediaEditor-iOS/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) with the **good first issue** label. But if you’d like to tackle something different, you're more than welcome to visit the [Issues](https://github.com/wordpress-mobile/MediaEditor-iOS/issues) page and pick an item that interests you.
+
+We always try to avoid duplicating efforts, so if you decide to work on an issue, leave a comment to state your intent. If you choose to focus on a new feature or the change you’re proposing is significant, we recommend waiting for a response before proceeding. The issue may no longer align with project goals.
+
+If the change is trivial, feel free to send a pull request without notifying us.
+
+### Pull Requests and Code Reviews
+
+All code contributions pass through pull requests. If you haven't created a pull request before, we recommend this free video series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
+
+The core team monitors and reviews all pull requests. Depending on the changes, we will either approve them or close them with an explanation. We might also work with you to improve a pull request before approval.
+
+We do our best to respond quickly to all pull requests. If you don't get a response from us after a week, feel free to reach out to us via Slack.
+
+## Getting in Touch
+
+If you have questions or just want to say hi, join the [WordPress Slack](https://make.wordpress.org/chat/) and drop a message on the `#mobile` channel.
diff --git a/Cartfile b/Cartfile
index e1d730e..cb856ff 100644
--- a/Cartfile
+++ b/Cartfile
@@ -1,2 +1 @@
github "TimOliver/TOCropViewController"
-github "Quick/Nimble"
diff --git a/Cartfile.private b/Cartfile.private
new file mode 100644
index 0000000..897faf4
--- /dev/null
+++ b/Cartfile.private
@@ -0,0 +1 @@
+github "Quick/Nimble"
diff --git a/Example/Base.lproj/Main.storyboard b/Example/Base.lproj/Main.storyboard
deleted file mode 100644
index 25a7638..0000000
--- a/Example/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Example/Extending/BrightnessCapability/AdditionalCapabilityViewController.swift b/Example/Extending/BrightnessCapability/AdditionalCapabilityViewController.swift
new file mode 100644
index 0000000..51d9d03
--- /dev/null
+++ b/Example/Extending/BrightnessCapability/AdditionalCapabilityViewController.swift
@@ -0,0 +1,48 @@
+import UIKit
+import MediaEditor
+
+class AdditionalCapabilityViewController: UIViewController {
+ @IBOutlet weak var imageView: UIImageView!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Append Brightness to the list of capabilities
+ MediaEditor.capabilities.append(BrightnessViewController.self)
+
+ // Add tap gesture in the image
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
+ imageView.isUserInteractionEnabled = true
+ imageView.addGestureRecognizer(tapGestureRecognizer)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ if isMovingFromParent {
+ MediaEditor.capabilities.removeLast()
+ }
+ }
+
+ @objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer) {
+ guard let image = imageView.image else {
+ return
+ }
+
+ // Give the image to the MediaEditor (you can also pass an array of UIImage)
+ let mediaEditor = MediaEditor(image)
+ mediaEditor.edit(from: self, onFinishEditing: { images, action in
+ // Display the edited image
+ guard let images = images as? [UIImage] else {
+ return
+ }
+
+ self.imageView.image = images.first
+ }, onCancel: {
+ // User canceled
+ })
+ }
+}
diff --git a/Example/Extending/BrightnessCapability/BrightnessCapability.storyboard b/Example/Extending/BrightnessCapability/BrightnessCapability.storyboard
new file mode 100644
index 0000000..1666998
--- /dev/null
+++ b/Example/Extending/BrightnessCapability/BrightnessCapability.storyboard
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/Extending/BrightnessCapability/BrightnessCapability.swift b/Example/Extending/BrightnessCapability/BrightnessCapability.swift
new file mode 100644
index 0000000..4188eba
--- /dev/null
+++ b/Example/Extending/BrightnessCapability/BrightnessCapability.swift
@@ -0,0 +1,89 @@
+import UIKit
+import MediaEditor
+
+// MARK: - BrightnessViewController
+
+class BrightnessViewController: UIViewController {
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var brightnessSlider: UISlider!
+
+ let context = MediaEditor.ciContext
+
+ var onFinishEditing: ((UIImage, [MediaEditorOperation]) -> ())?
+
+ var onCancel: (() -> ())?
+
+ var image: UIImage!
+
+ lazy var ciImage: CIImage? = {
+ return CIImage(image: image)
+ }()
+
+ lazy var brightnessFilter: CIFilter? = {
+ guard let ciImage = ciImage else {
+ return nil
+ }
+
+ let ciFilter = CIFilter(name: "CIColorControls")
+ ciFilter?.setValue(ciImage, forKey: "inputImage")
+
+ return ciFilter
+ }()
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ imageView.image = image
+ }
+
+
+ @IBAction func cancel(_ sender: Any) {
+ onCancel?()
+ }
+
+ @IBAction func done(_ sender: Any) {
+ // Check if the user changed the brightness
+ guard brightnessSlider.value > 0 else {
+ onCancel?()
+ return
+ }
+
+ // Get the UIImage
+ guard let ciImage = imageView.image?.ciImage, let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
+ onCancel?()
+ return
+ }
+
+ onFinishEditing?(UIImage(cgImage: cgImage), [])
+ }
+
+ // When the slider changes, apply the brightness value
+ @IBAction func sliderValueChanged(_ sender: UISlider) {
+ brightnessFilter?.setValue(sender.value, forKey: "inputBrightness")
+ if let outputImage = brightnessFilter?.outputImage {
+ imageView.image = UIImage(ciImage: outputImage)
+ }
+ }
+
+ // Load it from storyboard
+ static func fromStoryboard() -> BrightnessViewController {
+ return UIStoryboard(name: "BrightnessCapability", bundle: .main).instantiateViewController(withIdentifier: "brightnessViewController") as! BrightnessViewController
+ }
+}
+
+extension BrightnessViewController: MediaEditorCapability {
+ static var name = "Brightness"
+
+ static var icon = UIImage(named: "ink")!
+
+ static func initialize(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) -> CapabilityViewController {
+ let viewController = BrightnessViewController.fromStoryboard()
+ viewController.onFinishEditing = onFinishEditing
+ viewController.onCancel = onCancel
+ viewController.image = image
+ return viewController
+ }
+
+ func apply(styles: MediaEditorStyles) {
+ // Apply styles here
+ }
+}
diff --git a/Example/Image Editing/Device Library/DeviceLibraryViewController.swift b/Example/Image Editing/Device Library/DeviceLibraryViewController.swift
new file mode 100644
index 0000000..c44754c
--- /dev/null
+++ b/Example/Image Editing/Device Library/DeviceLibraryViewController.swift
@@ -0,0 +1,106 @@
+import UIKit
+import Photos
+import MediaEditor
+
+class DeviceLibraryViewController: UIViewController {
+
+ @IBOutlet weak var imagesCollectionView: UICollectionView!
+ @IBOutlet weak var descriptionLabel: UILabel!
+
+ var insertedImages: [AsyncImage] = []
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ imagesCollectionView.dataSource = self
+ imagesCollectionView.delegate = self
+ }
+
+ @IBAction func edit(_ sender: Any) {
+ // Load the last 10 Photos
+ let photos: [PHAsset] = fetchLatestPhotos(limit: 10)
+
+ // Give 'em to the Media Editor
+ let mediaEditor = MediaEditor(photos)
+
+ // Present the Media Editor
+ mediaEditor.edit(from: self, onFinishEditing: { images, actions in
+ self.insertedImages = images
+ self.imagesCollectionView.reloadData()
+ self.descriptionLabel.isHidden = true
+ }, onCancel: {
+ // User tapped cancel
+ })
+ }
+
+}
+
+// MARK: - UICollectionViewDataSource
+
+extension DeviceLibraryViewController: UICollectionViewDataSource {
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ return insertedImages.count
+ }
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageViewCollectionCell.identifier, for: indexPath) as! ImageViewCollectionCell
+
+ // Check if the image was edited
+ if let editedImage = insertedImages[indexPath.row].editedImage {
+ cell.configure(image: editedImage, wasEdited: true)
+ // If it wasn't, load the PHAsset and display it
+ } else if let phAsset = insertedImages[indexPath.row] as? PHAsset {
+ loadImage(from: phAsset, into: cell)
+ }
+
+ return cell
+ }
+}
+
+// MARK: - Size of the cells
+
+extension DeviceLibraryViewController: UICollectionViewDelegateFlowLayout {
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+ let width = (imagesCollectionView.frame.width - 21) / 3
+ return CGSize(width: width, height: width)
+ }
+}
+
+// MARK: - PHAsset related methods
+
+extension DeviceLibraryViewController {
+ func fetchLatestPhotos(limit: Int) -> [PHAsset] {
+ var assets: [PHAsset] = []
+
+ // Create fetch options.
+ let options = PHFetchOptions()
+ options.fetchLimit = limit
+
+ // Add sortDescriptor so the lastest photos will be returned.
+ let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
+ options.sortDescriptors = [sortDescriptor]
+
+ // Fetch the photos.
+ let result = PHAsset.fetchAssets(with: .image, options: options)
+
+ // Add them to an array
+ result.enumerateObjects { photo, _, _ in
+ assets.append(photo)
+ }
+
+ return assets
+ }
+
+ func loadImage(from asset: PHAsset, into cell: ImageViewCollectionCell) {
+ let options = PHImageRequestOptions()
+ options.deliveryMode = .opportunistic
+ options.version = .current
+ options.resizeMode = .fast
+ PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .default, options: options) { image, info in
+ guard let image = image else {
+ return
+ }
+
+ cell.configure(image: image, wasEdited: false)
+ }
+ }
+}
diff --git a/Example/Image Editing/Device Library/ImageViewCollectionCell.swift b/Example/Image Editing/Device Library/ImageViewCollectionCell.swift
new file mode 100644
index 0000000..aa382ee
--- /dev/null
+++ b/Example/Image Editing/Device Library/ImageViewCollectionCell.swift
@@ -0,0 +1,13 @@
+import UIKit
+
+class ImageViewCollectionCell: UICollectionViewCell {
+ static let identifier = "imageViewCollectionCell"
+
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var editedLabel: UILabel!
+
+ func configure(image: UIImage, wasEdited: Bool) {
+ imageView.image = image
+ editedLabel.isHidden = !wasEdited
+ }
+}
diff --git a/Example/Image Editing/Plain UIImage/PlainUIImageViewController.swift b/Example/Image Editing/Plain UIImage/PlainUIImageViewController.swift
new file mode 100644
index 0000000..cac9e50
--- /dev/null
+++ b/Example/Image Editing/Plain UIImage/PlainUIImageViewController.swift
@@ -0,0 +1,41 @@
+import UIKit
+import MediaEditor
+
+class PlainUIImageViewController: UIViewController {
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var secondImageView: UIImageView!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Add tap gesture in the first image
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
+ imageView.isUserInteractionEnabled = true
+ imageView.addGestureRecognizer(tapGestureRecognizer)
+
+ // Add tap gesture in the second image
+ let secondTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
+ secondImageView.isUserInteractionEnabled = true
+ secondImageView.addGestureRecognizer(secondTapGestureRecognizer)
+ }
+
+ @objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer) {
+ guard let firstImage = imageView.image,
+ let secondImage = secondImageView.image else {
+ return
+ }
+
+ // Give the image to the MediaEditor (you can also pass an array of UIImage)
+ let mediaEditor = MediaEditor([firstImage, secondImage])
+ mediaEditor.edit(from: self, onFinishEditing: { images, action in
+ // Display the edited image
+ guard let images = images as? [UIImage] else {
+ return
+ }
+
+ self.imageView.image = images.first
+ self.secondImageView.image = images[1]
+ }, onCancel: {
+ // User canceled
+ })
+ }
+}
diff --git a/Example/Image Editing/Remote Image/RemoteImageViewController.swift b/Example/Image Editing/Remote Image/RemoteImageViewController.swift
new file mode 100644
index 0000000..c90e6a8
--- /dev/null
+++ b/Example/Image Editing/Remote Image/RemoteImageViewController.swift
@@ -0,0 +1,88 @@
+import UIKit
+import MediaEditor
+
+class RemoteImageViewController: UIViewController {
+
+ @IBOutlet weak var firstImageView: UIImageView!
+ @IBOutlet weak var secondImageView: UIImageView!
+
+ let images = [
+ RemoteImage(thumb: UIImage(named: "thumb1"), fullImageURL: "https://cldup.com/_rSwtEeDGD.jpg"),
+ RemoteImage(thumb: UIImage(named: "thumb2"), fullImageURL: "https://cldup.com/L-cC3qX2DN.jpg")
+ ]
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Add tap gesture in the first image
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
+ firstImageView.isUserInteractionEnabled = true
+ firstImageView.addGestureRecognizer(tapGestureRecognizer)
+
+ // Add tap gesture in the second image
+ let secondTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
+ secondImageView.isUserInteractionEnabled = true
+ secondImageView.addGestureRecognizer(secondTapGestureRecognizer)
+
+ // Show the thumbs in this VC
+ firstImageView.image = images.first?.thumb
+ secondImageView.image = images[1].thumb
+ }
+
+ @objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer) {
+ // Give the images to the MediaEditor
+ let mediaEditor = MediaEditor(images)
+ mediaEditor.edit(from: self, onFinishEditing: { images, action in
+ // Display the returned images
+ if let image = images.first?.editedImage {
+ self.firstImageView.image = image
+ }
+
+ if let image = images[1].editedImage {
+ self.secondImageView.image = image
+ }
+ }, onCancel: {
+ // User canceled
+ })
+ }
+
+}
+/// Here we have a class that conforms to AsyncImage, in order to use remote images in the Media Editor
+/// Basically, we need to provide the thumb and the full image
+class RemoteImage: AsyncImage {
+ var thumb: UIImage?
+
+ let fullImageURL: URL
+
+ var tasks: [URLSessionDataTask] = []
+
+ init(thumb: UIImage?, fullImageURL: String) {
+ self.thumb = thumb
+ self.fullImageURL = URL(string: fullImageURL)!
+ }
+
+ func thumbnail(finishedRetrievingThumbnail: @escaping (UIImage?) -> ()) {
+ // In this example we already provide the thumb
+ }
+
+ /// Here we download the full quality image
+ func full(finishedRetrievingFullImage: @escaping (UIImage?) -> ()) {
+ let task = URLSession.shared.dataTask(with: fullImageURL) { data, response, error in
+ guard let data = data, error == nil else {
+ // If any error occured, calls the callback without an image
+ finishedRetrievingFullImage(nil)
+ return
+ }
+
+ // If the download was succesfull, gives the image to the callback
+ let downloadedImage = UIImage(data: data)
+ finishedRetrievingFullImage(downloadedImage)
+ }
+ task.resume()
+ tasks.append(task)
+ }
+
+ func cancel() {
+ tasks.forEach { $0.cancel() }
+ }
+
+}
diff --git a/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from Example/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/Example/Assets.xcassets/Contents.json b/Example/Resources/Assets.xcassets/Contents.json
similarity index 100%
rename from Example/Assets.xcassets/Contents.json
rename to Example/Resources/Assets.xcassets/Contents.json
diff --git a/Example/Resources/Assets.xcassets/image.imageset/Contents.json b/Example/Resources/Assets.xcassets/image.imageset/Contents.json
new file mode 100644
index 0000000..4ca559a
--- /dev/null
+++ b/Example/Resources/Assets.xcassets/image.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "cXyG__fTLN.jpg",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Example/Resources/Assets.xcassets/image.imageset/cXyG__fTLN.jpg b/Example/Resources/Assets.xcassets/image.imageset/cXyG__fTLN.jpg
new file mode 100644
index 0000000..05e42b8
Binary files /dev/null and b/Example/Resources/Assets.xcassets/image.imageset/cXyG__fTLN.jpg differ
diff --git a/Example/Resources/Assets.xcassets/image2.imageset/8lhI-gKnI2.jpg b/Example/Resources/Assets.xcassets/image2.imageset/8lhI-gKnI2.jpg
new file mode 100644
index 0000000..73ba83c
Binary files /dev/null and b/Example/Resources/Assets.xcassets/image2.imageset/8lhI-gKnI2.jpg differ
diff --git a/Example/Resources/Assets.xcassets/image2.imageset/Contents.json b/Example/Resources/Assets.xcassets/image2.imageset/Contents.json
new file mode 100644
index 0000000..f72c4b9
--- /dev/null
+++ b/Example/Resources/Assets.xcassets/image2.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "8lhI-gKnI2.jpg",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Example/Resources/Assets.xcassets/ink.imageset/Contents.json b/Example/Resources/Assets.xcassets/ink.imageset/Contents.json
new file mode 100644
index 0000000..2dfdfa9
--- /dev/null
+++ b/Example/Resources/Assets.xcassets/ink.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "gridicons-ink.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
diff --git a/Example/Resources/Assets.xcassets/ink.imageset/gridicons-ink.pdf b/Example/Resources/Assets.xcassets/ink.imageset/gridicons-ink.pdf
new file mode 100644
index 0000000..c05d7fb
Binary files /dev/null and b/Example/Resources/Assets.xcassets/ink.imageset/gridicons-ink.pdf differ
diff --git a/Example/Resources/Assets.xcassets/thumb1.imageset/Contents.json b/Example/Resources/Assets.xcassets/thumb1.imageset/Contents.json
new file mode 100644
index 0000000..54c84ec
--- /dev/null
+++ b/Example/Resources/Assets.xcassets/thumb1.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "_rSwtEeDGD.jpg",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Example/Resources/Assets.xcassets/thumb1.imageset/_rSwtEeDGD.jpg b/Example/Resources/Assets.xcassets/thumb1.imageset/_rSwtEeDGD.jpg
new file mode 100644
index 0000000..d9c74e7
Binary files /dev/null and b/Example/Resources/Assets.xcassets/thumb1.imageset/_rSwtEeDGD.jpg differ
diff --git a/Example/Resources/Assets.xcassets/thumb2.imageset/Contents.json b/Example/Resources/Assets.xcassets/thumb2.imageset/Contents.json
new file mode 100644
index 0000000..d7c5988
--- /dev/null
+++ b/Example/Resources/Assets.xcassets/thumb2.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "L-cC3qX2DN.jpg",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Example/Resources/Assets.xcassets/thumb2.imageset/L-cC3qX2DN.jpg b/Example/Resources/Assets.xcassets/thumb2.imageset/L-cC3qX2DN.jpg
new file mode 100644
index 0000000..b5adff4
Binary files /dev/null and b/Example/Resources/Assets.xcassets/thumb2.imageset/L-cC3qX2DN.jpg differ
diff --git a/Example/Base.lproj/LaunchScreen.storyboard b/Example/Resources/Base.lproj/LaunchScreen.storyboard
similarity index 100%
rename from Example/Base.lproj/LaunchScreen.storyboard
rename to Example/Resources/Base.lproj/LaunchScreen.storyboard
diff --git a/Example/Resources/Base.lproj/Main.storyboard b/Example/Resources/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..7dce340
--- /dev/null
+++ b/Example/Resources/Base.lproj/Main.storyboard
@@ -0,0 +1,412 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/ViewController.swift b/Example/ViewController.swift
index e1ee2a5..a1fd952 100644
--- a/Example/ViewController.swift
+++ b/Example/ViewController.swift
@@ -1,41 +1,13 @@
import UIKit
-import Photos
-import MediaEditor
-class ViewController: UIViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
- // Do any additional setup after loading the view.
- }
+class ViewController: UITableViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
- let photos = fetchLatestPhotos(limit: 30)
- let mediaEditor = MediaEditor(photos)
- mediaEditor.edit(from: self, onFinishEditing: { _, _ in }, onCancel: { })
- }
-
- func fetchLatestPhotos(limit: Int) -> [PHAsset] {
- var assets: [PHAsset] = []
-
- // Create fetch options.
- let options = PHFetchOptions()
- options.fetchLimit = limit
-
- // Add sortDescriptor so the lastest photos will be returned.
- let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
- options.sortDescriptors = [sortDescriptor]
-
- // Fetch the photos.
- let result = PHAsset.fetchAssets(with: .image, options: options)
-
- // Add them to an array
- result.enumerateObjects { photo, _, _ in
- assets.append(photo)
- }
- return assets
+ #if !targetEnvironment(simulator)
+ tableView.footerView(forSection: 2)?.isHidden = true
+ #endif
}
}
diff --git a/MediaEditor.podspec b/MediaEditor.podspec
index 3c4b2e9..3a9305d 100644
--- a/MediaEditor.podspec
+++ b/MediaEditor.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'MediaEditor'
- s.version = '0.1.3'
+ s.version = '1.0.0'
s.summary = 'An extensible Media Editor for iOS.'
s.description = <<-DESC
diff --git a/MediaEditor.xcodeproj/project.pbxproj b/MediaEditor.xcodeproj/project.pbxproj
index c1ae403..3dc6d6c 100644
--- a/MediaEditor.xcodeproj/project.pbxproj
+++ b/MediaEditor.xcodeproj/project.pbxproj
@@ -7,6 +7,18 @@
objects = {
/* Begin PBXBuildFile section */
+ 8B05570523E1BF5900C10787 /* DeviceLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */; };
+ 8B05570723E1C1D800C10787 /* ImageViewCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */; };
+ 8B05570923E1CF2E00C10787 /* PlainUIImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */; };
+ 8B05570B23E1E9F200C10787 /* RemoteImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570A23E1E9F200C10787 /* RemoteImageViewController.swift */; };
+ 8B05570D23E1F2F300C10787 /* AdditionalCapabilityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570C23E1F2F300C10787 /* AdditionalCapabilityViewController.swift */; };
+ 8B05571223E1F73300C10787 /* BrightnessCapability.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B05571123E1F73300C10787 /* BrightnessCapability.storyboard */; };
+ 8B05571423E1F74A00C10787 /* BrightnessCapability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05571323E1F74A00C10787 /* BrightnessCapability.swift */; };
+ 8B062DCB23E8661400488F80 /* MediaEditorFilters.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B062DC923E8661400488F80 /* MediaEditorFilters.storyboard */; };
+ 8B062DCD23E8663C00488F80 /* UIImage+withSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B062DCC23E8663C00488F80 /* UIImage+withSize.swift */; };
+ 8B062DD023E87A9400488F80 /* MediaEditorFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B062DCF23E87A9400488F80 /* MediaEditorFilterTests.swift */; };
+ 8B062DD423E8925100488F80 /* MediaEditorFilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B062DD223E8924900488F80 /* MediaEditorFilterCell.swift */; };
+ 8B062DD723E8937100488F80 /* MediaEditorFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B062DD523E8936000488F80 /* MediaEditorFilters.swift */; };
8B11A3CF23D8F590000721F5 /* Bundle+mediaEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B11A3CE23D8F590000721F5 /* Bundle+mediaEditor.swift */; };
8B12DEC723DFB21A00243CD8 /* MediaEditor.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B50469223D7C88200068F66 /* MediaEditor.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
8B50469723D7C88200068F66 /* MediaEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B50469523D7C88200068F66 /* MediaEditor.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -80,6 +92,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLibraryViewController.swift; sourceTree = ""; };
+ 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewCollectionCell.swift; sourceTree = ""; };
+ 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainUIImageViewController.swift; sourceTree = ""; };
+ 8B05570A23E1E9F200C10787 /* RemoteImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageViewController.swift; sourceTree = ""; };
+ 8B05570C23E1F2F300C10787 /* AdditionalCapabilityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalCapabilityViewController.swift; sourceTree = ""; };
+ 8B05571123E1F73300C10787 /* BrightnessCapability.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = BrightnessCapability.storyboard; sourceTree = ""; };
+ 8B05571323E1F74A00C10787 /* BrightnessCapability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessCapability.swift; sourceTree = ""; };
+ 8B062DC923E8661400488F80 /* MediaEditorFilters.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MediaEditorFilters.storyboard; sourceTree = ""; };
+ 8B062DCC23E8663C00488F80 /* UIImage+withSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+withSize.swift"; sourceTree = ""; };
+ 8B062DCF23E87A9400488F80 /* MediaEditorFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorFilterTests.swift; sourceTree = ""; };
+ 8B062DD223E8924900488F80 /* MediaEditorFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorFilterCell.swift; sourceTree = ""; };
+ 8B062DD523E8936000488F80 /* MediaEditorFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorFilters.swift; sourceTree = ""; };
8B11A3CE23D8F590000721F5 /* Bundle+mediaEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+mediaEditor.swift"; sourceTree = ""; };
8B50469223D7C88200068F66 /* MediaEditor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MediaEditor.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8B50469523D7C88200068F66 /* MediaEditor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MediaEditor.h; sourceTree = ""; };
@@ -146,6 +170,95 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 8B05570E23E1F63A00C10787 /* BrightnessCapability */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05571123E1F73300C10787 /* BrightnessCapability.storyboard */,
+ 8B05571323E1F74A00C10787 /* BrightnessCapability.swift */,
+ 8B05570C23E1F2F300C10787 /* AdditionalCapabilityViewController.swift */,
+ );
+ path = BrightnessCapability;
+ sourceTree = "";
+ };
+ 8B05571523E2377F00C10787 /* Image Editing */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05571623E2378F00C10787 /* Device Library */,
+ 8B05571723E237A900C10787 /* Remote Image */,
+ 8B05571823E237B700C10787 /* Plain UIImage */,
+ );
+ path = "Image Editing";
+ sourceTree = "";
+ };
+ 8B05571623E2378F00C10787 /* Device Library */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */,
+ 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */,
+ );
+ path = "Device Library";
+ sourceTree = "";
+ };
+ 8B05571723E237A900C10787 /* Remote Image */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05570A23E1E9F200C10787 /* RemoteImageViewController.swift */,
+ );
+ path = "Remote Image";
+ sourceTree = "";
+ };
+ 8B05571823E237B700C10787 /* Plain UIImage */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */,
+ );
+ path = "Plain UIImage";
+ sourceTree = "";
+ };
+ 8B05571923E237DD00C10787 /* Extending */ = {
+ isa = PBXGroup;
+ children = (
+ 8B05570E23E1F63A00C10787 /* BrightnessCapability */,
+ );
+ path = Extending;
+ sourceTree = "";
+ };
+ 8B05571B23E2380200C10787 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 8B5046AC23D7C9AD00068F66 /* Assets.xcassets */,
+ 8B5046A923D7C9AC00068F66 /* Main.storyboard */,
+ 8B5046AE23D7C9AD00068F66 /* LaunchScreen.storyboard */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ 8B062DC723E865F800488F80 /* Filters */ = {
+ isa = PBXGroup;
+ children = (
+ 8B062DD123E8923500488F80 /* Cells */,
+ 8B062DC923E8661400488F80 /* MediaEditorFilters.storyboard */,
+ 8B062DD523E8936000488F80 /* MediaEditorFilters.swift */,
+ );
+ path = Filters;
+ sourceTree = "";
+ };
+ 8B062DCE23E87A7900488F80 /* Filters */ = {
+ isa = PBXGroup;
+ children = (
+ 8B062DCF23E87A9400488F80 /* MediaEditorFilterTests.swift */,
+ );
+ path = Filters;
+ sourceTree = "";
+ };
+ 8B062DD123E8923500488F80 /* Cells */ = {
+ isa = PBXGroup;
+ children = (
+ 8B062DD223E8924900488F80 /* MediaEditorFilterCell.swift */,
+ );
+ path = Cells;
+ sourceTree = "";
+ };
8B50468823D7C88200068F66 = {
isa = PBXGroup;
children = (
@@ -190,11 +303,11 @@
8B5046A223D7C9AC00068F66 /* Example */ = {
isa = PBXGroup;
children = (
+ 8B05571523E2377F00C10787 /* Image Editing */,
+ 8B05571923E237DD00C10787 /* Extending */,
+ 8B05571B23E2380200C10787 /* Resources */,
8B5046A323D7C9AC00068F66 /* AppDelegate.swift */,
8B5046A723D7C9AC00068F66 /* ViewController.swift */,
- 8B5046A923D7C9AC00068F66 /* Main.storyboard */,
- 8B5046AC23D7C9AD00068F66 /* Assets.xcassets */,
- 8B5046AE23D7C9AD00068F66 /* LaunchScreen.storyboard */,
8B5046B123D7C9AD00068F66 /* Info.plist */,
);
path = Example;
@@ -213,6 +326,7 @@
isa = PBXGroup;
children = (
8B5046C423D7CE1600068F66 /* UIImage+AsyncImage.swift */,
+ 8B062DCC23E8663C00488F80 /* UIImage+withSize.swift */,
8B5046C523D7CE1600068F66 /* PHAsset+AsyncImage.swift */,
8B11A3CE23D8F590000721F5 /* Bundle+mediaEditor.swift */,
);
@@ -223,6 +337,7 @@
isa = PBXGroup;
children = (
8B5046C923D7CE1600068F66 /* Crop */,
+ 8B062DC723E865F800488F80 /* Filters */,
8B5046CC23D7CE1600068F66 /* MediaEditorCapability.swift */,
);
path = Capabilities;
@@ -262,6 +377,7 @@
isa = PBXGroup;
children = (
8B50472423D7D36C00068F66 /* Crop */,
+ 8B062DCE23E87A7900488F80 /* Filters */,
);
path = Capabilities;
sourceTree = "";
@@ -404,6 +520,7 @@
files = (
8B5046E023D7CE1600068F66 /* MediaEditorHub.storyboard in Resources */,
8B5046DF23D7CE1600068F66 /* Media.xcassets in Resources */,
+ 8B062DCB23E8661400488F80 /* MediaEditorFilters.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -411,6 +528,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 8B05571223E1F73300C10787 /* BrightnessCapability.storyboard in Resources */,
8B5046B023D7C9AD00068F66 /* LaunchScreen.storyboard in Resources */,
8B5046AD23D7C9AD00068F66 /* Assets.xcassets in Resources */,
8B5046AB23D7C9AC00068F66 /* Main.storyboard in Resources */,
@@ -454,9 +572,12 @@
buildActionMask = 2147483647;
files = (
8B5046E223D7CE1600068F66 /* MediaEditorThumbCell.swift in Sources */,
+ 8B062DD423E8925100488F80 /* MediaEditorFilterCell.swift in Sources */,
8B5046D623D7CE1600068F66 /* UIImage+AsyncImage.swift in Sources */,
8B5046DE23D7CE1600068F66 /* AsyncImage.swift in Sources */,
8B5046DC23D7CE1600068F66 /* MediaEditorCapability.swift in Sources */,
+ 8B062DCD23E8663C00488F80 /* UIImage+withSize.swift in Sources */,
+ 8B062DD723E8937100488F80 /* MediaEditorFilters.swift in Sources */,
8B5046DB23D7CE1600068F66 /* MediaEditorCropZoomRotate.swift in Sources */,
8B5046E323D7CE1600068F66 /* MediaEditorStyle.swift in Sources */,
8B5046D823D7CE1600068F66 /* MediaEditor.swift in Sources */,
@@ -474,7 +595,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 8B05571423E1F74A00C10787 /* BrightnessCapability.swift in Sources */,
+ 8B05570D23E1F2F300C10787 /* AdditionalCapabilityViewController.swift in Sources */,
+ 8B05570923E1CF2E00C10787 /* PlainUIImageViewController.swift in Sources */,
+ 8B05570723E1C1D800C10787 /* ImageViewCollectionCell.swift in Sources */,
8B5046A823D7C9AC00068F66 /* ViewController.swift in Sources */,
+ 8B05570523E1BF5900C10787 /* DeviceLibraryViewController.swift in Sources */,
+ 8B05570B23E1E9F200C10787 /* RemoteImageViewController.swift in Sources */,
8B5046A423D7C9AC00068F66 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -488,6 +615,7 @@
8B50472723D7D36C00068F66 /* MediaEditorHubTests.swift in Sources */,
8B50472923D7D36C00068F66 /* MediaEditorTests.swift in Sources */,
8B50472823D7D36C00068F66 /* MediaEditorCropZoomRotateTests.swift in Sources */,
+ 8B062DD023E87A9400488F80 /* MediaEditorFilterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/MediaEditor.xcodeproj/xcshareddata/xcschemes/MediaEditor.xcscheme b/MediaEditor.xcodeproj/xcshareddata/xcschemes/MediaEditor.xcscheme
new file mode 100644
index 0000000..bd8cf43
--- /dev/null
+++ b/MediaEditor.xcodeproj/xcshareddata/xcschemes/MediaEditor.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index b6f9b9b..07e7ef4 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,101 @@
-# MediaEditor-iOS
\ No newline at end of file
+# MediaEditor
+
+[![CircleCI](https://circleci.com/gh/wordpress-mobile/MediaEditor-iOS.svg?style=svg)](https://circleci.com/gh/wordpress-mobile/MediaEditor-iOS) [![Version](https://img.shields.io/cocoapods/v/MediaEditor.svg?style=flat)](http://cocoadocs.org/docsets/MediaEditor) [![License](https://img.shields.io/cocoapods/l/MediaEditor.svg?style=flat)](http://cocoadocs.org/docsets/MediaEditor) [![Platform](https://img.shields.io/cocoapods/p/MediaEditor.svg?style=flat)](http://cocoadocs.org/docsets/MediaEditor) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
+
+MediaEditor is an extendable library for iOS that allows you to quickly and easily add image editing features to your app. You can edit single or multiple images, from the device's library or any other source. It has been designed to feel natural and part of the OS.
+
+
+
+
+
+# Features
+
+- [x] [`PHAsset`](https://developer.apple.com/documentation/photokit/phasset) support
+- [x] Editing of Plain `UIImage`
+- [x] Editing of remote images
+- [x] Single media support
+- [x] Multiple media support
+- [x] Editing in both portrait and landscape modes
+- [x] Cool filters
+- [x] Crop, zoom and rotate capability (thanks to [`TOCropViewController`](https://github.com/TimOliver/TOCropViewController))
+- [x] Easily extendable
+- [x] Customizable UI
+
+## Usage
+
+Using `MediaEditor` is very simple, just give it the media and present from a `ViewController`:
+
+```swift
+let assets: [PHAsset] = [asset1, asset2, asset3]
+let mediaEditor = MediaEditor(assets)
+mediaEditor.edit(from: self, onFinishEditing: { images, actions in
+ // images contains the returned images, edited or not
+ // actions contains the actions made during this session
+}, onCancel: {
+ // User canceled
+})
+```
+
+This presents the MediaEditor from the `ViewController` with a callback that is called when the user is finished editing.
+
+You can easily determine if an image has been edited by checking the `isEdited` property of the objects returned in the `images` array.
+
+You can initialize the `MediaEditor` with a single or an array of: `PHAsset`, `UIImage` or any other entity that conforms to `AsyncImage`.
+
+## More Examples
+
+Check the Example app for even more ways to use the MediaEditor:
+
+* Device Library: Edit media from the device library and output them in a `UICollectionView`
+* Remote Image: Edit media that is remotely hosted by conforming to the `AsyncImage` protocol and downloading high-quality images only when needed.
+* Plain UIImage: Editing plain UIImage's
+* Extending the `MediaEditor` capability by adding your own brightness extension
+
+# Requirements
+
+* iOS 11.0+
+* Swift 5
+
+# Installation
+
+### Cocoapods
+
+Add the following to your Podfile:
+
+```ruby
+pod 'MediaEditor'
+```
+
+### Carthage
+
+1. Add the following to your Cartfile:
+```
+github "wordpress-mobile/MediaEditor-iOS"
+```
+
+2. Run `carthage update`
+
+3. From the `Carthage/Build` folder, import `MediaEditor.framework` and `TOCropViewController.framework` into your Xcode project.
+
+4. Follow the remaining steps on [Getting Started with Carthage](https://github.com/Carthage/Carthage#getting-started) to finish integrating the framework.
+
+### Manual Installation
+
+
+To install manually copy the `Sources/` folder to your project and follow the steps to [manual install `TOCropViewController`](https://github.com/TimOliver/TOCropViewController/blob/master/README.md#installation) too.
+
+## Contributing
+
+Read our [Contributing Guide](CONTRIBUTING.md) to learn about reporting issues, contributing code, and more ways to contribute.
+
+## Getting in Touch
+
+If you have questions about getting setup or just want to say hi, join the [WordPress Slack](https://chat.wordpress.org) and drop a message on the `#mobile` channel.
+
+## Author
+
+WordPress, mobile@automattic.com
+
+## License
+
+MediaEditor is available under the GPL license. See the [LICENSE file](./LICENSE) for more info.
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
new file mode 100644
index 0000000..3b835aa
--- /dev/null
+++ b/RELEASE-NOTES.txt
@@ -0,0 +1,12 @@
+1.0.0
+-----
+* Adds support to Carthage
+* Fix the support to PHAssets saved in iCloud
+* Added Filters Capability
+* Changes the Capability API
+
+0.1.3
+-----
+* Fix crashes in iOS 11 and 12 #4
+* Fix Autoulayout warnings #5
+* Fix swipe left-to-right conflict when cropping #7
diff --git a/Sources/Capabilities/Crop/MediaEditorCropZoomRotate.swift b/Sources/Capabilities/Crop/MediaEditorCropZoomRotate.swift
index bef64d6..9205508 100644
--- a/Sources/Capabilities/Crop/MediaEditorCropZoomRotate.swift
+++ b/Sources/Capabilities/Crop/MediaEditorCropZoomRotate.swift
@@ -1,81 +1,64 @@
import UIKit
import TOCropViewController
-class MediaEditorCropZoomRotate: NSObject, MediaEditorCapability {
- static var name = "Crop, Zoom, Rotate"
+typealias MediaEditorCropZoomRotate = TOCropViewController
- static var icon = UIImage(named: "gridicons-crop", in: .mediaEditor, compatibleWith: nil)!
+extension TOCropViewController: MediaEditorCapability {
+ public static var name = "Crop, Zoom, Rotate"
- var image: UIImage
+ public static var icon = UIImage(named: "gridicons-crop", in: .mediaEditor, compatibleWith: nil)!
- var onFinishEditing: (UIImage, [MediaEditorOperation]) -> ()
-
- var onCancel: (() -> ())
-
- lazy var viewController: UIViewController = {
+ public static func initialize(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) -> CapabilityViewController {
let cropViewController = TOCropViewController(image: image)
+ weak var toCrop = cropViewController
+
cropViewController.hidesNavigationBar = false
- cropViewController.delegate = self
+ cropViewController.onDidCropToRect = { image, _, _ in
+ onFinishEditing(image, toCrop?.actions ?? [])
+ }
+
+ cropViewController.onDidFinishCancelled = { _ in
+ onCancel()
+ }
return cropViewController
- }()
-
- required init(_ image: UIImage,
- onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (),
- onCancel: @escaping () -> ()) {
- self.image = image
- self.onFinishEditing = onFinishEditing
- self.onCancel = onCancel
}
- func apply(styles: MediaEditorStyles) {
- guard let viewController = viewController as? TOCropViewController else {
- return
- }
-
+ public func apply(styles: MediaEditorStyles) {
if let doneLabel = styles[.doneLabel] as? String {
- viewController.toolbar.doneTextButton.setTitle(doneLabel, for: .normal)
+ toolbar.doneTextButton.setTitle(doneLabel, for: .normal)
}
if let cancelLabel = styles[.cancelLabel] as? String {
- viewController.toolbar.cancelTextButton.setTitle(cancelLabel, for: .normal)
+ toolbar.cancelTextButton.setTitle(cancelLabel, for: .normal)
}
if let cancelColor = styles[.cancelColor] as? UIColor {
- viewController.toolbar.cancelTextButton.tintColor = cancelColor
- viewController.toolbar.cancelIconButton.tintColor = cancelColor
+ toolbar.cancelTextButton.tintColor = cancelColor
+ toolbar.cancelIconButton.tintColor = cancelColor
}
if let resetIcon = styles[.resetIcon] as? UIImage {
- viewController.toolbar.resetButton.setImage(resetIcon, for: .normal)
+ toolbar.resetButton.setImage(resetIcon, for: .normal)
}
if let doneIcon = styles[.doneIcon] as? UIImage {
- viewController.toolbar.doneIconButton.setImage(doneIcon, for: .normal)
+ toolbar.doneIconButton.setImage(doneIcon, for: .normal)
}
if let cancelIcon = styles[.cancelIcon] as? UIImage {
- viewController.toolbar.cancelIconButton.setImage(cancelIcon, for: .normal)
+ toolbar.cancelIconButton.setImage(cancelIcon, for: .normal)
}
if let rotateClockwiseIcon = styles[.rotateClockwiseIcon] as? UIImage {
- viewController.toolbar.rotateClockwiseButton?.setImage(rotateClockwiseIcon, for: .normal)
+ toolbar.rotateClockwiseButton?.setImage(rotateClockwiseIcon, for: .normal)
}
if let rotateCounterclockwiseButtonHidden = styles[.rotateCounterclockwiseButtonHidden] as? Bool {
- viewController.toolbar.rotateCounterclockwiseButtonHidden = rotateCounterclockwiseButtonHidden
+ toolbar.rotateCounterclockwiseButtonHidden = rotateCounterclockwiseButtonHidden
}
}
-}
-
-extension MediaEditorCropZoomRotate: TOCropViewControllerDelegate {
- func cropViewController(_ cropViewController: TOCropViewController, didFinishCancelled cancelled: Bool) {
- onCancel()
- }
- func cropViewController(_ cropViewController: TOCropViewController, didCropTo image: UIImage, with cropRect: CGRect, angle: Int) {
- onFinishEditing(image, cropViewController.actions)
- }
}
diff --git a/Sources/Capabilities/Filters/Cells/MediaEditorFilterCell.swift b/Sources/Capabilities/Filters/Cells/MediaEditorFilterCell.swift
new file mode 100644
index 0000000..5bcc540
--- /dev/null
+++ b/Sources/Capabilities/Filters/Cells/MediaEditorFilterCell.swift
@@ -0,0 +1,24 @@
+import UIKit
+
+class MediaEditorFilterCell: UICollectionViewCell {
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var title: UILabel!
+
+ func configure(image: UIImage, title: String) {
+ imageView.image = image
+ self.title.text = title
+ }
+
+ func showBorder(color: UIColor? = nil) {
+ imageView.layer.borderWidth = 5
+ imageView.layer.borderColor = color?.cgColor ?? Constant.defaultSelectedColor
+ }
+
+ func hideBorder() {
+ imageView.layer.borderWidth = 0
+ }
+
+ private enum Constant {
+ static var defaultSelectedColor = UIColor(red: 0.133, green: 0.443, blue: 0.694, alpha: 1).cgColor
+ }
+}
diff --git a/Sources/Capabilities/Filters/MediaEditorFilters.storyboard b/Sources/Capabilities/Filters/MediaEditorFilters.storyboard
new file mode 100644
index 0000000..5669058
--- /dev/null
+++ b/Sources/Capabilities/Filters/MediaEditorFilters.storyboard
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/Capabilities/Filters/MediaEditorFilters.swift b/Sources/Capabilities/Filters/MediaEditorFilters.swift
new file mode 100644
index 0000000..edcc132
--- /dev/null
+++ b/Sources/Capabilities/Filters/MediaEditorFilters.swift
@@ -0,0 +1,201 @@
+import UIKit
+
+struct MediaEditorFilter {
+ let name: String
+ let ciFilterName: String
+}
+
+class MediaEditorFilters: UIViewController {
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var filtersCollectionView: UICollectionView!
+ @IBOutlet weak var cancelButton: UIButton!
+ @IBOutlet weak var doneButton: UIButton!
+
+ var image: UIImage!
+
+ lazy var thumbImage: UIImage = {
+ let size = Constant.thumbWidth * UIScreen.main.scale
+ return image.fit(size: CGSize(width: size, height: size))
+ }()
+
+ var filters: [MediaEditorFilter] {
+ return [
+ MediaEditorFilter(
+ name: "None",
+ ciFilterName: ""
+ ),
+ MediaEditorFilter(
+ name: "Sepia",
+ ciFilterName: "CISepiaTone"
+ ),
+ MediaEditorFilter(
+ name: "Mono",
+ ciFilterName: "CIPhotoEffectMono"
+ ),
+ MediaEditorFilter(
+ name: "Noir",
+ ciFilterName: "CIPhotoEffectNoir"
+ ),
+ MediaEditorFilter(
+ name: "Vintage",
+ ciFilterName: "CIPhotoEffectProcess"
+ ),
+ MediaEditorFilter(
+ name: "Tonal",
+ ciFilterName: "CIPhotoEffectTonal"
+ ),
+ MediaEditorFilter(
+ name: "Transfer",
+ ciFilterName: "CIPhotoEffectTransfer"
+ ),
+ MediaEditorFilter(
+ name: "Chrome",
+ ciFilterName: "CIPhotoEffectChrome"
+ ),
+ MediaEditorFilter(
+ name: "Fade",
+ ciFilterName: "CIPhotoEffectFade"
+ ),
+ MediaEditorFilter(
+ name: "Instant",
+ ciFilterName: "CIPhotoEffectInstant"
+ )
+ ]
+ }
+
+ let context = MediaEditor.ciContext
+
+ var onFinishEditing: ((UIImage, [MediaEditorOperation]) -> ())?
+
+ var onCancel: (() -> ())?
+
+ private var selectedFilterIndex = IndexPath(row: 0, section: 0)
+
+ override func viewDidLoad() {
+ imageView.image = image
+ filtersCollectionView.dataSource = self
+ filtersCollectionView.delegate = self
+ }
+
+ @IBAction func cancel(_ sender: Any) {
+ onCancel?()
+ }
+
+ @IBAction func done(_ sender: Any) {
+ guard selectedFilterIndex.row > 0 else {
+ onCancel?()
+ return
+ }
+
+ guard let ciImage = imageView.image?.ciImage, let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
+ onCancel?()
+ return
+ }
+
+ onFinishEditing?(UIImage(cgImage: cgImage), [.filter])
+ }
+
+ func sepiaFilter(_ input: CIImage, intensity: Double) -> CIImage? {
+ let sepiaFilter = CIFilter(name:"CISepiaTone")
+ sepiaFilter?.setValue(input, forKey: kCIInputImageKey)
+ sepiaFilter?.setValue(intensity, forKey: kCIInputIntensityKey)
+ return sepiaFilter?.outputImage
+ }
+
+ func filter(_ image: UIImage, name: String) -> UIImage {
+ guard let ciImage = CIImage(image: image) else {
+ return image
+ }
+
+ let sepiaFilter = CIFilter(name: name)
+ sepiaFilter?.setValue(ciImage, forKey: kCIInputImageKey)
+
+ guard let outputImage = sepiaFilter?.outputImage else {
+ return image
+ }
+ return UIImage(ciImage: outputImage)
+ }
+
+ static func initialize() -> MediaEditorFilters {
+ return UIStoryboard(
+ name: "MediaEditorFilters",
+ bundle: Bundle(for: MediaEditorFilters.self)
+ ).instantiateViewController(withIdentifier: "filtersViewController") as! MediaEditorFilters
+ }
+
+ private enum Constant {
+ static var thumbWidth: CGFloat = 64
+ static var filterCellWidth: CGFloat = 71
+ static var filterCellHeight: CGFloat = 93
+ }
+}
+
+extension MediaEditorFilters: MediaEditorCapability {
+ static var name = "Filters"
+
+ static var icon = UIImage(named: "filters", in: .mediaEditor, compatibleWith: nil)!
+
+ static func initialize(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) -> CapabilityViewController {
+ let viewController: MediaEditorFilters = MediaEditorFilters.initialize()
+ viewController.onFinishEditing = onFinishEditing
+ viewController.onCancel = onCancel
+ viewController.image = image
+ return viewController
+ }
+
+ func apply(styles: MediaEditorStyles) {
+ if let doneLabel = styles[.doneLabel] as? String {
+ doneButton.setTitle(doneLabel, for: .normal)
+ }
+
+ if let cancelLabel = styles[.cancelLabel] as? String {
+ cancelButton.setTitle(cancelLabel, for: .normal)
+ }
+
+ if let cancelColor = styles[.cancelColor] as? UIColor {
+ cancelButton.tintColor = cancelColor
+ }
+ }
+}
+
+// MARK: - UICollectionViewDataSource
+
+extension MediaEditorFilters: UICollectionViewDataSource {
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ return filters.count
+ }
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "filterCell", for: indexPath) as! MediaEditorFilterCell
+
+ cell.configure(image: filter(thumbImage, name: filters[indexPath.row].ciFilterName), title: filters[indexPath.row].name)
+
+ if indexPath == selectedFilterIndex {
+ cell.showBorder()
+ } else {
+ cell.hideBorder()
+ }
+
+ return cell
+ }
+}
+
+// MARK: - UICollectionViewDelegate
+
+extension MediaEditorFilters: UICollectionViewDelegate {
+ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+ (collectionView.cellForItem(at: selectedFilterIndex) as? MediaEditorFilterCell)?.hideBorder()
+ (collectionView.cellForItem(at: indexPath) as? MediaEditorFilterCell)?.showBorder()
+ selectedFilterIndex = indexPath
+ imageView.image = filter(image, name: filters[indexPath.row].ciFilterName)
+ }
+}
+
+// MARK: - UICollectionViewDelegateFlowLayout
+
+extension MediaEditorFilters: UICollectionViewDelegateFlowLayout {
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+ return CGSize(width: Constant.filterCellWidth, height: Constant.filterCellHeight)
+ }
+}
+
diff --git a/Sources/Capabilities/MediaEditorCapability.swift b/Sources/Capabilities/MediaEditorCapability.swift
index c122e84..59977ee 100644
--- a/Sources/Capabilities/MediaEditorCapability.swift
+++ b/Sources/Capabilities/MediaEditorCapability.swift
@@ -1,21 +1,28 @@
import UIKit
+/// A type that is a UIViewController and also conforms to MediaEditorCapability
+public typealias CapabilityViewController = UIViewController & MediaEditorCapability
+
+/// A protocol that defines some properties for a Capability
public protocol MediaEditorCapability {
+
+ /// The name of your Capability. Eg.: "Emojis"
static var name: String { get }
+ /// An icon that represents your Capability. This will be displayed in the Media Editor interface.
static var icon: UIImage { get }
- var image: UIImage { get set }
-
- var viewController: UIViewController { get }
-
- var onFinishEditing: (UIImage, [MediaEditorOperation]) -> () { get }
-
- var onCancel: (() -> ()) { get }
-
- init(_ image: UIImage,
+ /// A static initializer for a CapabilityViewController.
+ ///
+ /// Use this method to initialize your UIViewController that conforms to MediaEditorCapability.
+ /// - Parameter image: `UIImage` to be displayed and edited
+ /// - Parameter onFinishEditing: block to be called when the user finished editing the image
+ /// - Parameter onCancel: block to be called when the user cancels editing the image
+ ///
+ static func initialize(_ image: UIImage,
onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (),
- onCancel: @escaping () -> ())
+ onCancel: @escaping () -> ()) -> CapabilityViewController
+ /// A function that applies given styles into your UIViewController
func apply(styles: MediaEditorStyles)
}
diff --git a/Sources/Enums/MediaEditorOperation.swift b/Sources/Enums/MediaEditorOperation.swift
index 1e83849..9483fac 100644
--- a/Sources/Enums/MediaEditorOperation.swift
+++ b/Sources/Enums/MediaEditorOperation.swift
@@ -3,5 +3,6 @@ import Foundation
public enum MediaEditorOperation {
case crop
case rotate
+ case filter
case other
}
diff --git a/Sources/Extensions/PHAsset+AsyncImage.swift b/Sources/Extensions/PHAsset+AsyncImage.swift
index 3c07f17..dcaf9d7 100644
--- a/Sources/Extensions/PHAsset+AsyncImage.swift
+++ b/Sources/Extensions/PHAsset+AsyncImage.swift
@@ -33,7 +33,8 @@ extension PHAsset: AsyncImage {
options.deliveryMode = .opportunistic
options.version = .current
options.resizeMode = .fast
- let requestID = PHImageManager.default().requestImage(for: self, targetSize: CGSize(width: pixelWidth, height: pixelHeight), contentMode: .default, options: options) { image, info in
+ options.isNetworkAccessAllowed = true
+ let requestID = PHImageManager.default().requestImage(for: self, targetSize: UIScreen.main.bounds.size, contentMode: .default, options: options) { image, info in
guard let image = image else {
finishedRetrievingThumbnail(nil)
return
@@ -51,6 +52,7 @@ extension PHAsset: AsyncImage {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.version = .current
+ options.isNetworkAccessAllowed = true
let requestID = PHImageManager.default().requestImage(for: self, targetSize: CGSize(width: pixelWidth, height: pixelHeight), contentMode: .default, options: options) { image, info in
guard let image = image else {
finishedRetrievingFullImage(nil)
diff --git a/Sources/Extensions/UIImage+withSize.swift b/Sources/Extensions/UIImage+withSize.swift
new file mode 100644
index 0000000..a89428d
--- /dev/null
+++ b/Sources/Extensions/UIImage+withSize.swift
@@ -0,0 +1,21 @@
+import UIKit
+
+extension UIImage {
+ func with(size: CGSize) -> UIImage {
+ let renderer = UIGraphicsImageRenderer(size: size)
+ let image = renderer.image { _ in
+ draw(in: CGRect(origin: CGPoint.zero, size: size))
+ }
+
+ return image
+ }
+
+ func fit(size: CGSize) -> UIImage {
+ let aspect = self.size.width / self.size.height
+ if size.width / aspect <= size.height {
+ return with(size: CGSize(width: size.width, height: size.width / aspect))
+ } else {
+ return with(size: CGSize(width: size.height * aspect, height: size.height))
+ }
+ }
+}
diff --git a/Sources/Media.xcassets/filters.imageset/Contents.json b/Sources/Media.xcassets/filters.imageset/Contents.json
new file mode 100644
index 0000000..a29322a
--- /dev/null
+++ b/Sources/Media.xcassets/filters.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "filters-icon.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Sources/Media.xcassets/filters.imageset/filters-icon.pdf b/Sources/Media.xcassets/filters.imageset/filters-icon.pdf
new file mode 100644
index 0000000..e352e2f
Binary files /dev/null and b/Sources/Media.xcassets/filters.imageset/filters-icon.pdf differ
diff --git a/Sources/MediaEditor.swift b/Sources/MediaEditor.swift
index 36bc27c..a4458b7 100644
--- a/Sources/MediaEditor.swift
+++ b/Sources/MediaEditor.swift
@@ -11,8 +11,11 @@ import UIKit
And by being a ViewController, this allows it to be custom presented.
*/
open class MediaEditor: UINavigationController {
- /// The capabilities do be displayed in the Media Editor. You can add your own capability here.
- public static var capabilities: [MediaEditorCapability.Type] = [MediaEditorCropZoomRotate.self]
+ /// The capabilities are displayed in the Media Editor. You can add your own capabilities here.
+ public static var capabilities: [MediaEditorCapability.Type] = [MediaEditorFilters.self, MediaEditorCropZoomRotate.self]
+
+ /// A CIContext to be shared among capabilities. If your app already has one, you can assign it here.
+ public static var ciContext = CIContext()
/// The ViewController that shows thumbnails and capabilities
var hub: MediaEditorHub = {
@@ -40,7 +43,7 @@ open class MediaEditor: UINavigationController {
public private(set) var actions: [MediaEditorOperation] = []
/// Returns which MediaEditorCapability is being displayed.
- public private(set) var currentCapability: MediaEditorCapability?
+ public private(set) var currentCapability: CapabilityViewController?
/// A Boolean value indicating whether the Media Editor is being used to edit plain UIImages
public private(set) var isEditingPlainUIImages = false
@@ -177,7 +180,7 @@ open class MediaEditor: UINavigationController {
private func setupForAsync() {
isEditingPlainUIImages = images.count > 0
-
+
asyncImages.enumerated().forEach { offset, asyncImage in
if let thumb = asyncImage.thumb {
thumbnailAvailable(thumb, offset: offset)
@@ -249,7 +252,7 @@ open class MediaEditor: UINavigationController {
private func present(capability capabilityEntity: MediaEditorCapability.Type, with image: UIImage) {
prepareTransition()
- let capability = capabilityEntity.init(
+ let capability = capabilityEntity.initialize(
image,
onFinishEditing: { [weak self] image, actions in
self?.finishEditing(image: image, actions: actions)
@@ -261,7 +264,7 @@ open class MediaEditor: UINavigationController {
capability.apply(styles: styles)
currentCapability = capability
- pushViewController(capability.viewController, animated: false)
+ pushViewController(capability, animated: false)
}
private func finishEditing(image: UIImage, actions: [MediaEditorOperation]) {
diff --git a/Sources/MediaEditorHub.swift b/Sources/MediaEditorHub.swift
index 8f8f476..ee681db 100644
--- a/Sources/MediaEditorHub.swift
+++ b/Sources/MediaEditorHub.swift
@@ -326,7 +326,7 @@ extension MediaEditorHub: UICollectionViewDelegateFlowLayout {
if collectionView == imagesCollectionView {
return CGSize(width: imagesCollectionView.frame.width, height: imagesCollectionView.frame.height)
} else if collectionView == thumbsCollectionView {
- return CGSize(width: Constants.thumbHeight, height: Constants.thumbHeight)
+ return numberOfThumbs > 1 ? CGSize(width: Constants.thumbHeight, height: Constants.thumbHeight) : .zero
} else {
return CGSize(width: Constants.toolbarHeight, height: Constants.toolbarHeight)
}
diff --git a/Tests/Capabilities/Crop/MediaEditorCropZoomRotateTests.swift b/Tests/Capabilities/Crop/MediaEditorCropZoomRotateTests.swift
index 4401e79..ad350f5 100644
--- a/Tests/Capabilities/Crop/MediaEditorCropZoomRotateTests.swift
+++ b/Tests/Capabilities/Crop/MediaEditorCropZoomRotateTests.swift
@@ -9,56 +9,56 @@ class MediaEditorCropZoomRotateTests: XCTestCase {
private let image = UIImage()
func testIsAMediaEditorCapability() {
- let mediaEditorCrop = MediaEditorCropZoomRotate(image, onFinishEditing: { _, _ in }, onCancel: {})
+ let mediaEditorCrop = MediaEditorCropZoomRotate.initialize(image, onFinishEditing: { _, _ in }, onCancel: {})
expect(mediaEditorCrop).to(beAKindOf(MediaEditorCapability.self))
}
func testDoNotHideNavigation() {
- let mediaEditorCrop = MediaEditorCropZoomRotate(image, onFinishEditing: { _, _ in }, onCancel: {})
+ let mediaEditorCrop = MediaEditorCropZoomRotate.initialize(image, onFinishEditing: { _, _ in }, onCancel: {})
- let viewController = mediaEditorCrop.viewController as? TOCropViewController
+ let viewController = mediaEditorCrop as? TOCropViewController
expect(viewController?.hidesNavigationBar).to(beFalse())
}
func testOnDidCropToRectCallOnFinishEditing() {
var onFinishEditingCalled = false
- let mediaEditorCrop = MediaEditorCropZoomRotate(
+ let mediaEditorCrop = MediaEditorCropZoomRotate.initialize(
image,
onFinishEditing: { _, _ in
onFinishEditingCalled = true
},
onCancel: {})
- let viewController = mediaEditorCrop.viewController as? TOCropViewController
+ let viewController = mediaEditorCrop as? TOCropViewController
- viewController?.delegate?.cropViewController?(viewController!, didCropTo: image, with: .zero, angle: 0)
+ viewController?.onDidCropToRect?(image, .zero, 0)
expect(onFinishEditingCalled).to(beTrue())
}
func testOnDidFinishCancelledCall() {
var onCancelCalled = false
- let mediaEditorCrop = MediaEditorCropZoomRotate(
+ let mediaEditorCrop = MediaEditorCropZoomRotate.initialize(
image,
onFinishEditing: { _, _ in },
onCancel: {
onCancelCalled = true
}
)
- let viewController = mediaEditorCrop.viewController as? TOCropViewController
+ let viewController = mediaEditorCrop as? TOCropViewController
- viewController?.delegate?.cropViewController?(viewController!, didFinishCancelled: true)
+ viewController?.onDidFinishCancelled?(true)
expect(onCancelCalled).to(beTrue())
}
func testHideRotateCounterclockwiseButton() {
- let mediaEditorCrop = MediaEditorCropZoomRotate(image, onFinishEditing: { _, _ in }, onCancel: {})
+ let mediaEditorCrop = MediaEditorCropZoomRotate.initialize(image, onFinishEditing: { _, _ in }, onCancel: {})
mediaEditorCrop.apply(styles: [.rotateCounterclockwiseButtonHidden: true])
- let viewController = mediaEditorCrop.viewController as? TOCropViewController
+ let viewController = mediaEditorCrop as? TOCropViewController
expect(viewController?.toolbar.rotateCounterclockwiseButtonHidden).to(beTrue())
}
diff --git a/Tests/Capabilities/Filters/MediaEditorFilterTests.swift b/Tests/Capabilities/Filters/MediaEditorFilterTests.swift
new file mode 100644
index 0000000..1fe1d80
--- /dev/null
+++ b/Tests/Capabilities/Filters/MediaEditorFilterTests.swift
@@ -0,0 +1,36 @@
+import XCTest
+import Nimble
+
+@testable import MediaEditor
+
+class MediaEditorFilterTests: XCTestCase {
+
+ func testName() {
+ let name = MediaEditorFilters.name
+
+ expect(name).to(equal("Filters"))
+ }
+
+ func testIcon() {
+ let icon = MediaEditorFilters.icon
+
+ expect(icon).to(equal(UIImage(named: "filters", in: .mediaEditor, compatibleWith: nil)!))
+ }
+
+ func testApplyStyles() {
+ let mediaEditorFilters = MediaEditorFilters.initialize(UIImage(), onFinishEditing: { _, _ in }, onCancel: {})
+ mediaEditorFilters.loadView()
+
+ mediaEditorFilters.apply(styles: [
+ .doneLabel: "foo",
+ .cancelLabel: "bar",
+ .cancelColor: UIColor.black
+ ])
+
+ let viewController = mediaEditorFilters as! MediaEditorFilters
+ expect(viewController.doneButton.titleLabel?.text).to(equal("foo"))
+ expect(viewController.cancelButton.titleLabel?.text).to(equal("bar"))
+ expect(viewController.cancelButton.tintColor).to(equal(.black))
+ }
+
+}
diff --git a/Tests/MediaEditorHubTests.swift b/Tests/MediaEditorHubTests.swift
index cbab77d..779ba71 100644
--- a/Tests/MediaEditorHubTests.swift
+++ b/Tests/MediaEditorHubTests.swift
@@ -102,6 +102,7 @@ class MediaEditorHubTests: XCTestCase {
let hub: MediaEditorHub = MediaEditorHub.initialize()
hub.availableThumbs = [0: UIImage(), 1: UIImage()]
hub.loadViewIfNeeded()
+ hub.imagesCollectionView.reloadData()
hub.loadingImage(at: 0)
hub.collectionView(hub.thumbsCollectionView, didSelectItemAt: IndexPath(row: 1, section: 0))
@@ -113,6 +114,7 @@ class MediaEditorHubTests: XCTestCase {
let hub: MediaEditorHub = MediaEditorHub.initialize()
hub.availableThumbs = [0: UIImage(), 1: UIImage()]
hub.loadViewIfNeeded()
+ hub.imagesCollectionView.reloadData()
hub.loadingImage(at: 1)
hub.loadingImage(at: 0)
diff --git a/Tests/MediaEditorTests.swift b/Tests/MediaEditorTests.swift
index 0818347..9a74f09 100644
--- a/Tests/MediaEditorTests.swift
+++ b/Tests/MediaEditorTests.swift
@@ -75,7 +75,7 @@ class MediaEditorTests: XCTestCase {
func testShowTheCapabilityRightAway() {
let mediaEditor = MediaEditor(image)
- expect(mediaEditor.visibleViewController).to(equal(mediaEditor.currentCapability?.viewController))
+ expect(mediaEditor.visibleViewController).to(equal((mediaEditor.currentCapability as? MockCapability)))
}
func testWhenCancelingDismissTheMediaEditor() {
@@ -84,7 +84,7 @@ class MediaEditorTests: XCTestCase {
let mediaEditor = MediaEditor(image)
viewController.present(mediaEditor, animated: false)
- mediaEditor.currentCapability?.onCancel()
+ (mediaEditor.currentCapability as? MockCapability)?.onCancel()
expect(viewController.presentedViewController).toEventually(beNil())
}
@@ -96,17 +96,17 @@ class MediaEditorTests: XCTestCase {
didCallOnFinishEditing = true
}
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
expect(didCallOnFinishEditing).to(beTrue())
}
func testWhenFinishEditingKeepRecordOfTheActions() {
let mediaEditor = MediaEditor(image)
- mediaEditor.currentCapability?.onFinishEditing(image, [.crop])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.crop])
mediaEditor.onFinishEditing = { _, _ in }
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
expect(mediaEditor.actions).to(equal([.crop, .rotate]))
}
@@ -118,7 +118,7 @@ class MediaEditorTests: XCTestCase {
returnedImages = images as! [UIImage]
}
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
expect(returnedImages).to(equal([image]))
}
@@ -203,8 +203,8 @@ class MediaEditorTests: XCTestCase {
asyncImage.simulate(fullImageHasBeenDownloaded: fullImage)
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil())
- expect(mediaEditor.visibleViewController).to(equal(mediaEditor.currentCapability?.viewController))
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil())
+ expect(mediaEditor.visibleViewController).to(equal((mediaEditor.currentCapability as? MockCapability)))
}
func testCallCancelOnAsyncImageWhenUserCancel() {
@@ -248,10 +248,10 @@ class MediaEditorTests: XCTestCase {
mediaEditor.onFinishEditing = { images, _ in
returnedImages = images
}
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil()) // Wait capability appear
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil()) // Wait capability appear
// When
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
// Then
expect(returnedImages.first?.isEdited).to(beTrue())
@@ -296,7 +296,7 @@ class MediaEditorTests: XCTestCase {
let mediaEditor = MediaEditor([whiteImage, blackImage])
- expect(mediaEditor.currentCapability).to(beNil())
+ expect((mediaEditor.currentCapability as? MockCapability)).to(beNil())
expect(mediaEditor.visibleViewController).to(equal(mediaEditor.hub))
}
@@ -307,8 +307,8 @@ class MediaEditorTests: XCTestCase {
mediaEditor.capabilityTapped(0)
- expect(mediaEditor.currentCapability).toNot(beNil())
- expect(mediaEditor.visibleViewController).to(equal(mediaEditor.currentCapability?.viewController))
+ expect((mediaEditor.currentCapability as? MockCapability)).toNot(beNil())
+ expect(mediaEditor.visibleViewController).to(equal((mediaEditor.currentCapability as? MockCapability)))
}
func testCallingOnCancelWhenShowingACapabilityGoesBackToHub() {
@@ -317,9 +317,9 @@ class MediaEditorTests: XCTestCase {
let mediaEditor = MediaEditor([whiteImage, blackImage])
mediaEditor.capabilityTapped(0)
- mediaEditor.currentCapability?.onCancel()
+ (mediaEditor.currentCapability as? MockCapability)?.onCancel()
- expect(mediaEditor.currentCapability).to(beNil())
+ expect((mediaEditor.currentCapability as? MockCapability)).to(beNil())
expect(mediaEditor.visibleViewController).to(equal(mediaEditor.hub))
}
@@ -330,7 +330,7 @@ class MediaEditorTests: XCTestCase {
let mediaEditor = MediaEditor([whiteImage, blackImage])
mediaEditor.capabilityTapped(0)
- mediaEditor.currentCapability?.onFinishEditing(editedImage, [.crop])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(editedImage, [.crop])
expect(mediaEditor.images[0]).to(equal(editedImage))
expect(mediaEditor.hub.availableImages[0]).to(equal(editedImage))
@@ -346,7 +346,7 @@ class MediaEditorTests: XCTestCase {
viewController.present(mediaEditor, animated: false)
mediaEditor.capabilityTapped(0)
- mediaEditor.currentCapability?.onCancel()
+ (mediaEditor.currentCapability as? MockCapability)?.onCancel()
expect(mediaEditor.visibleViewController).toEventually(equal(mediaEditor.hub))
}
@@ -359,7 +359,7 @@ class MediaEditorTests: XCTestCase {
returnedImages = images as! [UIImage]
}
mediaEditor.capabilityTapped(0)
- mediaEditor.currentCapability?.onFinishEditing(editedImage, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(editedImage, [.rotate])
mediaEditor.hub.doneButton.sendActions(for: .touchUpInside)
@@ -393,7 +393,7 @@ class MediaEditorTests: XCTestCase {
let mediaEditor = MediaEditor(asyncImages)
- expect(mediaEditor.currentCapability).to(beNil())
+ expect((mediaEditor.currentCapability as? MockCapability)).to(beNil())
expect(mediaEditor.visibleViewController).to(equal(mediaEditor.hub))
}
@@ -403,7 +403,7 @@ class MediaEditorTests: XCTestCase {
mediaEditor.capabilityTapped(0)
- expect(mediaEditor.currentCapability).to(beNil())
+ expect((mediaEditor.currentCapability as? MockCapability)).to(beNil())
expect(mediaEditor.visibleViewController).to(equal(mediaEditor.hub))
}
@@ -426,8 +426,8 @@ class MediaEditorTests: XCTestCase {
firstImage.simulate(fullImageHasBeenDownloaded: fullImage)
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil())
- expect(mediaEditor.visibleViewController).to(equal(mediaEditor.currentCapability?.viewController))
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil())
+ expect(mediaEditor.visibleViewController).to(equal((mediaEditor.currentCapability as? MockCapability)))
}
func testWhenTheFullImageIsAvailableUpdateTheImageReferences() {
@@ -455,8 +455,8 @@ class MediaEditorTests: XCTestCase {
mediaEditor.onFinishEditing = { images, _ in
returnedImages = images
}
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil()) // Wait capability appear
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil()) // Wait capability appear
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
// When
mediaEditor.hub.doneButton.sendActions(for: .touchUpInside)
@@ -474,10 +474,10 @@ class MediaEditorTests: XCTestCase {
mediaEditor.capabilityTapped(0)
firstImage.simulate(fullImageHasBeenDownloaded: image)
seconImage.simulate(fullImageHasBeenDownloaded: image)
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil()) // Wait capability appear
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil()) // Wait capability appear
// When
- mediaEditor.currentCapability?.onFinishEditing(image, [.rotate])
+ (mediaEditor.currentCapability as? MockCapability)?.onFinishEditing(image, [.rotate])
// Then
expect(mediaEditor.editedImagesIndexes).to(equal([0]))
@@ -496,32 +496,42 @@ class MediaEditorTests: XCTestCase {
firstImage.simulate(fullImageHasBeenDownloaded: image)
// Then
- expect(mediaEditor.currentCapability).toEventuallyNot(beNil())
+ expect((mediaEditor.currentCapability as? MockCapability)).toEventuallyNot(beNil())
}
}
-class MockCapability: MediaEditorCapability {
+class MockCapability: CapabilityViewController {
static var name = "MockCapability"
static var icon = UIImage()
var applyCalled = false
- var image: UIImage
+ var image: UIImage!
lazy var viewController: UIViewController = {
return UIViewController()
}()
- var onFinishEditing: (UIImage, [MediaEditorOperation]) -> ()
+ var onFinishEditing: ((UIImage, [MediaEditorOperation]) -> ())!
- var onCancel: (() -> ())
+ var onCancel: (() -> ())!
- required init(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) {
- self.image = image
- self.onFinishEditing = onFinishEditing
- self.onCancel = onCancel
+ static func initialize(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) -> CapabilityViewController {
+ let viewController = MockCapability()
+ viewController.image = image
+ viewController.onFinishEditing = onFinishEditing
+ viewController.onCancel = onCancel
+ return viewController
+ }
+
+ init() {
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
}
func apply(styles: MediaEditorStyles) {