PhotoCropper
This is a simple image crop library for iOS I made for fun on Christmas
which doesn't support customized resizing by users.
This would be appropriate when limiting crop rate control to users.
Preview
Requirements
-
Swift 5
-
iOS 10.0 +
-
RxSwift 6.0
-
RxCocoa 6.0
-
RxGesture 4.0
-
SnapKit 5.0
-
Then 2.0
Installation
CocoaPods
PhotoCropper is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'PhotoCropper'
$ pod install
Swift Package Manager(SPM)
In Xcode, add as Swift package with this URL: https://github.com/aaronLab/PhotoCropper
Usage
-
Import
PhotoCropperon top of your view controller file. -
β οΈ If you want to change the ratio or some configurations, you can change the values inviewDidLoadby the singleton instance calledPhotoCropper"before you initialize thePhotoCropperView." For more information, please check the example files. -
Create a view using
PhotoCropperView(with:)in aview controller. -
Make constraints of the
PhotoCropperViewyou made in theview controller. -
Subscribe the
resultImage: PublishSubject<UIImage?>in thePhotoCropperViewyou declared and when the subjec emits the result image, you can go with that. -
To change the ratio, use
PhotoCropper.shared.ratio: BehaviorRelay<CGFloat>like below.- e.g.
PhotoCropper.shared.ratio.accept(2 / 3) - 2 / 3 means 2:3 ratio of the size.
- e.g.
-
Now you can crop the image using
PhtoCropperView.crop: PublishSubject<Void>- To do this, you can bind a button or send a signal manually like this:
PhotoCropoerVirw.crop.onNext(())
- To do this, you can bind a button or send a signal manually like this:
Customization
PhotoCropper
A singleton configuration instance.
β οΈ You should change the values before you initialize the view except the ratio and cornerRadius whose type isBehaviorRelay<CGFloat>.
You can change the valuse like this: PhotoCropper.shared.<PROPERTY_NAME>
/// The crop ratio
public var ratio: BehaviorRelay<CGFloat> { get }
/// Defines the `max zoom scale multiplier` by the `minZoomScale`
///
/// Default value is `5.0`
public var maxZoomScaleMultiplier: CGFloat
/// Hide grid
///
/// Default value is `false`
public var hideGrid: Boo
/// Hide frame border
///
/// Default value is `false`
public var hideFrameBorder: Bool
/// Hide the overlay views on the image
///
/// Default value is `false`
public var hideOverlay: Bool
/// Appearance
public var appearance: PhotoCropperAppearance
/// Defines the transition duration.
///
/// The default value is `0.25`
public var transitionDuration: CGFloat
/// Defines the animation duration.
///
/// The default value is `0.3`
public var animationDuration: CGFloat
/// Grid hide delay duration
///
/// - This defines the duration of when the grid will be hidden after user interaction done.
///
/// Default duration is `0.3`
public var gridHideDelayDuration: TimeInterval
/// Number of the grid lines
///
/// The number of the grid lines in the frame by each orientation.
///
/// Default duration is `3`
public var numberOfGridLines
PhotoCropperAppearance
Appearance configurations.
You can change the values from
PhotoCroppersingleton instance.
PhotoCropper.shared.appearance.<PROPERTY_NAME>
/// Corner radius of the frame.
///
/// Default value is `0`
public var cornerRadius: BehaviorRelay<CGFloat>
/// The color of `frame`.
///
/// Default value is `.white`
public var frameColor: UIColor
/// The color of `grid`.
///
/// Default value is `.white`
public var gridColor: UIColor
/// The color of the overlay for the image out of the frame.
///
/// Default value is `.black.withAlphaComponent(0.6)`
public var overlayColor: UIColor
/// The frame border width
///
/// Default value is `2.0`
public var frameBorderWidth: CGFloat
/// The grid width
///
/// Default value is `1.0`
public var gridWidth: CGFloat
Example
To run the example project, clone the repo, and run pod install from the Example directory first.
//
// ViewController.swift
// PhotoCropper
//
// Created by Aaron Lee on 12/26/2021.
// Copyright (c) 2021 Aaron Lee. All rights reserved.
//
import UIKit
import RxCocoa
import RxGesture
import RxSwift
import SnapKit
import Then
import PhotoCropper
class ViewController: UIViewController {
private var bag = DisposeBag()
private var doneButton = UIBarButtonItem(barButtonSystemItem: .done,
target: nil,
action: nil)
private var photoCropperView = PhotoCropperView(with: UIImage(named: "image"))
private var stackView = UIStackView()
.then {
$0.axis = .vertical
$0.spacing = 8
$0.distribution = .fill
$0.alignment = .fill
}
private var orientationSegmentedControl = UISegmentedControl(
items: Orientation.allCases.map { $0.rawValue }
)
private var ratioSegmentedControl = UISegmentedControl(
items: Ratio.allCases.map { $0.description(by: .landscape) }
)
override func viewDidLoad() {
super.viewDidLoad()
configureView()
layoutView()
bindRx()
}
}
// MARK: - Configure
extension ViewController {
private func configureView() {
title = "PhotoCropper Example"
navigationItem.rightBarButtonItem = doneButton
if #available(iOS 13.0, *) {
view.backgroundColor = .systemBackground
} else {
view.backgroundColor = .white
}
}
}
// MARK: - Layout
extension ViewController {
private func layoutView() {
view.addSubview(photoCropperView)
view.addSubview(stackView)
layoutPhotoCropperView()
layoutStackView()
layoutSegmentedControls()
}
private func layoutPhotoCropperView() {
photoCropperView.snp.makeConstraints {
$0.top.leading.equalTo(view.safeAreaLayoutGuide)
$0.trailing.equalTo(view.safeAreaLayoutGuide)
$0.bottom.equalTo(stackView.snp.top)
}
}
private func layoutStackView() {
stackView.snp.makeConstraints {
$0.leading.equalTo(view.safeAreaLayoutGuide).offset(16)
$0.trailing.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
}
}
private func layoutSegmentedControls() {
[orientationSegmentedControl, ratioSegmentedControl].forEach {
$0.selectedSegmentIndex = 0
stackView.addArrangedSubview($0)
$0.snp.makeConstraints {
$0.height.equalTo(30)
}
}
}
}
// MARK: - Bind
extension ViewController {
private func bindRx() {
bindInput()
bindOutput()
}
}
// MARK: - Input
extension ViewController {
private func bindInput() {
bindDoneButton()
bindOrientationSegmentedControlSelectedIndex()
bindRatioSegmentedControlSelectedIndex()
}
private func bindDoneButton() {
doneButton.rx.tap
.bind(to: photoCropperView.crop)
.disposed(by: bag)
}
private func bindOrientationSegmentedControlSelectedIndex(){
orientationSegmentedControl
.rx
.selectedSegmentIndex
.skip(1)
.map { [weak self] index -> CGFloat in
guard let self = self else { return 1 }
let orientation = Orientation.allCases[index]
let ratio = Ratio.allCases[self.ratioSegmentedControl.selectedSegmentIndex]
let ratioValue = ratio.ratio(by: orientation)
return ratioValue
}
.bind(to: PhotoCropper.shared.ratio)
.disposed(by: bag)
}
private func bindRatioSegmentedControlSelectedIndex() {
ratioSegmentedControl
.rx
.selectedSegmentIndex
.skip(1)
.map { [weak self] index -> CGFloat in
guard let self = self else { return 1 }
let orientation = Orientation.allCases[self.orientationSegmentedControl.selectedSegmentIndex]
let ratio = Ratio.allCases[index]
let ratioValue = ratio.ratio(by: orientation)
return ratioValue
}
.bind(to: PhotoCropper.shared.ratio)
.disposed(by: bag)
}
}
// MARK: - Output
extension ViewController {
private func bindOutput() {
photoCropperView.resultImage
.subscribe(onNext: { [weak self] image in
guard let self = self else { return }
let vc = ResultImageViewController()
vc.imageView.image = image
self.navigationController?.pushViewController(vc, animated: true)
})
.disposed(by: bag)
}
}
Author
Aaron Lee, [email protected]
License
PhotoCropper is available under the MIT license. See the LICENSE file for more info.

