์ฌ๊ฐํ์ ๊ฐ์งํ์ฌ ๊ณต์ ํ ์ ์๋ ์ฑ
- ๊ฐ์ง๋ ์ฌ๊ฐํ์ ์๋/์๋์ผ๋ก ์ดฌ์
- ์ดฌ์์ ์ /๋ฌด ๊ฒฐ์
- ์๋์ผ๋ก ์ดฌ์๋ ์ด๋ฏธ์ง๋ฅผ ๋ชจ์๋ฆฌ/๋ณ์ ํฐ์น์ ๋ฐ๋ผ ์์ ํ ์ ์๋ ๋ชจ๋ ์ง์
- ์ดฌ์๋ ์ด๋ฏธ์ง ์ญ์ ๋ฐ ํ์ ๊ทธ๋ฆฌ๊ณ ๊ณต์ ๊ธฐ๋ฅ
| ๋ฉ์ธํ๋ฉด | ํธ์งํ๋ฉด | ๋ฏธ๋ฆฌ๋ณด๊ธฐํ๋ฉด |
|---|---|---|
![]() |
| ์๋์ดฌ์ | UI ๋ณ๊ฒฝ | ํธ์งํ๋ฉด |
|---|---|---|
![]() |
![]() |
![]() |
๊ฐ View์ ๋ฐ๋ผ ์ ์ ์ ์ค์ฒ์ ๋ํ ๋์์ ๋ค์ด์ด๊ทธ๋จ์ผ๋ก ํํ
ํ๋ ์์ํฌ AVFoundation๋ฅผ ํ์ฉ
- ์นด๋ฉ๋ผ ์ดฌ์ํ๋ ์์์ ๋ฐ์ดํฐ์์ ์ด๋ฏธ์ง๋ฅผ View์ ๋ฐ์
- ์ดฌ์ ๋ฒํผ์ ํด๋ฆญ ์, Capture๋ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ด
- 1๋ฒ์์ ๋ฐ์์จ ์ด๋ฏธ์ง์์
CIDetector๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ๊ฐํ์ ๊ฐ์ง - (์๋ ๋ชจ๋) 3๋ฒ์์ ๊ฐ์ง ์๊ฐ์ด 1.5์ด ํ ์ดฌ์
- ์ดฌ์๋๋ฉด ์ข์ธก ํ๋จ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ ๋ง์ง๋ง ์ด๋ฏธ์ง ๋ฐ ๊ฐฏ์๋ฅผ ํํ
- ๋ชจ๋ ๋ณ๊ฒฝ์ ์ํ Custom Button ์ถ๊ฐ
์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ CIImage๋ก ๋ง๋ค๊ณ ๋๊ธฐ๊ธฐ ์ํด Async/Await ํจ์๋ฅผ ๋ง๋ค์ด์ ์ ๋ฌ
class Scanner: NSObject {
private var scanSuccessBlock: ((CIImage?) -> Void)?
func scan() async -> CIImage? {
let settings = AVCapturePhotoSettings()
photoOutput.capturePhoto(with: settings, delegate: self)
return await withCheckedContinuation { continuation in
scanSuccessBlock = { image in
continuation.resume(returning: image)
}
}
}
}
extension Scanner: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if error != nil {
scanSuccessBlock?(nil)
return
}
if let data = photo.fileDataRepresentation() {
let image = CIImage(data: data)
scanSuccessBlock?(image)
}
}
}๊ตญ๊ฐ๋ง๋ค ์ดฌ์์์ ์ ๋ฌด๊ฐ ์๊ฒ ์ง๋ง, ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์๋๋ก ์ฝ๋๋ฅผ ๋ฐ์

