Skip to content

Latest commit

ย 

History

History
530 lines (435 loc) ยท 19.5 KB

README.md

File metadata and controls

530 lines (435 loc) ยท 19.5 KB

README

WHAAT: Auto Translate ๐Ÿ” 



์†Œ๊ฐœ

  • ์นด๋ฉ”๋ผ๊ฐ€ ๋น„์ถ˜ ๊ธ€์ž๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฒˆ์—ญํ•ด์ฃผ๋Š” ์•ฑ
  • iOS 16 ์ด์ƒ์˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
  • ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ : 23/10/09~23/10/17

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

API ํ™œ์šฉ์„ ์œ„ํ•ด์„œ SecretKey.plist ํŒŒ์ผ ์ƒ์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. plist์— ๋“ค์–ด๊ฐ€๋Š” ๋‚ด์šฉ์€ ์•„๋ž˜ ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

  • PapagoClientSecret : Naver Developer์˜ Secret Key(String)
  • PapagoClientId : Naver Developer์˜ Client ID(String)

๋ชฉ์ฐจ

  1. ๐Ÿ‘ฉโ€๐Ÿ’ป ํŒ€์› ์†Œ๊ฐœ
  2. ๐Ÿ“… ํƒ€์ž„ ๋ผ์ธ
  3. ๐Ÿ› ๏ธ ํ™œ์šฉ ๊ธฐ์ˆ 
  4. ๐Ÿ“Š ์‹œ๊ฐํ™” ๊ตฌ์กฐ
  5. ๐Ÿ“ฑ ์‹คํ–‰ ํ™”๋ฉด
  6. ๐Ÿ“Œ ํ•ต์‹ฌ ๊ฒฝํ—˜
  7. ๐Ÿงจ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
  8. ๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ


๐Ÿ‘ฉโ€๐Ÿ’ป ํŒ€์› ์†Œ๊ฐœ

maxhyunm
([email protected])


๐Ÿ“… ํƒ€์ž„ ๋ผ์ธ

๋‚ ์งœ ๋‚ด์šฉ
2023.10.09. ํ™œ์šฉํ•  ๊ธฐ์ˆ ์Šคํƒ ๊ฒฐ์ •
ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๊ตฌ์ƒ
2023.10.10. Network, Model, Error ๊ด€๋ จ ๊ธฐ๋ณธ ํƒ€์ž… ์ƒ์„ฑ
DataScanner ํ™”๋ฉด์ธ์‹ ๊ธฐ๋Šฅ ๊ตฌํ˜„
2023.10.11. API ํ†ต์‹ ์„ ์œ„ํ•œ ๋„คํŠธ์›Œํฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„
Repository ํƒ€์ž… ์ƒ์„ฑ
ViewModel ์ƒ์„ฑ
2023.10.12. DataScannerViewController๋ฅผ MainViewController ๋‚ด๋ถ€๋กœ ์ด๋™
์–ธ์–ด ์„ ํƒ์„ ์œ„ํ•œ PickerView ์ถ”๊ฐ€
Item ํƒ€์ž… ์ƒ์„ฑ
RxSwift ์ถ”๊ฐ€ํ•˜์—ฌ ViewModel ๋ณ€๊ฒฝ
2023.10.13. TextPickerField ํƒ€์ž… ์ƒ์„ฑ
๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ํ”ฝ์Šค
2023.10.14. NetworkManager ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ
ColorNamespace ์ถ”๊ฐ€
Localization ์ถ”๊ฐ€
2023.10.15. ๋„คํŠธ์›Œํฌ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
ResultViewModel ๋ถ„๋ฆฌ
2023.10.16. ์•„์ด์ฝ˜ ๋ฐ ๋Ÿฐ์น˜์Šคํฌ๋ฆฐ ์ƒ์„ฑ
Copy ์ž‘์—… ์ถ”๊ฐ€
2023.10.17. Toast ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
ResultViewController -> TextLabel๋กœ ๋ณ€๊ฒฝ
README ์ž‘์„ฑ


๐Ÿ› ๏ธ ํ™œ์šฉ ๊ธฐ์ˆ 

Framework Architecture Concurrency API Dependency Manager
UIKit MVVM RxSwift Papago ๋ฒˆ์—ญ, ์–ธ์–ด๊ฐ์ง€ SPM


๐Ÿ“Š ์‹œ๊ฐํ™” ๊ตฌ์กฐ

File Tree

