Skip to content

Instantly share code, notes, and snippets.

@barabashd
Created October 15, 2022 12:42
Show Gist options
  • Save barabashd/4eb05455951eae008d0a57221d13612e to your computer and use it in GitHub Desktop.
Save barabashd/4eb05455951eae008d0a57221d13612e to your computer and use it in GitHub Desktop.
enum MediaLibrary {}
// MARK: - ViewModel
extension MediaLibrary {
final class ViewModel {
let progressCompletionThreshold: Double
var zoomPosition: ZoomPosition,
assetsCount: Int,
_zoomStatus: ZoomType?
var fraction: Double {
1 / Double(zoomPosition.rawValue)
}
var zoomStatus: ZoomType {
guard let zoomStatus = _zoomStatus else {
fatalError("zoomStatus can't be accessed as nil")
}
return zoomStatus
}
init(
zoomPosition: ZoomPosition = .middle,
assetsCount: Int,
progressCompletionThreshold: Double = 0.5
) {
self.zoomPosition = zoomPosition
self.assetsCount = assetsCount
self.progressCompletionThreshold = progressCompletionThreshold
}
}
}
// MARK: - ZoomPosition
extension MediaLibrary.ViewModel {
enum ZoomPosition: Int {
case min = 19,
middleToMin = 9,
middle = 5,
middleToMax = 3,
max = 1
private mutating func zoomOut() {
switch self {
case .min:
break
case .middleToMin:
self = .min
case .middle:
self = .middleToMin
case .middleToMax:
self = .middle
case .max:
self = .middleToMax
}
}
private mutating func zoomIn() {
switch self {
case .min:
self = .middleToMin
case .middleToMin:
self = .middle
case .middle:
self = .middleToMax
case .middleToMax:
self = .max
case .max:
break
}
}
mutating func finishZoom(for type: MediaLibrary.ViewModel.ZoomType) {
switch type {
case .zoomIn:
zoomIn()
case .zoomOut:
zoomOut()
}
}
}
}
// MARK: - ZoomType
extension MediaLibrary.ViewModel {
enum ZoomType {
case zoomIn,
zoomOut
func progress(from scale: CGFloat) -> CGFloat {
switch self {
case .zoomIn:
return scale - 1
case .zoomOut:
return 2 * (1 - scale)
}
}
}
}
extension MediaLibrary {
final class ViewController: UIViewController {
enum Section {
case main
}
private let viewModel: ViewModel
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .black
view.addSubview(collectionView)
collectionView.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.reusableIdentifier)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
])
return collectionView
}()
private lazy var dataSource: UICollectionViewDiffableDataSource<Section, Int> = {
let dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCell.reusableIdentifier, for: indexPath) as? ImageCell else {
fatalError("can't dequeue ImageCell")
}
cell.configure()
return cell
}
return dataSource
}()
private lazy var pinchGesture: UIPinchGestureRecognizer = {
let pinchGesture = UIPinchGestureRecognizer()
pinchGesture.addTarget(self, action: #selector(handlePinchGesture(_:)))
return pinchGesture
}()
private var transitionLayout: UICollectionViewTransitionLayout {
guard let collectionViewLayout = collectionView.collectionViewLayout as? UICollectionViewTransitionLayout else {
fatalError("Unknown UICollectionViewTransitionLayout")
}
return collectionViewLayout
}
// MARK: - UICollectionViewLayout
private var layout: UICollectionViewLayout {
UICollectionViewCompositionalLayout(
section: .init(
group: .horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(viewModel.fraction)
),
subitems: [
.init(
layoutSize: .init(
widthDimension: .fractionalWidth(viewModel.fraction),
heightDimension: .fractionalHeight(1)
)
)
]
)
)
)
}
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
applySnapshot()
enableGesture()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
private extension MediaLibrary.ViewController {
func applySnapshot() {
// TODO: - update with more elagant way
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
snapshot.appendSections([.main])
snapshot.appendItems(Array(0..<viewModel.assetsCount))
dataSource.apply(snapshot, animatingDifferences: false)
}
@objc func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
viewModel._zoomStatus = gesture.zoomType
viewModel.zoomPosition.finishZoom(for: viewModel.zoomStatus)
collectionView.startInteractiveTransition(to: layout) { [unowned self] _, _ in
self.enableGesture()
}
case .changed:
print(gesture.progress(with: viewModel.zoomStatus))
transitionLayout.transitionProgress = gesture.progress(with: viewModel.zoomStatus)
case .ended, .failed, .cancelled:
transitionLayout.transitionProgress > viewModel.progressCompletionThreshold ? finishPinchGesture() : cancelPinchGesture()
case .possible, .recognized:
break
@unknown default:
fatalError("Unknown new gesture status not handled yet!")
}
}
func finishPinchGesture() {
collectionView.finishInteractiveTransition()
disableGesture()
viewModel._zoomStatus = nil
}
func cancelPinchGesture() {
collectionView.cancelInteractiveTransition()
disableGesture()
}
func enableGesture() {
collectionView.addGestureRecognizer(pinchGesture)
}
func disableGesture() {
collectionView.removeGestureRecognizer(pinchGesture)
}
}
fileprivate extension UIPinchGestureRecognizer {
private static let minProgress: CGFloat = 0,
maxProgress: CGFloat = 1
var zoomType: MediaLibrary.ViewModel.ZoomType {
scale > 1 ? .zoomIn : .zoomOut
}
// TODO: - make zoom continious
func progress(with zoomStatus: MediaLibrary.ViewModel.ZoomType) -> CGFloat {
max(Self.minProgress, min(zoomStatus.progress(from: scale), Self.maxProgress))
}
}
extension UIColor {
static var random: UIColor {
let colors: [UIColor] = [.red, .yellow, .blue, .brown, .cyan, .darkGray, .green, .magenta, .orange]
return colors.randomElement()!
}
}
protocol HasReusableIdentifier {
static var reusableIdentifier: String { get }
}
extension HasReusableIdentifier {
static var reusableIdentifier: String { String(describing: self) }
}
final class ImageCell: UICollectionViewCell, HasReusableIdentifier {
func configure() {
backgroundColor = .random
}
}
@barabashd
Copy link
Author

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