extension Scanner: AVCaptureVideoDataOutputSampleBufferDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
if isMuted {
AudioServicesDisposeSystemSoundID(1108)
} else {
AudioServicesPlaySystemSound(1108)
}
}
}์ฝ 1.5์ด๋ฅผ ์ธ์ํ๋ ๋์ ์ ๋๋ฉ์ด์ ๊ณผ ์ฐ๋ํ ์ ์๋๋ก ์งํ์ํฉ์ delegate๋ก ์ ๋ฌ 16.7%์ ์งํ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ฉฐ, 1.5์ด๊ฐ ๋๋ฌํ๊ฒ ๋์๋ค๋ฉด ์ดฌ์ํ ์ ์๋๋ก delegate๋ก ์ ๋ฌ
class AutoDetector {
// ...
private func startTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
@objc private func fireTimer() {
processing += 0.167
delegate?.autoDectectorDidDetected(self, processing: processing)
if processing >= 1.0 {
delegate?.autoDectectorCompleted(self)
resetTimer()
}
}
}CIDetector์ CIDetectorTypeRectangle ํํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง ๋ด ์ฌ๊ฐํ์ ๊ฐ์ง
๊ฐ์ง๋ CIRectangleFeature์ ๊ฐ์ ์ดฌ์๋ ์ด๋ฏธ์ง ๋ด ์ฌ๊ฐํ์ ์ขํ๋ก ๋ณด์ ์ด ํ์
์ข์ฐ ๋ฐ์ , ๊ฐ๋ ๋ณ๊ฒฝ ๋ฑ ๋๋ฐ์ด์ค์ ํฌ๊ธฐ์ ๋ง์ถ๊ธฐ ์ํด ๊ฐ์ ๋ณด์
์ดฌ์๋ ์ด๋ฏธ์ง๋ค์ ์ ์ ์ slide ์ ์ค์ณ๋ฅผ ํตํด ์ด๋ฏธ์ง๋ฅผ ํ ์ฅ์ฉ ๋ณผ ์ ์๋๋ก ํํ
์ดฌ์๋ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์๊ณ ํ์ /์ญ์ ๋ฅผ ํ ์ ์๋ ๋ชจ๋
slide๋ฅผ ํตํด ์ข, ์ฐ๋ก ์ดฌ์๋ ์ด๋ฏธ์ง๋ฅผ ๋ณผ ์ ์๋๋ก ํํ
์ด๋ฏธ์ง๊ฐ ์ญ์ ๋๋ index์ ๋ฐ๋ผ ์ ๋๋ฉ์ด์
์ ๋ค๋ฅด๊ฒ ํํ
@objc private func deleteImage() {
// ํ์ฌ content์ pageIndex ํ์
guard let viewController = self.pageViewController.viewControllers?.first,
let contentController = viewController as? ContentViewController,
let currentIndex = contentController.pageIndex else {
return
}
images.remove(at: currentIndex)
// ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ์ดฌ์ ๋ชจ๋๋ก ๋์๊ฐ๊ธฐ
guard !images.isEmpty else {
delegate?.previewViewControllerWillDisappear(self, images: images)
navigationController?.popViewController(animated: true)
return
}
// ์ญ์ ๋ index๊ฐ ๋ง์ง๋ง ๋ฒํธ์๋ค๋ฉด index - 1๋ก .reverse ํํ๋ก ํํ
guard currentIndex != images.count else {
let willAppearController = contentViewController(atIndex: currentIndex - 1)!
pageViewController.setViewControllers([willAppearController],
direction: .reverse,
animated: true)
setTitle(withIndex: currentIndex - 1)
return
}
// ์ ์กฐ๊ฑด์ ์ ์ธํ ๋ชจ๋ ๊ฒฝ์ฐ์ ์๋ ์ญ์ ๋ index์ ๋ฐ์ดํฐ๋ก .forward ํํ๋ก ํํ
let willAppearController = contentViewController(atIndex: currentIndex)!
pageViewController.setViewControllers([willAppearController],
direction: .forward,
animated: true)
setTitle(withIndex: currentIndex)
}ํด๋น ์ด๋ฏธ์ง์ orientation์ ์ฝ์ด์ ์๋ก์ด ์ด๋ฏธ์ง๋ก ์์ฑํ๋๋ก
UIImage(ciImage:scale:orientation:)๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐํ
func rotateCounterClockwise() -> UIImage? {
var newOrientation: UIImage.Orientation?
switch self.imageOrientation {
case .up:
newOrientation = .left
case .down:
newOrientation = .right
case .left:
newOrientation = .down
case .right:
newOrientation = .up
default:
break
}
// ...
}์ดฌ์๋ ์ด๋ฏธ์ง์์ ๊ฐ์ง๋ ์ฌ๊ฐํ์ด ์๋ค๋ฉด ์ด๋ฏธ์ง๋ฅผ ์๋ฅผ ์ ์๋ ์ฌ๊ฐํ์ด ์กด์ฌ ํฐ์น์ ๋ฐ๋ผ ์ฌ๊ฐํ์ ๋ชจ์์ ๋ณ๊ฒฝํ ์ ์๋๋ก ํํ
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) ํจ์๋ฅผ ์ฌ์ ์ํ์ฌ ํํ
ํฐ์น๋ฅผ ํ ๋๋ง๋ค View๊ฐ ๋์ฌ์ง๋ ์์น ๋ฐ ์ฌ๊ฐํ์ ๋ค์ ๊ทธ๋ฆด ์ ์๋๋ก ํํ
func draw(_ rect: CGRect) ํจ์๋ฅผ ์ฌ์ ์ํ์ฌ path๋ฅผ ํํ
์ฌ๊ฐํ ๋ชจ์๋ฆฌ๋ค์ ์ขํ๊ฐ ๋ฌ๋ผ์ง ๋๋ง๋ค ์๋ก ๊ทธ๋ ค์ง ์ ์๋๋ก setNeedsDisplay()๋ฅผ ํธ์ถ
์ขํ๋ค์ ์ฌ์ฉํ์ฌ context ์์ path๋ฅผ ๊ทธ๋ฆฌ๊ธฐ
View๋ frame์ ๊ธฐ๋ฐ์ผ๋ก ๊ทธ๋ ค์ง๊ธฐ ๋๋ฌธ์ ์ง์ ์ ํํ์ ํฐ์นํ ๋์๋ง ๋ณ๊ฒฝํ ์ ์๋๋ก
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?๋ฅผ ์ฌ์ ์ํ์ฌ์ ์ฌ์ฉ
๋ด๋ถ ์์ฑ์ผ๋ก ์ขํ๋ฅผ ๊ฐ์ง๊ณ ์์ด ์ง์ ๊ณผ ํฐ์นํ๋ ๋ถ๋ถ์ ๊ฑฐ๋ฆฌ๋ฅผ ์ธก์
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let line = line(start: startPoint, end: endPoint)
let distance = distance(to: line, from: point)
if distance <= 10 {
return self
}
return nil
}๋ํ, superView๋ฅผ ๋์ด๊ฐ์ง ์๋๋ก ํ๊ธฐ ์ํด์ outOfSuperview๋ก ์ฐธ/๊ฑฐ์ง์ ํ์ธ
์ ์ค์ฒ๊ฐ ๋์ด๊ฐ๋๋ผ๋ ์ด๋ํ์ง ๋ชปํ๋๋ก ๋ฐฉ์ง
private func outOfSuperview(through point: CGPoint) -> Bool {
guard let superview else {
return true
}
let limitX = superview.bounds.maxX
let limitY = superview.bounds.maxY
guard startPoint.x + point.x > 0,
startPoint.x + point.x < limitX,
endPoint.x + point.x > 0,
endPoint.x + point.x < limitX,
startPoint.y + point.y > 0,
startPoint.y + point.y < limitY,
endPoint.y + point.y > 0,
endPoint.y + point.y < limitY else {
return true
}
return false
}SwiftUI๋ก ์ฐ๊ฒฐ๋ ์ดฌ์ ์๋/์๋ ๋ชจ๋ ๋ฐ ์ดฌ์์ ์ /๋ฌด๋ฅผ ์ ํํ๋ ์ ๋๋ฉ์ด์ ์ ์ถ๊ฐํ View ์ด๋ฏธ์ง๋ฅผ ํญํ๋ฉด ์ต์ ์ ์ ํํ ์ ์๋ ์ ๋๋ฉ์ด์ ์ถ๊ฐ
private lazy var abilitiesController = UIHostingController(rootView: abilitiesView)
view.addSubview(abilitiesController.view)
abilitiesController.view.backgroundColor = .clear


