-
News Article - EPSON Innovation Chanllenge ๊ณต์ ๋ด์ค ๊ธฐ์ฌ
-
์ค์ ๊ธ๋ก๋ฒ ๊ธฐ์ EPSON ํ๋ณด์ ์ฐ์ธ ์ ํ๋ธ ์์ ๋งํฌ
- ์น ํ๋ค ์ฑ์ AI ๊ธฐ์ ์ ํ์ฉํ์ฌ ์์ด๊ฐ ์์ํ ๊ทธ๋ฆผ์ ์์ฑ์ผ๋ก ์ ๋ ฅํ๋ฉด ํด๋น ๋์์ ์์ฑํ๊ณ , ์ด๋ฅผ ํ๋ฆฐํธํ์ฌ ์์น ๋์ด๋ฅผ ์ฆ๊ธธ ์ ์๋ ์ฑ์ ๋๋ค. ์์น ์ด ์๋ฃ๋ ํ์๋ ๋์์ ์ค์บํ์ฌ, ์์น ํ ์บ๋ฆญํฐ์ ํจ๊ป ์ฌ์ง์ ์ฐ์ด ์ถ์ต์ ๋จ๊ธธ ์ ์์ต๋๋ค.
- ์ก์ Epson Innovation Challenge ์ต์ฐ์์(1์) ์์, ์๊ธ 1,000๋ง ์
- ๊ตญ๋ด ์ต์ด ๊ฐ์ต ๋ฐ ์ธ๊ณ 3๋ฒ์งธ๋ก ์ด๋ฆฐ ๊ธ๋ก๋ฒ ๋ํ์์ ์ฐ์น
- ์ผ๋ณธ ๋ณธ์ฌ ์ธ์ด์ฝ ์ก์(Seiko EPSON Corporation) ์ต๊ณ ์ด์์ฑ
์์(COO) Yoshida Junkichi๋ก๋ถํฐ ๋ช
ํจ์ ๋ฐ๊ณ
๊ธ๋ก๋ฒ ์ฑ ์ถ์๋ฅผ ์ํ ์๋ถ ํ์ ์งํ - ํ๊ตญ์ก์ ๋ํ์ด์ฌ(CEO) Fujii Shigeo์ ๋คํธ์ํน
- ์ก์ EPSON๊ณผ ํํธ๋์ฝ ์ฒด๊ฒฐ, ๊ธ๋ก๋ฒ ์ฑ ๋ฐ์นญ ์งํ์ค
ํ๊ตญ์ก์ ๋ํ์ด์ฌ(CEO) ํ์ง์ด ์๊ฒ์ค
- 2024.06.01 ~ 2024.6.29 (4์ฃผ)
- iOS Developer 1๋ช , Back-end 2๋ช , Design 1๋ช , PM 1๋ช
-
Framework
UIKit
,Speech
,Alamofire
,Kingfisher
,SnapKit
,Photos
,AVFoundation
,Gifu
,Lottie
,AuthenticationServices
-
Design Pattern
MVC
- 1.์์ฑ์ธ์์ ํ์ฉํ ํ
์คํธ ์ถ์ถ ๋ฐ ๋์ ์์ฑ
- 2.์์ฑ๋ ๋์ ์ธ์ํ๊ธฐ(EPSON Connect API)
- 3.์ค์บํ ๋์์ ์ค์บํจ์์ ๊ด๋ฆฌํ๊ณ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ
- 4.์ค์บ๋ ์บ๋ฆญํฐ์ ๋ชจ์
์ ์ ์ฉํ์ฌ ์์ง์ด๊ฒ ๋ง๋ค๊ธฐ
- 5.์ค์บ๋ ์บ๋ฆญํฐ๋ฅผ ํ์ฉํ ์คํฐ์ปค ์ฌ์ง ์ดฌ์
- 6.์ ํ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ
์น ํ๋ค ์ฑ์ ์์ฑ์ธ์(Speech Recognition)์ ํ์ฉํ์ฌ ์ฌ์ฉ์์ ์์ฑ์ ์ค์๊ฐ์ผ๋ก ํ
์คํธ๋ก ๋ณํํฉ๋๋ค. ๋ณํ๋ ํ
์คํธ๋ AI ๊ธฐ๋ฐ ๋์ ์์ฑ API์ ์ฐ๋๋์ด, ์ฌ์ฉ์๊ฐ ์์ํ ๋ด์ฉ์ ๋์์ผ๋ก ์๊ฐํํฉ๋๋ค. ์์ฑ์ธ์์ ํจ์จ์ฑ์ ๋์ด๊ธฐ ์ํด iOS์ SFSpeechRecognizer๋ฅผ ์ฌ์ฉํ๊ณ , ๋น๋๊ธฐ ๋คํธ์ํฌ ์์ฒญ์ผ๋ก ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์์ผฐ์ต๋๋ค.
Speech
ํ๋ ์ ์ํฌ ์ฌ์ฉspeechRecognizer
: ํ๊ตญ์ด ์์ฑ ์ธ์์ ๋ด๋นrecognitionRequest
: ์์ฑ ๋ฐ์ดํฐ๋ฅผ SFSpeechRecognizer๋ก ์ ๋ฌํ๋ ์์ฒญ ๊ฐ์ฒดaudioEngine
: ๋ง์ดํฌ ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌ
let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ko-KR"))!
var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
var recognitionTask: SFSpeechRecognitionTask?
let audioEngine = AVAudioEngine()
- ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ์์ฑ ์
๋ ฅ์ด ์์๋๋ฉฐ audioEngine์ด ๋ง์ดํฌ ๋ฐ์ดํฐ๋ฅผ ์์ง
- ์ธ์ ์ค๊ฐ์ ๋ฒํผ์ ๋ค์ ๋๋ฅด๋ฉด ์์ฑ ์
๋ ฅ ์ข
๋ฃ
@objc func startRecording() {
if audioEngine.isRunning {
audioEngine.stop()
recognitionRequest?.endAudio()
voiceView.recordButton.isEnabled = false
} else {
startSpeechRecognition()
voiceView.recordButton.setTitle("Stop", for: [])
showBottomSheet()
}
}
- ์
๋ ฅ ๋ฐ์ดํฐ: audioEngine์ด ๋ง์ดํฌ ๋ฐ์ดํฐ๋ฅผ ์์งํด recognitionRequest๋ก ์ ๋ฌ
- ํ
์คํธ ๋ณํ: ์์ฑ ๋ฐ์ดํฐ๋ SFSpeechRecognizer๋ก ๋ณํ๋๋ฉฐ, bestTranscription ์์ฑ์์ ์ต์ ํ๋ ํ
์คํธ๋ฅผ ๊ฐ์ ธ์ด
- ์ค๊ฐ๊ฒฐ๊ณผ ์ฒ๋ฆฌ: shouldReportPartialResults๋ฅผ ํ์ฑํํ์ฌ ์ค์๊ฐ์ผ๋ก ๋ณํ๋ ํ
์คํธ ํ์
func startSpeechRecognition() {
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
let inputNode = audioEngine.inputNode
recognitionRequest?.shouldReportPartialResults = true
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest!) { (result, error) in
if let result = result {
self.recognizedText = result.bestTranscription.formattedString
self.bottomSheetView.updateText(self.recognizedText)
}
if error != nil || result?.isFinal == true {
self.audioEngine.stop()
inputNode.removeTap(onBus: 0)
self.recognitionRequest = nil
self.recognitionTask = nil
}
}
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, when in
self.recognitionRequest?.append(buffer)
}
audioEngine.prepare()
try? audioEngine.start()
}
- ๋ณํ๋ ํ
์คํธ๋ฅผ ์๋ฒ์ ์ ๋ฌ
- Alamofire๋ก ํต์ ๋คํธ์ํฌ ์์ฒญ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ์ฌ UI๊ฐ ๋ฉ์ถ์ง ์๋๋ก ์ค์
- ๋์ ์์ฑ: ์๋ฒ์์ ์์ฑ๋ ๋์์ URL๊ณผ ID๋ฅผ ๋ฐํ
- JSON ํ์์ผ๋ก ๋ณํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐํธํ๊ฒ ์๋ฒ๋ก ์ ์ก
- ์์ฑ๋ ๋์ ๊ฒฐ๊ณผ ํ์: ๋์์ ๋ก๋ํ์ฌ ์๋ก์ด ํ๋ฉด์์ ์ฌ์ฉ์์๊ฒ ์๊ฐํ
@objc func generateDrawing() {
let prompt = voiceView.textView.text ?? ""
let parameters: [String: Any] = ["prompt": prompt]
AF.request("https://api.zionhann.com/chillin/drawings/gen", method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { response in
switch response.result {
case .success(let value):
if let json = value as? [String: Any],
let drawingId = json["drawingId"] as? Int,
let urlString = json["url"] as? String,
let url = URL(string: urlString) {
let createDrawingVC = CreateDrawingViewController()
createDrawingVC.loadImage(from: url)
createDrawingVC.drawingId = drawingId
self.navigationController?.pushViewController(createDrawingVC, animated: true)
}
case .failure(let error):
print("Error: \(error)")
}
}
}
์น ํ๋ค ์ฑ์ Epson Connect API๋ฅผ ํ์ฉํ์ฌ ์์ฑ๋ ๋์์ ์ธ์ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ฌ์ฉ์๋ ์ถ๋ ฅ๋ฌผ์ ํฌ๊ธฐ๋ฅผ ์ ํํ ํ, ํ๋ฆฐํฐ์์ ํต์ ์ ํตํด ์ํ๋ ํฌ๊ธฐ๋ก ๋์์ ์ธ์ํ ์ ์์ต๋๋ค. ์ด ๊ณผ์ ์ ์ฌ์ฉ์์ ์ ํ์ ๊ธฐ๋ฐ์ผ๋ก ์๋ฒ์ ๋น๋๊ธฐ๋ก ํต์ ํ๋ฉฐ, ์์ ์ ์ธ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ณด์ฅํฉ๋๋ค.
- Epson Connect API: Epson ํ๋ฆฐํฐ์์ ํต์ ์ ํตํด ์ธ์๋ฅผ ์ฒ๋ฆฌ
- Alamofire: ์๋ฒ์์ ๋คํธ์ํฌ ํต์ ์ฒ๋ฆฌ
- UIAlertController: ์ฌ์ฉ์ ํ์ธ ๋ฉ์์ง ํ์
- JSON Encoding: ๋ฐ์ดํฐ ์ ์ก์ ์ํ JSON ํ์ ๋ณํ
- ์ฌ์ฉ์๊ฐ ์ถ๋ ฅ๋ฌผ ํฌ๊ธฐ๋ฅผ ์ ํํ๋ฉด, ์ ํ๋ ํฌ๊ธฐ๊ฐ ์๋ฒ๋ก ์ ๋ฌ๋์ด ์ธ์ ์ค๋น๋ฅผ ์งํํฉ๋๋ค. ํฌ๊ธฐ ์ ํ์ ๋ฒํผ์ ํตํด ์ด๋ฃจ์ด์ง๋ฉฐ, ์ ํ๋ ํฌ๊ธฐ๋
selectedSize
๋ณ์์ ์ ์ฅ๋ฉ๋๋ค.
@objc func sizeButtonTapped(_ sender: UIButton) {
let buttons = [createDrawingView.largeSizeButton, createDrawingView.mediumSizeButton, createDrawingView.smallSizeButton]
buttons.forEach { button in
if button == sender {
button.backgroundColor = .lightGray
switch button {
case createDrawingView.largeSizeButton:
selectedSize = "LARGE"
case createDrawingView.mediumSizeButton:
selectedSize = "MEDIUM"
case createDrawingView.smallSizeButton:
selectedSize = "SMALL"
default:
selectedSize = "LARGE"
}
} else {
button.backgroundColor = .white
}
}
}
- Epson Connect API๋ฅผ ํตํด ๋์ ID์ ํฌ๊ธฐ ๋ฐ์ดํฐ๋ฅผ JSON ํ์์ผ๋ก ์๋ฒ์ ์ ๋ฌํฉ๋๋ค. ๋คํธ์ํฌ ์์ฒญ์ Alamofire๋ฅผ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌํ๋ฉฐ, ์ฑ๊ณต ์ฌ๋ถ์ ๋ฐ๋ผ ์ฌ์ฉ์์๊ฒ ๊ฒฐ๊ณผ๋ฅผ ์๋ฆฝ๋๋ค.
func printDrawing() {
guard let drawingId = drawingId else { return }
let parameters: [String: Any] = ["drawingId": drawingId, "scale": selectedSize]
AF.request("https://api.zionhann.com/chillin/drawings/print", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: ["Content-Type": "application/json"]).responseJSON { response in
if let statusCode = response.response?.statusCode {
if statusCode == 200 {
print("Print Success: \(response)")
self.showPrintSuccessAlert()
} else {
print("Print Failure: \(response)")
self.showPrintFailureAlert()
}
} else {
print("Error: \(response.error ?? AFError.explicitlyCancelled)")
self.showPrintFailureAlert()
}
self.createDrawingView.printSuccessReturnUI(false)
}
}
- ์น ํ๋ค ์ฑ์ ์ฌ์ฉ์๊ฐ ์ค์บํ ๋์์ ์ค์บํจ์์ ํ์ธํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ์๋ฒ์์ ํต์ ์ ํตํด ์ฌ์ฉ์๊ฐ ์์ฑํ ๋์์ ๋ถ๋ฌ์ ๊น๋ํ UI๋ก ํ์ํฉ๋๋ค. ํ์ด์ง๋ค์ด์
์ ํตํด ํ ๋ฒ์ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค์ง ์๊ณ , ์คํฌ๋กค ์ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๋ฐฉ์์ผ๋ก ํจ์จ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
- Alamofire: ์๋ฒ์์ ์ค์บ ๋ฐ์ดํฐ๋ฅผ ๋น๋๊ธฐ๋ก ๊ฐ์ ธ์ค๊ธฐ
- UICollectionView: ์ฌ์ฉ์ ์ธํฐํ์ด์ค์์ ๋์์ ์ ๋ ฌ ๋ฐ ํ์
- Kingfisher: ์๋ฒ์์ ๋ฐ์ ์ด๋ฏธ์ง URL์ ๋น ๋ฅด๊ฒ ๋ ๋๋ง
- Pagination: ๋ฐ์ดํฐ ์์ฒญ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด ํ์ด์ง ๋จ์๋ก ๋ฐ์ดํฐ ๋ก๋
- ์ค์บํจ์ ์ ์ฅ๋ ๋์์ ๋ถ๋ฌ์ค๊ธฐ ์ํด ์๋ฒ์ GET ์์ฒญ์ ๋ณด๋
๋๋ค. ์์ฒญ ์, ํ์ฌ ํ์ด์ง ์ ๋ณด์ ๋ฐ์ดํฐ๋ฅผ ํํฐ๋งํ๊ธฐ ์ํ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํฉ๋๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋์ ๋ก๋ฉ ์ํ๋ฅผ ์ ์งํ๋ฉฐ, ์๋ฒ ์๋ต์ ๋ฐ๋ผ UI๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
func fetchDrawings(page: Int) {
guard !isLoading, hasMoreData else { return }
isLoading = true
let url = "https://api.zionhann.com/chillin/drawings"
let parameters: [String: Any] = ["type": "GENERATED", "page": page]
AF.request(url, method: .get, parameters: parameters).responseJSON { response in
self.isLoading = false
switch response.result {
case .success(let value):
if let json = value as? [String: Any], let data = json["data"] as? [[String: Any]] {
let newDrawings = data.compactMap { Drawing(dictionary: $0) }
if newDrawings.isEmpty {
self.hasMoreData = false
} else {
self.drawings.append(contentsOf: newDrawings)
self.scanView.collectionView.reloadData()
}
}
case .failure(let error):
print("Error: \(error)")
}
}
}
- ๋ถ๋ฌ์จ ๋์ ๋ฐ์ดํฐ๋ฅผ
UICollectionView
๋ฅผ ์ฌ์ฉํด ํ์ํฉ๋๋ค. ๊ฐ ๋์์CollectionViewCell
์ ์ด๋ฏธ์ง์ ํจ๊ป ๊น๋ํ๊ฒ ๋ ๋๋ง๋๋ฉฐ, ์ฌ์ฉ์๊ฐ ์ค์บํจ์ ๋์์ ์ฝ๊ฒ ํ์ํ ์ ์์ต๋๋ค.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return drawings.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
let drawing = drawings[indexPath.item]
cell.configure(with: drawing)
return cell
}
- ์คํฌ๋กค ์์น๋ฅผ ๊ฐ์งํด ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ์คํฌ๋กค์ ๋ด๋ ค ๋์ ๋๋ฌํ๋ฉด ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ์์ฒญํ๊ณ , ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋ณํฉํ์ฌ ํ์ํฉ๋๋ค.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let height = scrollView.frame.size.height
if offsetY > contentHeight - height * 2 {
currentPage += 1
fetchDrawings(page: currentPage)
}
}
-
์ค์บํจ์์ ๋์์ ์ ํํ๋ฉด, ํด๋น ๋์์ ์์ธ ํ๋ฉด์ผ๋ก ์ด๋ํฉ๋๋ค. ์์ธ ํ๋ฉด์์๋ ์ ํํ ๋์์ ํ๋ํ๊ฑฐ๋, ์ธ์ ๋ฐ ์ ์ฅ๊ณผ ๊ฐ์ ์ถ๊ฐ ์์ ์ ์ํํ ์ ์์ต๋๋ค.
-
1.
Photos
Framework ํ์ฉ -
PHPhotoLibrary
๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ๊ทผ ๊ถํ์ ์์ฒญํ๊ณ , ์ ์ฅ ์์ ์ ์ํํฉ๋๋ค. -
2. ๊ถํ ์์ฒญ ๋ฐ ์ด๋ฏธ์ง ์ ์ฅ
-
์ฌ์ฉ์๊ฐ ๊ถํ์ ํ์ฉํ๋ฉด, ์ ํํ ๋์ ์ด๋ฏธ์ง๋ฅผ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅํฉ๋๋ค.
-
์ ์ฅ ์๋ฃ ํ ์๋ฆผ ์ฐฝ์ผ๋ก ์ฌ์ฉ์์๊ฒ ํผ๋๋ฐฑ์ ์ ๊ณตํฉ๋๋ค.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let drawing = drawings[indexPath.item]
let scanCheckVC = ScanCheckViewController()
scanCheckVC.drawing = drawing
scanCheckVC.drawingId = drawing.drawingId
let scanCheckNaviController = UINavigationController(rootViewController: scanCheckVC)
scanCheckNaviController.modalPresentationStyle = .overFullScreen
self.present(scanCheckNaviController, animated: true, completion: nil)
}
@objc func saveImageButtonTapped() {
guard let image = scanCheckView.resultImageView.image else { return }
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
} else {
print("Authorization denied")
}
}
}
@objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
print("Error saving image: \(error.localizedDescription)")
} else {
print("Image saved successfully")
let alert = UIAlertController(title: "์ ์ฅ ์๋ฃ", message: "์ด๋ฏธ์ง๊ฐ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ๋์์ต๋๋ค.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ํ์ธ", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
- ์ฌ์ฉ์๋ ์ค์บ๋ ์บ๋ฆญํฐ๋ฅผ ์ ํํ์ฌ ๋ค์ํ ๋ชจ์
ํจ๊ณผ๋ฅผ ์ ์ฉํ ์ ์์ผ๋ฉฐ, ์์ฑ๋ ์ ๋๋ฉ์ด์
์ GIF๋ก ์ ์ฅํ์ฌ ๊ฐ์งํ ์ ์์ต๋๋ค. ์บ๋ฆญํฐ์ ๋ชจ์
์ ์กฐํฉ์ผ๋ก ์ฐฝ์์ ์ด๊ณ ์ฌ๋ฏธ์๋ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
- ์ฌ์ฉ์๋ UICollectionView๋ฅผ ํตํด ์ค์บ๋ ์บ๋ฆญํฐ๋ฅผ ์ ํํฉ๋๋ค.
- ์ดํ ๋ชจ์
ํ์
๋ฒํผ(๋์ค, ์๋
, ์ ํ, ์ข๋น ๋ฑ) ์ค ํ๋๋ฅผ ์ ํํ์ฌ ์์ง์์ ์ค์ ํฉ๋๋ค.
- ์ ํ๋ ๋ชจ์
ํ์
์ ์๋ฒ ์์ฒญ ์ ์ ๋ฌ๋์ด ์ ๋๋ฉ์ด์
์ฒ๋ฆฌ์ ๋ฐ์๋ฉ๋๋ค.
@objc func motionButtonTapped(_ sender: UIButton) {
let buttons = [motionCheckView.danceButton, motionCheckView.helloButton, motionCheckView.jumpButton, motionCheckView.zombieButton]
buttons.forEach { button in
if button == sender {
button.backgroundColor = .lightGray
switch button {
case motionCheckView.danceButton:
motionSelected = "dance"
case motionCheckView.helloButton:
motionSelected = "hello"
case motionCheckView.jumpButton:
motionSelected = "jump"
case motionCheckView.zombieButton:
motionSelected = "zombie"
default:
break
}
} else {
button.backgroundColor = .white
}
}
}
- Alamofire๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ ์บ๋ฆญํฐ์ ๋ชจ์
ํ์
์ ์๋ฒ์ ์ ๋ฌํฉ๋๋ค.
- ์๋ฒ๋ ์์ฒญ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก GIF ํ์ผ์ ์์ฑํ๊ณ , ๊ฒฐ๊ณผ URL์ ๋ฐํํฉ๋๋ค.
@objc func motionStartButtonTapped() {
guard let drawing = drawing else { return }
guard let motionSelected = motionSelected else { return }
let url = "https://api.zionhann.com/chillin/motion/\(drawing.drawingId)"
let parameters: [String: Any] = ["motionType": motionSelected]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON { response in
switch response.result {
case .success(let value):
if let json = value as? [String: Any], let gifUrlString = json["url"] as? String, let gifUrl = URL(string: gifUrlString) {
self.presentMotionResultViewController(with: gifUrl)
} else {
self.showAlert(message: "ํด๋น ๊ทธ๋ฆผ์ ์์ง์ด๊ฒ ํ ์ ์์ต๋๋ค!")
}
case .failure:
self.showAlert(message: "๋ชจ์
์ ์ฉ์ ์คํจํ์ต๋๋ค.")
}
}
}
- ์๋ฒ์์ ๋ฐํ๋ GIF URL์ ์ฌ์ฉํด
Gifu
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ ๋๋ฉ์ด์ ์ UI์ ํ์ํฉ๋๋ค. - GIF ํ์ผ์
Photos
Framework๋ฅผ ํตํด ์ฌ์ฉ์์ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅํ ์ ์์ต๋๋ค. ์ ์ฅ ํ์๋ ์๋ฆผ์ ํตํด ์ ์ฅ ์ฑ๊ณต ์ฌ๋ถ๋ฅผ ์๋ ค์ค๋๋ค.
func saveGifToLibrary(data: Data) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else { return }
PHPhotoLibrary.shared().performChanges({
let options = PHAssetResourceCreationOptions()
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: data, options: options)
}) { success, error in
if success {
self.showSaveSuccessAlert()
} else {
self.showSaveErrorAlert(error: error)
}
}
}
}
- ์ค์บ๋ ์บ๋ฆญํฐ๋ฅผ ์ฌ์ง ์ดฌ์์ ํ์ฉํ์ฌ ์ฌ์ฉ์ ๋ง์ถคํ ์คํฐ์ปค ์ด๋ฏธ์ง๋ฅผ ์์ฑํ ์ ์๋๋ก ๊ตฌํ๋์์ต๋๋ค.
- ์ฌ์ฉ์๋ ์นด๋ฉ๋ผ ํ๋ฉด์ ์บ๋ฆญํฐ ์ด๋ฏธ์ง๋ฅผ ์ค๋ฒ๋ ์ดํ์ฌ ์ค์๊ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ํ์์ ์ดฌ์ํ ์ ์์ต๋๋ค.
- ์ดฌ์๋ ์ด๋ฏธ์ง๋ ์คํฐ์ปค๊ฐ ํฌํจ๋ ํํ๋ก ์ฌ์ฉ์ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ๋ฉ๋๋ค.
- ์ค์บ๋ ์บ๋ฆญํฐ ์ด๋ฏธ์ง๋ฅผ ์นด๋ฉ๋ผ ํ๋ฉด์ ์ค๋ฒ๋ ์ดํ์ฌ ์คํฐ์ปค์ฒ๋ผ ๋ฐฐ์นํฉ๋๋ค.
- ์ฌ์ฉ์๋ ๋๋๊ทธ(์ด๋)์ ํ์น ์ ์ค์ฒ(ํฌ๊ธฐ ์กฐ์ )๋ฅผ ํตํด ์คํฐ์ปค์ ์์น์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ ์ ์์ต๋๋ค.
func createOverlayView() -> UIView {
let overlayView = UIView(frame: view.bounds)
overlayView.backgroundColor = .clear
overlayView.isUserInteractionEnabled = true
let removeImageViewFrame = motionCheckView.convert(motionCheckView.removeImageView.frame, to: overlayView)
let removeImageView = UIImageView(frame: removeImageViewFrame)
removeImageView.image = motionCheckView.removeImageView.image
removeImageView.contentMode = .scaleAspectFill
removeImageView.isUserInteractionEnabled = true
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
removeImageView.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
removeImageView.addGestureRecognizer(pinchGesture)
overlayView.addSubview(removeImageView)
return overlayView
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else { return }
let translation = gesture.translation(in: view.superview)
view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y)
gesture.setTranslation(.zero, in: view.superview)
}
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
guard let view = gesture.view else { return }
view.transform = view.transform.scaledBy(x: gesture.scale, y: gesture.scale)
gesture.scale = 1.0
}
- ์ฌ์ง ์ดฌ์ ์ ์ค๋ฒ๋ ์ด๋ ์คํฐ์ปค๋ฅผ ํฌํจํ์ฌ ์ด๋ฏธ์ง๋ฅผ ์บก์ฒํฉ๋๋ค.
- ๊ฒฐ๊ณผ ์ด๋ฏธ์ง๋ ์คํฐ์ปค๊ฐ ์ฌ์ง ์์ ๋ฐฐ์น๋ ํํ๋ก ์ฌ์ฉ์ ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ๋ฉ๋๋ค.
@objc func shutterButtonTapped() {
if let imagePickerController = self.presentedViewController as? UIImagePickerController {
imagePickerController.takePicture()
}
}
func takeSnapshotWithOverlayAndSave(capturedImage: UIImage, isFrontCamera: Bool) {
var imageToSave = capturedImage
if isFrontCamera {
imageToSave = UIImage(cgImage: capturedImage.cgImage!, scale: capturedImage.scale, orientation: .leftMirrored)
}
UIGraphicsBeginImageContextWithOptions(imageToSave.size, false, imageToSave.scale)
imageToSave.draw(in: CGRect(origin: .zero, size: imageToSave.size))
if let overlayImage = motionCheckView.removeImageView.image {
overlayImage.draw(in: CGRect(x: 100, y: 100, width: 200, height: 200))
}
let combinedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if let combinedImage = combinedImage {
UIImageWriteToSavedPhotosAlbum(combinedImage, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
@objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
print("Error saving image: \(error.localizedDescription)")
} else {
print("Image saved successfully")
}
}
- ๊ธฐ๋ณธ ์นด๋ฉ๋ผ UI๋ฅผ ์จ๊ธฐ๊ณ ์ปค์คํ
๋ฒํผ(์ดฌ์, ์ ํ, ์ทจ์)์ ์ถ๊ฐํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์์ผฐ์ต๋๋ค.
- ์นด๋ฉ๋ผ ํ๋ฉด์ ์ ์ฌ๊ฐํ ๋น์จ๋ก ์กฐ์ ๋์ด ์คํฐ์ปค ์ฌ์ง ์ดฌ์์ ์ ํฉํฉ๋๋ค.
func presentCameraWithOverlay() {
let cameraVC = UIImagePickerController()
cameraVC.delegate = self
cameraVC.sourceType = .camera
cameraVC.cameraOverlayView = createOverlayView()
cameraVC.showsCameraControls = false
let scale = UIScreen.main.bounds.width / UIScreen.main.bounds.width
cameraVC.cameraViewTransform = CGAffineTransform(scaleX: scale, y: scale)
present(cameraVC, animated: true, completion: nil)
}
- ์น ํ๋ค ์ฑ์ Apple Sign-In ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ฌ ์ฌ์ฉ์๊ฐ ์ ํ ๊ณ์ ์ ํ์ฉํด ์ฑ์ ๊ฐํธํ๊ณ ์์ ํ๊ฒ ๋ก๊ทธ์ธํ ์ ์๋๋ก ๊ตฌํ๋์์ต๋๋ค. ์ด ๊ธฐ๋ฅ์
AuthenticationServices
ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ๋ฉฐ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ณดํธํ๋ฉด์ ์ํํ ์ธ์ฆ ๊ณผ์ ์ ์ ๊ณตํฉ๋๋ค.
- ์ ํ ๋ก๊ทธ์ธ ๋ฒํผ ์ ๊ณต
ASAuthorizationAppleIDButton
์ ํ์ฉํ ์ง๊ด์ ์ด๊ณ ๊น๋ํ UI ๊ตฌ์ฑ- ์ฌ์ฉ์์๊ฒ ์ ํ ๊ณ์ ์ ํตํด ๊ฐํธํ ๋ก๊ทธ์ธ ์ต์
์ ๊ณต
- ์ธ์ฆ ์์ฒญ ๋ฐ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ
- ์ฌ์ฉ์์ ์ด๋ฆ, ์ด๋ฉ์ผ, ๊ณ ์ ์๋ณ์๋ฅผ ์์ ํ๊ฒ ์์งํ์ฌ UserDefaults์ ์๋ฒ์ ์ ์ฅ
- ์์ง๋ ์ ๋ณด๋ ํฅํ ์๋ ๋ก๊ทธ์ธ ๋ฐ ๊ฐ์ธํ๋ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ ๋ฐ ํ์ฉ
- ์๋ ๋ก๊ทธ์ธ
- ์ด์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฑ ์คํ ์ ์๋์ผ๋ก ์ฌ์ฉ์ ์ธ์ฆ ์ํ
- ์ฌ์ฉ์ ํธ์์ฑ์ ํฌ๊ฒ ํฅ์
- ์๋ฒ ํต์ ๋ฐ ํ ํฐ ๊ด๋ฆฌ
Alamofire
๋ฅผ ์ฌ์ฉํด ์๋ฒ์ ํต์ ํ์ฌ ์ธ์ฆ ํ ํฐ์ ์์ ํ๊ฒ ์ฒ๋ฆฌ- ์๋ฒ๋ก๋ถํฐ ๋ฐ์ ํ ํฐ์ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์ ์ธ์
๊ด๋ฆฌ
- ๋ก๊ทธ์ธ ์คํจ ์ฒ๋ฆฌ
- ๋ค์ํ ์ค๋ฅ ์ฝ๋์ ๋ฐ๋ผ ์ฌ์ฉ์์๊ฒ ์ ์ ํ ํผ๋๋ฐฑ ์ ๊ณต
- ๋ก๊ทธ์ธ ์ฌ์๋ ์ต์
๊ณผ ๊ด๋ จ ์ ๋ณด๋ฅผ ์๋ด
- ์ ํ ์ธ์ฆ ์์ฒญ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ASAuthorizationController๋ฅผ ํตํด ์ ํ ์ธ์ฆ ์์ฒญ์ ์ํ
@objc func appleLoginButtonTapped() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
- ์ธ์ฆ ์ฑ๊ณต ์ ์ฌ์ฉ์ ์ ๋ณด ์ฒ๋ฆฌ ์ ํ ๊ณ์ ์ธ์ฆ์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋๋ฉด, ์ฌ์ฉ์์ ์ด๋ฆ, ์ด๋ฉ์ผ, ๊ณ ์ ์๋ณ์๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ์ด ์ ๋ณด๋ ์๋ฒ์์ ํต์ ์ ์ฌ์ฉ๋๋ฉฐ, UserDefaults์ ์ ์ฅ
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
let userIdentifier = appleIDCredential.user
let email = appleIDCredential.email ?? "Email not provided"
let fullName = appleIDCredential.fullName?.formatted() ?? "Name not provided"
// ์๋ฒ๋ก ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด ์ ์ก
let parameters: [String: Any] = ["code": String(data: appleIDCredential.authorizationCode!, encoding: .utf8) ?? ""]
let headers: HTTPHeaders = ["Content-Type": "application/json"]
AF.request("https://api.zionhann.com/chillin/auth/oauth2/token", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success(let value):
print("Authentication Successful: \(value)")
case .failure(let error):
print("Authentication Failed: \(error.localizedDescription)")
}
}
UserDefaults.standard.set(userIdentifier, forKey: "User")
}
- ์ธ์ฆ ์คํจ ์ฒ๋ฆฌ ์ธ์ฆ ๊ณผ์ ์ค ์คํจ ์, ์ฌ์ฉ์์๊ฒ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ๊ณ ์ ์ ํ ๋์ฒ ๋ฐฉ๋ฒ์ ์๋ด
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
if let authorizationError = error as? ASAuthorizationError {
switch authorizationError.code {
case .canceled:
print("User canceled the login request.")
case .failed:
print("Authorization request failed.")
case .invalidResponse:
print("Invalid response received.")
default:
print("Unknown error occurred.")
}
}
}
ยฉ 2024 ์ด์นํ (SeungHyeon Lee). All rights reserved.
-
๋ณธ GitHub ๋ฆฌ๋๋ฏธ๋ ํ ํ๋ก์ ํธ ์น ํ๋ค๋ฅผ ์๊ฐํ๊ธฐ ์ํด ์์ฑ๋์์ผ๋ฉฐ, ๋ฆฌ๋๋ฏธ์ ๋ชจ๋ ๋ด์ฉ์ ์ด์นํ(SeungHyeon Lee)์ด ์ง์ ์์ฑํ์์ต๋๋ค.
-
์ด ํ๋ก์ ํธ๋ iOS Developer 1๋ช , Back-end 2๋ช , PM 1๋ช ์ผ๋ก ๊ตฌ์ฑ๋ ํ์ด ํ๋ ฅํ์ฌ ์งํํ ๊ฒฐ๊ณผ๋ฌผ์ ๋๋ค. ๊ทธ๋ฌ๋ ๋ฆฌ๋๋ฏธ์ ํฌํจ๋ ๋ชจ๋ ํ ์คํธ, ์ด๋ฏธ์ง ๋ฐฐ์น, ์ค๋ช , ๊ธฐ์ ์คํ ์๊ฐ ๋ฑ์ ์ ์ ์์ ์์ ๋ฐํ๋๋ค.
-
๋ฆฌ๋๋ฏธ์ ๊ด๋ จํ์ฌ ๋ฌธ์๊ฐ ํ์ํ์ ๊ฒฝ์ฐ ์๋ ์ด๋ฉ์ผ๋ก ์ฐ๋ฝํด ์ฃผ์ธ์: [email protected]