.
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ TranslateApp
    โ”œโ”€โ”€ TranslateApp
    โ”‚ย ย  โ”œโ”€โ”€ en.lproj
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Localizable.strings
    โ”‚ย ย  โ”œโ”€โ”€ ko.lproj
    โ”‚ย ย  โ”‚   โ””โ”€โ”€ Localizable.strings
    โ”‚ย ย  โ”œโ”€โ”€ App
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ AppDelegate.swift
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ SceneDelegate.swift
    โ”‚ย ย  โ”œโ”€โ”€ Network
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ NetworkConfiguration.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ NetworkManager.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ URLSessionDataTaskProtocol.swift
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ URLSessionProtocol.swift
    โ”‚ย ย  โ”œโ”€โ”€ Model
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DTO
    โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ LanguageDetectorDTO.swift
    โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ TranslatorDTO.swift
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ TranslatorRepository.swift
    โ”‚ย ย  โ”œโ”€โ”€ Utility
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ AlertBuilder.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ CustomColors.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DecodingManager.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ KeywordArgument.swift
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Languages.swift
    โ”‚ย ย  โ”œโ”€โ”€ Error
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ APIError.swift
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DecodingError.swift
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ TranslateError.swift
    โ”‚ย ย  โ”œโ”€โ”€ Presentation
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ View
    โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Protocol
    โ”‚ย ย  โ”‚ย ย      โ”‚ย ย  โ”œโ”€โ”€ ToastShowable.swift
    โ”‚ย ย  โ”‚ย ย      โ”‚ย ย  โ””โ”€โ”€ ViewModelType.swift
    โ”‚ย ย  โ”‚ย ย      โ””โ”€โ”€ Main
    โ”‚ย ย  โ”‚ย ย       ย ย  โ”œโ”€โ”€ LanguagePickerField.swift
    โ”‚ย ย  โ”‚ย ย       ย ย  โ”œโ”€โ”€ TextLabel.swift
    โ”‚ย ย  โ”‚ย ย       ย ย  โ”œโ”€โ”€ MainViewController.swift
    โ”‚ย ย  โ”‚ย ย       ย ย  โ””โ”€โ”€ MainViewModel.swift
    โ”‚ย ย  โ”œโ”€โ”€ Resource
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Assets.xcassets
    โ”‚ย ย  โ”œโ”€โ”€ Info.plist
    โ”‚ย ย  โ””โ”€โ”€ SecretKey.plist
    โ”œโ”€โ”€ TranslateApp.xcodeproj
    โ””โ”€โ”€ TranslateAppTests
        โ”œโ”€โ”€ TranslateApp.xctestplan
        โ”œโ”€โ”€ TestDouble.swift
        โ”œโ”€โ”€ NetworkTexts.swift
        โ””โ”€โ”€ ViewModelTest.swift

UML



๐Ÿ“ฑ ์‹คํ–‰ ํ™”๋ฉด

์–ธ์–ด ๊ฐ์ง€ ๋ฒˆ์—ญ ์–ธ์–ด ์„ ํƒ ๊ฒฐ๊ณผ ๋ณต์‚ฌ


๐Ÿ“Œ ํ•ต์‹ฌ ๊ฒฝํ—˜

๐ŸŒŸ MVVM + Repository ํŒจํ„ด ํ™œ์šฉ

input ํƒ€์ž…๊ณผ output ํƒ€์ž…์„ ๋ถ„๋ฆฌํ•œ View Model์„ ์ ์šฉํ•œ MVVM ํŒจํ„ด์„ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋„คํŠธ์›Œํฌ ์ฒ˜๋ฆฌ์™€ ์—ฐ๊ฒฐ๋˜๋Š” ๋กœ์ง์€ Repository ํƒ€์ž…์œผ๋กœ ๋ถ„๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
protocol MainViewModelType {
    var inputs: MainViewModelInputsType { get }
    var outputs: MainViewModelOutputsType { get }
}

protocol MainViewModelInputsType {
    func scanText(_ input: String)
    func touchUpTranslate(source: String, target: String)
}

protocol MainViewModelOutputsType {
    var outputItem: PublishRelay<String> { get }
    var errorMessage: PublishRelay<String> { get }
}

protocol ViewModelWithError {
    var errorMessage: PublishRelay<String> { get }
    func handle(error: Error)
}
final class TranslatorRepository {
    let networkManager: NetworkManager
    
    init(networkManager: NetworkManager) {
        self.networkManager = networkManager
    }

    func detectLanguage(_ text: String, completion: @escaping(Result<Languages, Error>) -> Void) {
        ...
    }
    
