diff --git a/Deartoday/Deartoday.xcodeproj/project.pbxproj b/Deartoday/Deartoday.xcodeproj/project.pbxproj index bf52c523..715c764b 100644 --- a/Deartoday/Deartoday.xcodeproj/project.pbxproj +++ b/Deartoday/Deartoday.xcodeproj/project.pbxproj @@ -19,6 +19,12 @@ 924814AF287EB53A001177B1 /* DialogMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924814AE287EB53A001177B1 /* DialogMessageView.swift */; }; 924814B1287EB5CC001177B1 /* DialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924814B0287EB5CC001177B1 /* DialogViewController.swift */; }; 928A8A6B2875FB7C000D4E99 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 928A8A6A2875FB7C000D4E99 /* GoogleService-Info.plist */; }; + 92C27E8F2882C4D900077FA3 /* DTCropBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E8E2882C4D900077FA3 /* DTCropBorderView.swift */; }; + 92C27E912882C81400077FA3 /* DTImageCropOverlayView-.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E902882C81400077FA3 /* DTImageCropOverlayView-.swift */; }; + 92C27E932882C8C600077FA3 /* DTImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E922882C8C600077FA3 /* DTImageCropView.swift */; }; + 92C27E952882CA0300077FA3 /* DTResizableCropOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E942882CA0300077FA3 /* DTResizableCropOverlayView.swift */; }; + 92C27E972882CDE900077FA3 /* DTImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E962882CDE900077FA3 /* DTImagePicker.swift */; }; + 92C27E992882CEE600077FA3 /* DTImageCropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C27E982882CEE600077FA3 /* DTImageCropViewController.swift */; }; 92CC3B9C287CA17000EA5617 /* CountDownLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CC3B9B287CA17000EA5617 /* CountDownLabel.swift */; }; 92CC3BA2287D62C400EA5617 /* VirtualSpaceDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CC3BA1287D62C400EA5617 /* VirtualSpaceDataModel.swift */; }; 92CC3BA4287D62DE00EA5617 /* VirtualSpaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CC3BA3287D62DE00EA5617 /* VirtualSpaceViewController.swift */; }; @@ -102,6 +108,12 @@ 924814AE287EB53A001177B1 /* DialogMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogMessageView.swift; sourceTree = ""; }; 924814B0287EB5CC001177B1 /* DialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogViewController.swift; sourceTree = ""; }; 928A8A6A2875FB7C000D4E99 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 92C27E8E2882C4D900077FA3 /* DTCropBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTCropBorderView.swift; sourceTree = ""; }; + 92C27E902882C81400077FA3 /* DTImageCropOverlayView-.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTImageCropOverlayView-.swift"; sourceTree = ""; }; + 92C27E922882C8C600077FA3 /* DTImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTImageCropView.swift; sourceTree = ""; }; + 92C27E942882CA0300077FA3 /* DTResizableCropOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTResizableCropOverlayView.swift; sourceTree = ""; }; + 92C27E962882CDE900077FA3 /* DTImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTImagePicker.swift; sourceTree = ""; }; + 92C27E982882CEE600077FA3 /* DTImageCropViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTImageCropViewController.swift; sourceTree = ""; }; 92CC3B9B287CA17000EA5617 /* CountDownLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountDownLabel.swift; sourceTree = ""; }; 92CC3BA1287D62C400EA5617 /* VirtualSpaceDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualSpaceDataModel.swift; sourceTree = ""; }; 92CC3BA3287D62DE00EA5617 /* VirtualSpaceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualSpaceViewController.swift; sourceTree = ""; }; @@ -216,6 +228,12 @@ isa = PBXGroup; children = ( 923061D72877550E00F04EDA /* TimeTravelViewController.swift */, + 92C27E8E2882C4D900077FA3 /* DTCropBorderView.swift */, + 92C27E902882C81400077FA3 /* DTImageCropOverlayView-.swift */, + 92C27E922882C8C600077FA3 /* DTImageCropView.swift */, + 92C27E942882CA0300077FA3 /* DTResizableCropOverlayView.swift */, + 92C27E962882CDE900077FA3 /* DTImagePicker.swift */, + 92C27E982882CEE600077FA3 /* DTImageCropViewController.swift */, ); path = Controller; sourceTree = ""; @@ -848,6 +866,7 @@ buildActionMask = 2147483647; files = ( 92DB35682875656C001E2006 /* DDSButton.swift in Sources */, + 92C27E972882CDE900077FA3 /* DTImagePicker.swift in Sources */, 923061D82877550E00F04EDA /* TimeTravelViewController.swift in Sources */, 98D912AE287D558A0088A7F9 /* DeartodayModel.swift in Sources */, 923061D6287754FA00F04EDA /* TimeTravelView.swift in Sources */, @@ -860,14 +879,18 @@ 92DB358628756B76001E2006 /* TempDataModel.swift in Sources */, 92DB3570287565E4001E2006 /* UILabel+.swift in Sources */, 92DB358828756B7D001E2006 /* TempService.swift in Sources */, + 92C27E992882CEE600077FA3 /* DTImageCropViewController.swift in Sources */, 98B4B5B1287EC93B00F4AD7A /* TimeTapeTableViewCell.swift in Sources */, 92DB358028756AE8001E2006 /* MoyaLoggingPlugin.swift in Sources */, 4A45D37E287F0B2F00592749 /* OpenBoxOnboardingViewController.swift in Sources */, 92CC3B9C287CA17000EA5617 /* CountDownLabel.swift in Sources */, 924814B1287EB5CC001177B1 /* DialogViewController.swift in Sources */, + 92C27E932882C8C600077FA3 /* DTImageCropView.swift in Sources */, 923061D4287754DB00F04EDA /* TimeTravelDataModel.swift in Sources */, 92DB35312875633D001E2006 /* AppDelegate.swift in Sources */, + 92C27E8F2882C4D900077FA3 /* DTCropBorderView.swift in Sources */, 98B4B5AC287EB5EF00F4AD7A /* Storyboard.swift in Sources */, + 92C27E952882CA0300077FA3 /* DTResizableCropOverlayView.swift in Sources */, 92DB35662875652F001E2006 /* OnboardingViewController.swift in Sources */, 92DB3560287564ED001E2006 /* MainModel.swift in Sources */, 92DB35722875661A001E2006 /* Constant.swift in Sources */, @@ -877,6 +900,7 @@ 92DB35582875646E001E2006 /* OnboardingModel.swift in Sources */, 98BFBCCF2879B85400F01F04 /* UIFont+.swift in Sources */, 92CC3BA6287D633600EA5617 /* VirtualSpaceCollectionViewCell.swift in Sources */, + 92C27E912882C81400077FA3 /* DTImageCropOverlayView-.swift in Sources */, 92DB357A28756A61001E2006 /* URLConstant.swift in Sources */, 4A45D380287F2BF800592749 /* LetterOnboardingViewController.swift in Sources */, 92DB356E287565AC001E2006 /* UIScreen+.swift in Sources */, diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTCropBorderView.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTCropBorderView.swift new file mode 100644 index 00000000..c09d6883 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTCropBorderView.swift @@ -0,0 +1,64 @@ +// +// DTCropBorderView.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit + +internal class DTCropBorderView: UIView { + private let kNumberOfBorderHandles: CGFloat = 8 + private let kHandleDiameter: CGFloat = 24 + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + self.backgroundColor = UIColor.clear + } + + override func draw(_ rect: CGRect) { + let context = UIGraphicsGetCurrentContext() + + context!.setStrokeColor(UIColor(red: 1, green: 1, blue: 1, alpha: 0.5).cgColor) + context!.setLineWidth(1.5) + context!.addRect((CGRect.init(x: kHandleDiameter / 2, y: kHandleDiameter / 2, width: rect.size.width - kHandleDiameter, height: rect.size.height - kHandleDiameter))) + context?.strokePath() + + context?.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 0.95)) + for handleRect in calculateAllNeededHandleRects() { + context?.fillEllipse(in: handleRect) + } + } + + private func calculateAllNeededHandleRects() -> [CGRect] { + let width = self.frame.width + let height = self.frame.height + + let leftColX: CGFloat = 0 + let rightColX = width - kHandleDiameter + let centerColX = rightColX / 2 + + let topRowY: CGFloat = 0 + let bottomRowY = height - kHandleDiameter + let middleRowY = bottomRowY / 2 + + let topLeft = CGRect.init(x: leftColX, y: topRowY, width: kHandleDiameter, height: kHandleDiameter) + let topCenter = CGRect.init(x: centerColX, y: topRowY, width: kHandleDiameter, height: kHandleDiameter) + let topRight = CGRect.init(x: rightColX, y: topRowY, width: kHandleDiameter, height: kHandleDiameter) + let middleRight = CGRect.init(x: rightColX, y: middleRowY, width: kHandleDiameter, height: kHandleDiameter) + let bottomRight = CGRect.init(x: rightColX, y: bottomRowY, width: kHandleDiameter, height: kHandleDiameter) + let bottomCenter = CGRect.init(x: centerColX, y: bottomRowY, width: kHandleDiameter, height: kHandleDiameter) + let bottomLeft = CGRect.init(x: leftColX, y: bottomRowY, width: kHandleDiameter, height: kHandleDiameter) + let middleLeft = CGRect.init(x: leftColX, y: middleRowY, width: kHandleDiameter, height: kHandleDiameter) + + return [topLeft, topCenter, topRight, middleRight, bottomRight, bottomCenter, bottomLeft, + middleLeft] + } +} diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropOverlayView-.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropOverlayView-.swift new file mode 100644 index 00000000..ec67f543 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropOverlayView-.swift @@ -0,0 +1,49 @@ +// +// DTImageCropOverlayView-.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit + +internal class DTImageCropOverlayView: UIView { + var cropSize: CGSize! + var toolbar: UIToolbar! + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + self.isUserInteractionEnabled = true + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + self.backgroundColor = .clear + self.isUserInteractionEnabled = true + } + + override func draw(_ rect: CGRect) { + let toolbarSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 0 : 54) + + let width = self.frame.width + let height = self.frame.height - toolbarSize + + let heightSpan = floor(height / 2 - self.cropSize.height / 2) + let widthSpan = floor(width / 2 - self.cropSize.width / 2) + + // fill outer rect + UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).set() + UIRectFill(self.bounds) + + // fill inner border + UIColor(red: 1, green: 1, blue: 1, alpha: 0.5).set() + UIRectFrame(CGRect.init(x: widthSpan - 2, y: heightSpan - 2, width: self.cropSize.width + 4, height: self.cropSize.height + 4)) + + // fill inner rect + UIColor.clear.set() + UIRectFill(CGRect.init(x: widthSpan, y: heightSpan, width: self.cropSize.width, height: self.cropSize.height)) + } +} diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropView.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropView.swift new file mode 100644 index 00000000..3345f932 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropView.swift @@ -0,0 +1,249 @@ +// +// DTImageCropView.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit +import QuartzCore + +private class ScrollView: UIScrollView { + fileprivate override func layoutSubviews() { + super.layoutSubviews() + + if let zoomView = self.delegate?.viewForZooming?(in: self) { + let boundsSize = self.bounds.size + var frameToCenter = zoomView.frame + + // center horizontally + if frameToCenter.size.width < boundsSize.width { + frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2 + } else { + frameToCenter.origin.x = 0 + } + + // center vertically + if frameToCenter.size.height < boundsSize.height { + frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2 + } else { + frameToCenter.origin.y = 0 + } + + zoomView.frame = frameToCenter + } + } +} + +internal class WDImageCropView: UIView, UIScrollViewDelegate { + var resizableCropArea = false + + private var scrollView: UIScrollView! + private var imageView: UIImageView! + private var cropOverlayView: DTImageCropOverlayView! + private var xOffset: CGFloat! + private var yOffset: CGFloat! + + private static func scaleRect(rect: CGRect, scale: CGFloat) -> CGRect { + return CGRect.init( + x: rect.origin.x * scale, + y: rect.origin.y * scale, + width: rect.size.width * scale, + height: rect.size.height * scale) + } + + var imageToCrop: UIImage? { + get { + return self.imageView.image + } + set { + self.imageView.image = newValue + } + } + + var cropSize: CGSize { + get { + return self.cropOverlayView.cropSize + } + set { + if let view = self.cropOverlayView { + view.cropSize = newValue + } else { + if self.resizableCropArea { + self.cropOverlayView = DTResizableCropOverlayView(frame: self.bounds, + initialContentSize: CGSize(width: newValue.width, height: newValue.height)) + } else { + self.cropOverlayView = DTImageCropOverlayView(frame: self.bounds) + } + self.cropOverlayView.cropSize = newValue + self.addSubview(self.cropOverlayView) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.isUserInteractionEnabled = true + self.backgroundColor = .black + + self.scrollView = ScrollView(frame: frame) + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = false + self.scrollView.backgroundColor = .clear + self.addSubview(self.scrollView) + + self.imageView = UIImageView(frame: self.scrollView.frame) + self.imageView.contentMode = .scaleAspectFit + self.imageView.backgroundColor = .black + self.scrollView.addSubview(self.imageView) + + self.scrollView.minimumZoomScale = self.scrollView.frame.width / self.scrollView.frame.height + self.scrollView.maximumZoomScale = 20 + self.scrollView.setZoomScale(1.0, animated: false) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { + if !resizableCropArea { + return self.scrollView + } + + let resizableCropView = cropOverlayView as! DTResizableCropOverlayView + let outerFrame = resizableCropView.cropBorderView.frame.insetBy(dx: -10, dy: -10) + + if outerFrame.contains(point) { + if resizableCropView.cropBorderView.frame.size.width < 60 || + resizableCropView.cropBorderView.frame.size.height < 60 { + return super.hitTest(point, with: event) + } + + let innerTouchFrame = resizableCropView.cropBorderView.frame.insetBy(dx: 30, dy: 30) + if innerTouchFrame.contains(point) { + return self.scrollView + } + + let outBorderTouchFrame = resizableCropView.cropBorderView.frame.insetBy(dx: -10, dy: -10) + if outBorderTouchFrame.contains(point) { + return super.hitTest(point, with: event) + } + + return super.hitTest(point, with: event) + } + + return self.scrollView + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.cropSize; + let toolbarSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 0 : 54) + self.xOffset = floor((self.bounds.width - size.width) * 0.5) + self.yOffset = floor((self.bounds.height - toolbarSize - size.height) * 0.5) + + let height = self.imageToCrop!.size.height + let width = self.imageToCrop!.size.width + + var factor: CGFloat = 0 + var factoredHeight: CGFloat = 0 + var factoredWidth: CGFloat = 0 + + if width > height { + factor = width / size.width + factoredWidth = size.width + factoredHeight = height / factor + } else { + factor = height / size.height + factoredWidth = width / factor + factoredHeight = size.height + } + + self.cropOverlayView.frame = self.bounds + self.scrollView.frame = CGRect.init(x: xOffset, y: yOffset, width: size.width, height: size.height) + self.scrollView.contentSize = CGSize(width: size.width, height: size.height) + self.imageView.frame = CGRect.init(x: 0, y: floor((size.height - factoredHeight) * 0.5), + width: factoredWidth, height: factoredHeight) + } + + func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? { + return self.imageView + } + + func croppedImage() -> UIImage { + var visibleRect = resizableCropArea ? + calcVisibleRectForResizeableCropArea() : calcVisibleRectForCropArea() + + let rectTransform = orientationTransformedRectOfImage(image: imageToCrop!) + visibleRect = visibleRect.applying(rectTransform); + + let imageRef = imageToCrop!.cgImage!.cropping(to: visibleRect) + let result = UIImage(cgImage: imageRef!, scale: imageToCrop!.scale, + orientation: imageToCrop!.imageOrientation) + + return result + } + + private func calcVisibleRectForResizeableCropArea() -> CGRect { + let resizableView = cropOverlayView as! DTResizableCropOverlayView + + var sizeScale = self.imageView.image!.size.width / self.imageView.frame.size.width + sizeScale *= self.scrollView.zoomScale + + var visibleRect = resizableView.contentView.convert(resizableView.contentView.bounds, + to: imageView) + visibleRect = WDImageCropView.scaleRect(rect: visibleRect, scale: sizeScale) + + return visibleRect + } + + private func calcVisibleRectForCropArea() -> CGRect { + let scaleWidth = imageToCrop!.size.width / cropSize.width + let scaleHeight = imageToCrop!.size.height / cropSize.height + var scale: CGFloat = 0 + + if cropSize.width == cropSize.height { + scale = max(scaleWidth, scaleHeight) + } else if cropSize.width > cropSize.height { + scale = imageToCrop!.size.width < imageToCrop!.size.height ? + max(scaleWidth, scaleHeight) : + min(scaleWidth, scaleHeight) + } else { + scale = imageToCrop!.size.width < imageToCrop!.size.height ? + min(scaleWidth, scaleHeight) : + max(scaleWidth, scaleHeight) + } + + // extract visible rect from scrollview and scale it + var visibleRect = scrollView.convert(scrollView.bounds, to:imageView) + visibleRect = WDImageCropView.scaleRect(rect: visibleRect, scale: scale) + + return visibleRect + } + + private func orientationTransformedRectOfImage(image: UIImage) -> CGAffineTransform { + var rectTransform: CGAffineTransform! + + switch image.imageOrientation { + case .left: + rectTransform = CGAffineTransform(rotationAngle: .pi / 2).translatedBy( + x: 0, y: -image.size.height) + case .right: + rectTransform = CGAffineTransform(rotationAngle: .pi / 2).translatedBy( + x: -image.size.width, y: 0) + case .down: + rectTransform = CGAffineTransform(rotationAngle: .pi).translatedBy( + x: -image.size.width, y: -image.size.height) + default: + rectTransform = CGAffineTransform(rotationAngle: .pi / 2).translatedBy( + x: 0, y: -image.size.height) + } + + return rectTransform.scaledBy(x: image.scale, y: image.scale) + } +} diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropViewController.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropViewController.swift new file mode 100644 index 00000000..bf147b95 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImageCropViewController.swift @@ -0,0 +1,145 @@ +// +// DTImagePickerViewController.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit +import CoreGraphics + +internal protocol DTImageCropControllerDelegate { + func imageCropController(imageCropController: DTImageCropViewController, didFinishWithCroppedImage croppedImage: UIImage) +} + +internal class DTImageCropViewController: UIViewController { + var sourceImage: UIImage! + var delegate: DTImageCropControllerDelegate? + var cropSize: CGSize! + var resizableCropArea = false + + private var croppedImage: UIImage! + + private var imageCropView: WDImageCropView! + private var toolbar: UIToolbar! + private var useButton: UIButton! + private var cancelButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + + self.automaticallyAdjustsScrollViewInsets = false + + self.title = "Choose Photo" + + self.setupNavigationBar() + self.setupCropView() + self.setupToolbar() + + if UIDevice.current.userInterfaceIdiom == .phone { + self.navigationController?.isNavigationBarHidden = true + } else { + self.navigationController?.isNavigationBarHidden = false + } + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + self.imageCropView.frame = self.view.bounds + self.toolbar?.frame = CGRect.init(x: 0, y: self.view.frame.height - 54, + width: self.view.frame.size.width, height: 54) + } + + @objc func actionCancel(sender: AnyObject) { + self.navigationController?.popViewController(animated: true) + } + + @objc func actionUse(sender: AnyObject) { + croppedImage = self.imageCropView.croppedImage() + self.delegate?.imageCropController(imageCropController: self, didFinishWithCroppedImage: croppedImage) + } + + private func setupNavigationBar() { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, + target: self, action: #selector(actionCancel)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Use", style: .plain, + target: self, action: #selector(actionUse)) + } + + private func setupCropView() { + self.imageCropView = WDImageCropView(frame: self.view.bounds) + self.imageCropView.imageToCrop = sourceImage + self.imageCropView.resizableCropArea = self.resizableCropArea + self.imageCropView.cropSize = cropSize + self.view.addSubview(self.imageCropView) + } + + private func setupCancelButton() { + self.cancelButton = UIButton() + self.cancelButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) + self.cancelButton.titleLabel?.shadowOffset = CGSize(width: 0, height: -1) + self.cancelButton.frame = CGRect(x: 0, y: 0, width: 58, height: 30) + self.cancelButton.setTitle("Cancel", for: .normal) + self.cancelButton.setTitleShadowColor( + UIColor(red: 0.118, green: 0.247, blue: 0.455, alpha: 1), for: .normal) + self.cancelButton.addTarget(self, action: #selector(actionCancel), for: .touchUpInside) + } + + private func setupUseButton() { + self.useButton = UIButton() + self.useButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) + self.useButton.titleLabel?.shadowOffset = CGSize(width: 0, height: -1) + self.useButton.frame = CGRect.init(x: 0, y: 0, width: 58, height: 30) + self.useButton.setTitle("Use", for: .normal) + self.useButton.setTitleShadowColor( + UIColor(red: 0.118, green: 0.247, blue: 0.455, alpha: 1), for: .normal) + self.useButton.addTarget(self, action: #selector(actionUse), for: .touchUpInside) + } + + private func toolbarBackgroundImage() -> UIImage { + let components: [CGFloat] = [1, 1, 1, 1, 123.0 / 255.0, 125.0 / 255.0, 132.0 / 255.0, 1] + + UIGraphicsBeginImageContextWithOptions(CGSize(width: 320, height: 54), true, 0) + + let context = UIGraphicsGetCurrentContext() + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorSpace: colorSpace, colorComponents: components, locations: nil, count: 2)! + + context!.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: 54), options: []) + + let viewImage = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + + return viewImage! + } + + private func setupToolbar() { + if UIDevice.current.userInterfaceIdiom == .phone { + self.toolbar = UIToolbar(frame: .zero) + self.toolbar.isTranslucent = true + self.toolbar.barStyle = .black + self.view.addSubview(self.toolbar) + + self.setupCancelButton() + self.setupUseButton() + + let info = UILabel(frame: CGRect.zero) + info.text = "" + info.textColor = UIColor(red: 0.173, green: 0.173, blue: 0.173, alpha: 1) + info.backgroundColor = UIColor.clear + info.shadowColor = UIColor(red: 0.827, green: 0.731, blue: 0.839, alpha: 1) + info.shadowOffset = CGSize(width: 0, height: 1) + info.font = UIFont.boldSystemFont(ofSize: 18) + info.sizeToFit() + + let cancel = UIBarButtonItem(customView: self.cancelButton) + let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let label = UIBarButtonItem(customView: info) + let use = UIBarButtonItem(customView: self.useButton) + + self.toolbar.setItems([cancel, flex, label, flex, use], animated: false) + } + } +} diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImagePicker.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImagePicker.swift new file mode 100644 index 00000000..420f64f8 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTImagePicker.swift @@ -0,0 +1,59 @@ +// +// DTImagePicker.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit + +public protocol DTImagePickerDelegate { + func imagePicker(imagePicker: DTImagePicker, pickedImage: UIImage) + func imagePickerDidCancel(imagePicker: DTImagePicker) +} + +public class DTImagePicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate, DTImageCropControllerDelegate { + public var delegate: DTImagePickerDelegate? + public var cropSize: CGSize! + public var resizableCropArea = false + + private var _imagePickerController: UIImagePickerController! + + public var imagePickerController: UIImagePickerController { + return _imagePickerController + } + + override init() { + super.init() + + self.cropSize = CGSize.init(width: 320, height: 320) + _imagePickerController = UIImagePickerController() + _imagePickerController.delegate = self + _imagePickerController.sourceType = .photoLibrary + } + + private func hideController() { + self._imagePickerController.dismiss(animated: true, completion: nil) + } + + public func imagePickerControllerDidCancel(picker: UIImagePickerController) { + if self.delegate?.imagePickerDidCancel != nil { + self.delegate?.imagePickerDidCancel(imagePicker: self) + } else { + self.hideController() + } + } + + public func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) { + let cropController = DTImageCropViewController() + cropController.sourceImage = info[UIImagePickerController.InfoKey.originalImage.rawValue] as! UIImage + cropController.resizableCropArea = self.resizableCropArea + cropController.cropSize = self.cropSize + cropController.delegate = self + picker.pushViewController(cropController, animated: true) + } + + func imageCropController(imageCropController: DTImageCropViewController, didFinishWithCroppedImage croppedImage: UIImage) { + self.delegate?.imagePicker(imagePicker: self, pickedImage: croppedImage) + } +} diff --git a/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTResizableCropOverlayView.swift b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTResizableCropOverlayView.swift new file mode 100644 index 00000000..532fddd5 --- /dev/null +++ b/Deartoday/Deartoday/Screen/TimeTravel/Controller/DTResizableCropOverlayView.swift @@ -0,0 +1,242 @@ +// +// DT.swift +// Deartoday +// +// Created by 소연 on 2022/07/16. +// + +import UIKit + +private struct DTResizableViewBorderMultiplyer { + var widthMultiplyer: CGFloat! + var heightMultiplyer: CGFloat! + var xMultiplyer: CGFloat! + var yMultiplyer: CGFloat! +} + +internal class DTResizableCropOverlayView: DTImageCropOverlayView { + private let kBorderCorrectionValue: CGFloat = 12 + + var contentView: UIView! + var cropBorderView: DTCropBorderView! + + private var initialContentSize = CGSize(width: 0, height: 0) + private var resizingEnabled: Bool! + private var anchor: CGPoint! + private var startPoint: CGPoint! + private var resizeMultiplyer = DTResizableViewBorderMultiplyer() + + override var frame: CGRect { + get { + return super.frame + } + set { + super.frame = newValue + + let toolbarSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 0 : 54) + let width = self.bounds.size.width + let height = self.bounds.size.height + + contentView?.frame = CGRect.init(x: (width - initialContentSize.width) / 2, + y: (height - toolbarSize - initialContentSize.height) / 2, + width: initialContentSize.width, + height: initialContentSize.height) + + cropBorderView?.frame = CGRect.init( + x: (width - initialContentSize.width) / 2 - kBorderCorrectionValue, + y: (height - toolbarSize - initialContentSize.height) / 2 - kBorderCorrectionValue, + width: initialContentSize.width + kBorderCorrectionValue * 2, + height: initialContentSize.height + kBorderCorrectionValue * 2) + } + } + + init(frame: CGRect, initialContentSize: CGSize) { + super.init(frame: frame) + + self.initialContentSize = initialContentSize + self.addContentViews() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + func touchesBegan(touches: Set, withEvent event: UIEvent?) { + if let touch = touches.first { + let touchPoint = touch.location(in: cropBorderView) + + anchor = self.calculateAnchorBorder(anchorPoint: touchPoint) + fillMultiplyer() + resizingEnabled = true + startPoint = touch.location(in: self.superview) + } + } + + func touchesMoved(touches: Set, withEvent event: UIEvent?) { + if let touch = touches.first { + if resizingEnabled! { + self.resizeWithTouchPoint(point: touch.location(in: self.superview)) + } + } + } + + override func draw(_ rect: CGRect) { + //fill outer rect + UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).set() + UIRectFill(self.bounds) + + //fill inner rect + UIColor.clear.set() + UIRectFill(self.contentView.frame) + } + + private func addContentViews() { + let toolbarSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 0 : 54) + let width = self.bounds.size.width + let height = self.bounds.size.height + + contentView = UIView(frame: CGRect.init(x: (width - initialContentSize.width) / 2, + y: (height - toolbarSize - initialContentSize.height) / 2, + width: initialContentSize.width, + height: initialContentSize.height)) + contentView.backgroundColor = .clear + self.cropSize = contentView.frame.size + self.addSubview(contentView) + + cropBorderView = DTCropBorderView(frame: CGRect.init( + x: (width - initialContentSize.width) / 2 - kBorderCorrectionValue, + y: (height - toolbarSize - initialContentSize.height) / 2 - kBorderCorrectionValue, + width: initialContentSize.width + kBorderCorrectionValue * 2, + height: initialContentSize.height + kBorderCorrectionValue * 2)) + self.addSubview(cropBorderView) + } + + private func calculateAnchorBorder(anchorPoint: CGPoint) -> CGPoint { + let allHandles = getAllCurrentHandlePositions() + var closest: CGFloat = 3000 + var anchor: CGPoint! + + for handlePoint in allHandles { + // Pythagoras is watching you :-) + let xDist = handlePoint.x - anchorPoint.x + let yDist = handlePoint.y - anchorPoint.y + let dist = sqrt(xDist * xDist + yDist * yDist) + + closest = dist < closest ? dist : closest + anchor = closest == dist ? handlePoint : anchor + } + + return anchor + } + + private func getAllCurrentHandlePositions() -> [CGPoint] { + let leftX: CGFloat = 0 + let rightX = cropBorderView.bounds.size.width + let centerX = leftX + (rightX - leftX) / 2 + + let topY: CGFloat = 0 + let bottomY = cropBorderView.bounds.size.height + let middleY = topY + (bottomY - topY) / 2 + + // starting with the upper left corner and then following the rect clockwise + let topLeft = CGPoint(x: leftX, y: topY) + let topCenter = CGPoint(x: centerX, y: topY) + let topRight = CGPoint(x: rightX, y: topY) + let middleRight = CGPoint(x: rightX, y: middleY) + let bottomRight = CGPoint(x: rightX, y: bottomY) + let bottomCenter = CGPoint(x: centerX, y: bottomY) + let bottomLeft = CGPoint(x: leftX, y: bottomY) + let middleLeft = CGPoint(x: leftX, y: middleY) + + return [topLeft, topCenter, topRight, middleRight, bottomRight, bottomCenter, bottomLeft, + middleLeft] + } + + private func resizeWithTouchPoint(point: CGPoint) { + // This is the place where all the magic happends + // prevent goint offscreen... + let border = kBorderCorrectionValue * 2 + var pointX = point.x < border ? border : point.x + var pointY = point.y < border ? border : point.y + pointX = pointX > self.superview!.bounds.size.width - border ? + self.superview!.bounds.size.width - border : pointX + pointY = pointY > self.superview!.bounds.size.height - border ? + self.superview!.bounds.size.height - border : pointY + + let heightChange = (pointY - startPoint.y) * resizeMultiplyer.heightMultiplyer + let widthChange = (startPoint.x - pointX) * resizeMultiplyer.widthMultiplyer + let xChange = -1 * widthChange * resizeMultiplyer.xMultiplyer + let yChange = -1 * heightChange * resizeMultiplyer.yMultiplyer + + var newFrame = CGRect.init( + x: cropBorderView.frame.origin.x + xChange, + y: cropBorderView.frame.origin.y + yChange, + width: cropBorderView.frame.size.width + widthChange, + height: cropBorderView.frame.size.height + heightChange); + newFrame = self.preventBorderFrameFromGettingTooSmallOrTooBig(frame: newFrame) + self.resetFrame(to: newFrame) + startPoint = CGPoint(x: pointX, y: pointY) + } + + private func preventBorderFrameFromGettingTooSmallOrTooBig(frame: CGRect) -> CGRect { + let toolbarSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 0 : 54) + var newFrame = frame + + if newFrame.size.width < 64 { + newFrame.size.width = cropBorderView.frame.size.width + newFrame.origin.x = cropBorderView.frame.origin.x + } + if newFrame.size.height < 64 { + newFrame.size.height = cropBorderView.frame.size.height + newFrame.origin.y = cropBorderView.frame.origin.y + } + + if newFrame.origin.x < 0 { + newFrame.size.width = cropBorderView.frame.size.width + + (cropBorderView.frame.origin.x - self.superview!.bounds.origin.x) + newFrame.origin.x = 0 + } + + if newFrame.origin.y < 0 { + newFrame.size.height = cropBorderView.frame.size.height + + (cropBorderView.frame.origin.y - self.superview!.bounds.origin.y) + newFrame.origin.y = 0 + } + + if newFrame.size.width + newFrame.origin.x > self.frame.size.width { + newFrame.size.width = self.frame.size.width - cropBorderView.frame.origin.x + } + + if newFrame.size.height + newFrame.origin.y > self.frame.size.height - toolbarSize { + newFrame.size.height = self.frame.size.height - + cropBorderView.frame.origin.y - toolbarSize + } + + return newFrame + } + + private func resetFrame(to frame: CGRect) { + cropBorderView.frame = frame + contentView.frame = frame.insetBy(dx: kBorderCorrectionValue, dy: kBorderCorrectionValue) + cropSize = contentView.frame.size + self.setNeedsDisplay() + cropBorderView.setNeedsDisplay() + } + + private func fillMultiplyer() { + // -1 left, 0 middle, 1 right + resizeMultiplyer.heightMultiplyer = anchor.y == 0 ? + -1 : anchor.y == cropBorderView.bounds.size.height ? 1 : 0 + // -1 up, 0 middle, 1 down + resizeMultiplyer.widthMultiplyer = anchor.x == 0 ? + 1 : anchor.x == cropBorderView.bounds.size.width ? -1 : 0 + // 1 left, 0 middle, 0 right + resizeMultiplyer.xMultiplyer = anchor.x == 0 ? 1 : 0 + // 1 up, 0 middle, 0 down + resizeMultiplyer.yMultiplyer = anchor.y == 0 ? 1 : 0 + } +}