    func translate(source: Languages, target: Languages, text: String, completion: @escaping(Result<String, Error>) -> Void) {
        ...
    }
}

๐ŸŒŸ DataScannerViewController๋ฅผ ํ™œ์šฉํ•œ ์–ธ์–ด ์Šค์บ” ๊ตฌํ˜„

DataScannerViewController๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด iOS 16 ์ด์ƒ์ด์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด ๊ณ ๋ฏผ๋˜์–ด, ์ฒ˜์Œ์—๋Š” VNDocumentCameraViewController๋ฅผ ์‚ฌ์šฉํ•˜๋ ค ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์ด๋ผ๊ธฐ๋ณด๋‹ค๋Š” ํ•œ ์ฐจ๋ก€ ์ด๋ฏธ์ง€๋ฅผ ์Šค์บ”ํ•œ ๋’ค ๊ทธ ์ค‘์—์„œ ์„ ํƒํ•˜์—ฌ ํ…์ŠคํŠธ ์ธ์‹์„ ํ•˜๊ณ , ๊ทธ ํ›„์— ๋ฒˆ์—ญ์„ ํ•˜๋Š” ๋“ฑ ๋‹จ๊ณ„๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ์‹ค์‹œ๊ฐ„ ๋ฒˆ์—ญ์ด๋ผ๋Š” ๋Š๋‚Œ์ด ๋“ค์ง€ ์•Š๋Š” ๊ฒƒ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ DataScannerViewController์™€ DataScannerViewControllerDelegate๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋ฉ”๋ผ์— ๋น„์นœ ํ…์ŠคํŠธ๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
private let dataScanner: DataScannerViewController = {
    let scanner = DataScannerViewController(recognizedDataTypes: [.text()],
                                            qualityLevel: .balanced,
                                            recognizesMultipleItems: false,
                                            isHighFrameRateTrackingEnabled: true,
                                            isHighlightingEnabled: true)
    scanner.view.translatesAutoresizingMaskIntoConstraints = false
    return scanner
}()

...

extension MainViewController: DataScannerViewControllerDelegate {
    func dataScanner(_ dataScanner: DataScannerViewController,
                     didAdd addedItems: [RecognizedItem],
                     allItems: [RecognizedItem]) {
       translateLabel.removeFromSuperview()
        
        guard let item = addedItems.first,
              case .text(let text) = item else { return }

        let frame = CGRect(x: text.bounds.topLeft.x,
                           y: dataScanner.view.frame.origin.y + text.bounds.topLeft.y,
                           width: text.bounds.topRight.x - text.bounds.topLeft.x,
                           height: text.bounds.bottomRight.y - text.bounds.topRight.y)

        translateLabel.resetLabel(frame: frame)
        viewModel.inputs.scanText(text.transcript)
    }
}

๐ŸŒŸ RxSwift๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๊ตฌํ˜„

View์™€ View Model ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์€ ๋ฌผ๋ก , View ๋‚ด๋ถ€์˜ ๋ฒ„ํŠผ์ด๋‚˜ PickerView ์•ก์…˜์—๋„ RxSwift๋ฅผ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
final class MainViewModel: MainViewModelType, MainViewModelOutputsType, ViewModelWithError {
    ...
    var outputItem = PublishRelay<String>()
    var errorMessage = PublishRelay<String>()
    ...
}
private func bindOutput() {
    viewModel.outputs.outputItem
        .subscribe(on: MainScheduler.instance)
        .bind { [weak self] output in
            guard let self else { return }
            self.translateLabel.text = output
            self.view.addSubview(self.translateLabel)
        }
        .disposed(by: disposeBag)
}
func bindPickerView(disposeBag: DisposeBag) {
    Observable.just(category.menu).bind(to: pickerView.rx.itemTitles) { _, item in
        return item
    }.disposed(by: disposeBag)

    pickerView.selectRow(0, inComponent: 0, animated: false)
    self.text = category.menu.first
}

๐ŸŒŸ Builder ํŒจํ„ด ํ™œ์šฉ

Builder ํŒจํ„ด์„ ํ™œ์šฉํ•ด Alert ์ฒ˜๋ฆฌ๋ฅผ ์กฐ๊ธˆ ๋” ๊น”๋”ํžˆ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
struct AlertBuilder {
    let configuration: AlertConfiguration
    
    init(prefferedStyle: UIAlertController.Style) {
        ...
    }
    
    @discardableResult
    func setTitle(_ title: String) -> Self {
        ...
    }
    
    @discardableResult
    func setMessage(_ message: String) -> Self {
        ...
    }
    
    @discardableResult
    func addAction(_ actionType: AlertActionType, action: ((UIAlertAction) -> Void)? = nil) -> Self {
        ...
    }
    
    func build() -> UIAlertController {
        ...
    }
}
let alertBuilder = AlertBuilder(prefferedStyle: .alert)
    .setMessage(errorMessage)
    .addAction(.confirm) { action in
        self.dismiss(animated: true)
    }
    .build()

๐ŸŒŸ Generic์„ ํ™œ์šฉํ•œ ๋ฒ”์šฉ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„

๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ Entity๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” DecodingManager์˜ ๋ฉ”์„œ๋“œ๋ฅผ Generic์œผ๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
final class DecodingManager { static let shared = DecodingManager() let decoder = JSONDecoder()
private init() {}

func decode<T: Decodable>(_ data: Data?) throws -> T {
    guard let data = data,
          let decodedData = try? decoder.decode(T.self, from: data) else {
        throw DecodingError.decodingFailure
    }
    return decodedData
}

}

๐ŸŒŸ UITextField์™€ UIPickerView๋ฅผ ํ™œ์šฉํ•œ ์„ ํƒ์ฐฝ ๊ตฌํ˜„

Text Field์— Picker View๋ฅผ ์—ฐ๊ฒฐํ•œ LanguagePickerField ํƒ€์ž…์„ ์ƒ์„ฑํ•˜์—ฌ ์–ธ์–ด ์„ ํƒ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
final class LanguagePickerField: UITextField {
    private let pickerView = UIPickerView()
    
    ...
    
    inputView = pickerView
    
    ...
    
    func bindPickerView(disposeBag: DisposeBag) {
        Observable.just(category.menu).bind(to: pickerView.rx.itemTitles) { _, item in
            return item
        }.disposed(by: disposeBag)

        pickerView.selectRow(0, inComponent: 0, animated: false)
        self.text = category.menu.first
    }
    
    ...
    
}

๐ŸŒŸ UIToolBar ํ™œ์šฉ

UIToolBar์™€ flexibleSpace๋ฅผ ํ™œ์šฉํ•˜์—ฌ PickerView ์ƒ๋‹จ ๋ฒ„ํŠผ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
private let toolbar: UIToolbar = {
    let toolBar = UIToolbar(frame: CGRect(origin: .zero, size: CGSize(width: UIScreen.main.bounds.width, height: 35)))
    toolBar.translatesAutoresizingMaskIntoConstraints = false
    toolBar.barStyle = .default
    toolBar.isTranslucent = true
    toolBar.tintColor = Colors.barButtonTitle
    toolBar.sizeToFit()
    return toolBar
}()
    
...
    
let cancelButton = UIBarButtonItem(primaryAction: cancel)
let selectButton = UIBarButtonItem(primaryAction: select)
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

toolbar.setItems([cancelButton, flexibleSpace, selectButton], animated: true)
toolbar.isUserInteractionEnabled = true
self.inputAccessoryView = toolbar

๐ŸŒŸ GestureRecognizer ํ™œ์šฉ

label์— Long Press Guesture๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ์˜ค๋ž˜ ํ„ฐ์น˜ํ•˜๋ฉด ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
private func addGestureRecognizer() {
    let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
    translateLabel.addGestureRecognizer(gestureRecognizer)
}

@objc private func handleLongPress() {
    showToast(Constants.copyToastMessage, withDuration: 3.0, delay: 0.1)
    UIPasteboard.general.string = translateLabel.text
}

๐ŸŒŸ Localization ์ถ”๊ฐ€

์ง€์—ญํ™”๋ฅผ ์œ„ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์˜์–ด์™€ ํ•œ๊ตญ์–ด๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ์ฝ”๋“œ
"languageAuto" = "Detect Language";
"languageKorean" = "Korean";
"languageEnglish" = "English";
"languageJapanese" = "Japanese";
...
"languageAuto" = "์–ธ์–ด ๊ฐ์ง€";
"languageKorean" = "ํ•œ๊ตญ์–ด";
"languageEnglish" = "์˜์–ด";
"languageJapanese" = "์ผ๋ณธ์–ด";
...
...
switch self {
case .auto:
    return String(format: NSLocalizedString("languageAuto", comment: "์–ธ์–ด ๊ฐ์ง€"))
case .korean:
    return String(format: NSLocalizedString("languageKorean", comment: "ํ•œ๊ตญ์–ด"))
case .english:
    return String(format: NSLocalizedString("languageEnglish", comment: "์˜์–ด"))
case .japanese:
    return String(format: NSLocalizedString("languageJapanese", comment: "์ผ๋ณธ์–ด"))
case .chineseSimple:
...


๐Ÿงจ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

1๏ธโƒฃ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด๋ถ€์— DataScannerViewController ๋ฐฐ์น˜

๐Ÿšจ ๋ฌธ์ œ์ 
์ฒ˜์Œ์—๋Š” DataScannerViewController์—์„œ ์ง€๋‹ˆ๊ณ  ์žˆ๋Š” overlayContainerView์— Button๊ณผ TextField๋ฅผ ๋ฐฐ์น˜ํ•˜๋ ค ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ด๋‹น ์š”์†Œ๋“ค๊ณผ ์ƒํ˜ธ์ž‘์šฉ์„ ํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค. ๋ทฐ๋ฅผ splitํ•ด์•ผ ํ• ์ง€, ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด๋ถ€์— DataScannerViewController๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ์„์ง€ ๋งŽ์€ ๊ณ ๋ฏผ์„ ๊ฑฐ์ณค์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
DataScannerViewController์˜ View๋งŒ์„ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์— Subview๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

private func configureScanner() {
    dataScanner.delegate = self

    addChild(dataScanner)
    view.addSubview(dataScanner.view)

    NSLayoutConstraint.activate([
        dataScanner.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
        dataScanner.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        dataScanner.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
        dataScanner.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
    ])

    dataScanner.didMove(toParent: self)
}

2๏ธโƒฃ UILabel์˜ ์œ„์น˜ ์„ค์ •

๐Ÿšจ ๋ฌธ์ œ์ 
DataScannerViewContoller๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์Šค์บ”ํ•œ ํ…์ŠคํŠธ์˜ ์œ„์น˜์ •๋ณด๋„ ํ•จ๊ป˜ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋•Œ๋ฌธ์— ์นด๋ฉ”๋ผ๊ฐ€ ์ธ์‹ํ•œ ํ…์ŠคํŠธ ์œ„์น˜์— Label์„ ์˜ฌ๋ ค ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฒˆ์—ญ๋œ ๋‚ด์šฉ์„ ํ‘œ์‹œํ•˜๋ฉด ์ข‹๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด UILabel์— frame ์ •๋ณด๋ฅผ ๋„˜๊ฒจ์ฃผ์—ˆ์ง€๋งŒ, ๊ณ„์†ํ•ด์„œ ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ์žก์ง€ ๋ชปํ•˜๊ณ  ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๋Š” ์˜ค๋ฅ˜๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
label์˜ translatesAutoresizingMaskIntoConstraints ์„ค์ •์ด false๋กœ ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋‚ด์šฉ์„ ์ ์šฉํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ์„ค์ •์„ ์‚ญ์ œํ•˜์—ฌ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

3๏ธโƒฃ UIToolBar Frame

๐Ÿšจ ๋ฌธ์ œ์ 
๋ฒ„ํŠผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด UIToolBar๋ฅผ ์—ฐ๊ฒฐํ•œ ํ›„ PickerView๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ Constraint ์˜ค๋ฅ˜๊ฐ€ ๊ณ„์†ํ•˜์—ฌ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
UIToolBar์˜ frame ์„ค์ •์ด ์—†์–ด ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋Š” ๊ฒƒ์œผ๋กœ ํ™•์ธํ•˜์—ฌ, ์•„๋ž˜์™€ ๊ฐ™์ด frame์„ ์„ค์ •ํ•ด ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

let toolBar = UIToolbar(frame: CGRect(origin: .zero, size: CGSize(width: UIScreen.main.bounds.width, height: 35)))


๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ

๐ŸŽ : Apple Developer Documentations ๐ŸŸข : Naver Developers โšช๏ธ : ๊ธฐํƒ€ ์ž๋ฃŒ

๐ŸŽ URLSession
๐ŸŽ URLRequest
๐ŸŽ URLComponents
๐ŸŽ UIAlertController
๐ŸŽ DataScannerViewController
๐ŸŽ Scanning data with the camera
๐ŸŽ UIPickerView
๐ŸŽ UIToolbar
๐ŸŽ Bounds
๐ŸŽ Frame
๐ŸŸข ํŒŒํŒŒ๊ณ  ๋ฒˆ์—ญ API
๐ŸŸข ํŒŒํŒŒ๊ณ  ์–ธ์–ด ๊ฐ์ง€ API
โšช๏ธ RxSwift
โšช๏ธ Kodeco : New Scanning and Text Capabilities with VisionKit