안녕하세요. Skillist입니다
추후 수정사항이 없다면 아마도 이번 글이 마지막이 되지 않을까 생각해요.
번역 화면만 구현하면 됩니다!
지금까지 구현한 View 3개를 몽땅 사용할거에요.
그러다 보니 bind가 생각보다 더 복잡합니다. 각오 하고 따라오세요.
———————————————————————————————————————————————————
ViewModel부터 보겠습니다.
12라인 : 언어 선택 View에 대한 ViewModel입니다. ViewModel이 ViewModel을 가지고 있습니다.
13라인 : 번역할 text 입력 View에 대한 ViewModel입니다.
14라인 : 번역 결과 View에 대한 ViewModel입니다.
17라인 : 번역 API를 Request하고 Response가 도착할때 까지, 보여줄 indicatorView에서 활용할 Relay입니다.
간단하죠? 이미 다른 ViewModel에서 정의했기 때문에, bind에서 흐름만 연결해주면 됩니다.
———————————————————————————————————————————————————
View를 보겠습니다. 역시나 bind는 마지막에 볼게요.
지난번에 작성한 "번역 화면의 구성" 글부터 보고 오세요.
https://skillist.tistory.com/314
View : 하위에 ScrollView가 존재합니다.
ㄴ ScrollView : 하위에 StackView가 존재합니다.
ㄴ StackView : 하위에는 "언어 선택 View", "text 입력 View", "번역 결과 View" 가 존재합니다.
ㄴ 언어 선택 View
ㄴ text 입력 View
ㄴ 번역 결과 View
ㄴ indicatorView : 또 View 하위에는, api 요청 시 보여줄 indicatorView도 존재합니다.
16~60라인 : UI컴포넌트입니다.
62라인 : viewDidLoad에서 기본 레이아웃 설정을 합니다.
202라인 : view 기본 설정을 합니다.
207라인 : 스냅킷을 통해 레이아웃 구성합니다.
언어를 선택하면 보여줄 ActionSheet입니다.
PublishSubject<Language>를 리턴합니다.
버튼을 구성하고, 버튼 handler에서 Language를 onNext로 전달합니다.
아직 익숙하지 않은 부분인데, 자주 활용하여 친숙해져야겠습니다.
275라인 : Reactive를 extension합니다.
276라인 : Error 발생 시 보여주는 Alert입니다. api error 발생, 한국어에서 한국어로 번역 시도할 때 보여줄거에요.
Reactive extension을 몇번 구현해봤는데, 아직은 익숙하지가 않네요. 몇번을 해도 익숙하지가 않아요.
71라인 : 언어 선택 View(selectLanguageView)를 바인딩 합니다.
73라인 : source 버튼을 터치하면, 언어 선택 action sheet을 보여줍니다. 버튼을 터치하면, language를 받아와, changedSourceLanguage에 바인드 합니다.
81라인 : target 버튼을 터치하면, 언어 선택 action sheet을 보여줍니다. 버튼을 터치하면, language를 받아와, changedTargetLanguage에 바인드 합니다.
89, 100라인 : source, target 언어가 변경되면, 각 뷰의 언어 label 타이틀을 변경하고, 번역 결과 View를 hide합니다.
111라인 : source, target 언어와 번역할 Text의 최신 값을 묶어버립니다. api request할때 사용할거에요.
119라인 : 번역 버튼(엔터키, done키)을 터치하면, 111라인의 데이터를 받아와, requestModel을 생성합니다.
125라인 : model을 구독합니다.
127라인 : source와 target 언어가 같다면, Error Alert을 보여줍니다.
133라인 : 그게 아니라면, indicatorView를 보여줍니다.
134라인 : API를 request하고 response를 받아 핸들러로 처리합니다.
137라인 : 성공 시 번역 text를 방출합니다.
141라인 : 실패 시 Error Alert을 보여줍니다.
149라인 : 번역된 Text가 전달되면, HistoryModel을 만들고, UserDefault에 addHistory합니다.
160라인 : text 입력 View(sourceTextInputView)를 바인딩 합니다.
161라인 : Observable.merge()를 통해서 입력한 text가 변경되거나 clear 버튼을 터치하면, 번역 결과 View를 hide하도록 바인드합니다.
172라인 : 번역 결과 View(translatedTextOutputView) 바인딩 합니다.
183라인 : 보관(북마크) 버튼을 터치하면, HistoryModel을 생성하여, UserDefault의 addBookmark를 바인딩 합니다.
193라인 : viewModel의 isAPIRequesting을 indicatorView.isAnimating에 바인딩 합니다.
———————————————————————————————————————————————————
이번엔 API를 볼게요
11라인 : result를 enum으로 구현합니다.
17라인 : api 함수입니다. urlcomponent없이 바로 string으로 url을 가져오도록 했어요.
24라인 : 헤더 값을 추가합니다. 네이버 개발자 사이트에 가이드가 있습니다. 제 코드를 사용한다면, 본인의 키를 발행하여 하드코딩하여 사용하세요.
29라인 : Alamofire를 사용하여 request합니다. URLSession보다 간단하네요.
또, RxAlamofire를 사용하려고 시도해봤는데, 아직은 잘 모르겠어서, Alamofire를 사용했습니다. ㅠㅜㅜㅠ
———————————————————————————————————————————————————
이렇게 번역 앱을 함께 개발해봤어요!
어떠세요???????
RxSwift 느낌이 좀 왔나요??????
전 완전 느낌 왔어요!
여러 프로젝트를 진행하며, 성장이 느껴지는데,
이번 프로젝트는 아주아주 매우 대만족 입니다.
연휴는 날라갔지만, 저에게는 뼈와 살이되는 기간이었습니다!
모두들 고생하셨어요
잘못되거나 부족한 내용 등, 피드백 감사합니다!
https://github.com/DeveloperSkillist/TranstorKing
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 전체 코드 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
import RxCocoa
import RxSwift
struct TranslatorViewModel {
let selectLanguageViewModel = SelectLanguageViewModel()
let translatedTextOutputViewModel = TranslatedTextOutputViewModel()
let sourceTextInputViewModel = SourceTextInputViewModel()
//view -> viewModel
let isAPIRequesting = BehaviorRelay<Bool>(value: false)
//viewModel -> view
}
import UIKit
import RxSwift
import RxCocoa
import SnapKit
class TranslatorView: UIViewController {
let disposeBag = DisposeBag()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
private lazy var selectLanguageView: SelectLanguageView = {
let selectLanguageView = SelectLanguageView()
return selectLanguageView
}()
private lazy var sourceTextInputView: SourceTextInputView = {
let sourceTextInputView = SourceTextInputView()
return sourceTextInputView
}()
private lazy var translatedTextOutputView: TranslatedTextOutputView = {
let translatedTextOutputView = TranslatedTextOutputView()
return translatedTextOutputView
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 16
stackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
stackView.isLayoutMarginsRelativeArrangement = true
[
selectLanguageView,
sourceTextInputView,
translatedTextOutputView
].forEach {
stackView.addArrangedSubview($0)
}
return stackView
}()
private lazy var indicatorView: UIActivityIndicatorView = {
var indicatorView = UIActivityIndicatorView()
indicatorView.hidesWhenStopped = true
indicatorView.isHidden = true
indicatorView.style = .large
return indicatorView
}()
override func viewDidLoad() {
super.viewDidLoad()
attribute()
layout()
}
func bind(_ viewModel: TranslatorViewModel) {
//selectLanguageView
selectLanguageView.bind(viewModel.selectLanguageViewModel)
viewModel.selectLanguageViewModel.sourceLanguageButtonTap
.flatMapFirst { _ in
self.presentSelectLanguageActionSheet()
.map { $0 }
}
.bind(to: viewModel.selectLanguageViewModel.changedSourceLanguage)
.disposed(by: disposeBag)
viewModel.selectLanguageViewModel.targetLanguageButtonTap
.flatMapFirst { _ in
self.presentSelectLanguageActionSheet()
.map { $0 }
}
.bind(to: viewModel.selectLanguageViewModel.changedTargetLanguage)
.disposed(by: disposeBag)
viewModel.selectLanguageViewModel.sourceLanguage
.asObservable()
.map {
$0
}
.bind(onNext: {
viewModel.sourceTextInputViewModel.selectedLanguage.accept($0)
viewModel.translatedTextOutputViewModel.isHiddenView.accept(true)
})
.disposed(by: disposeBag)
viewModel.selectLanguageViewModel.targetLanguage
.asObservable()
.map {
$0
}
.bind(onNext: {
viewModel.translatedTextOutputViewModel.selectedLanguage.accept($0)
viewModel.translatedTextOutputViewModel.isHiddenView.accept(true)
})
.disposed(by: disposeBag)
let inputDatas = Observable.combineLatest(
viewModel.selectLanguageViewModel.sourceLanguage
.asObservable(),
viewModel.selectLanguageViewModel.targetLanguage
.asObservable(),
viewModel.sourceTextInputViewModel.inputText
)
let settedTranslateRequestModel = viewModel.sourceTextInputViewModel.translateButtonTap
.withLatestFrom(inputDatas) { ($1.0, $1.1, $1.2) }
.map { sourceLan, targetLan, text -> TranslateRequestModel in
return TranslateRequestModel(source: sourceLan.rawValue, target: targetLan.rawValue, text: text)
}
settedTranslateRequestModel
.subscribe(onNext: {
if $0.source == $0.target {
self.rx.showAlert
.onNext(Alert(title: "language_error_title".localize, message: "language_error_message".localize))
return
}
viewModel.isAPIRequesting.accept(true)
TranslateAPI().requestTranslate(translateRequestModel: $0) { result in
viewModel.isAPIRequesting.accept(false)
switch result {
case .success(let result):
viewModel.translatedTextOutputViewModel.translatedText
.accept(result.translatedText)
case .failure(let error):
self.rx.showAlert
.onNext(Alert(title: "network_error_title".localize, message: error.localizedDescription))
}
}
})
.disposed(by: disposeBag)
viewModel.translatedTextOutputViewModel.translatedText
.withLatestFrom(inputDatas) { ($0, $1.0, $1.1, $1.2) }
.map { translatedText, sourceLan, targetLan, sourceText -> HistoryModel in
return HistoryModel(sourceLanguage: sourceLan, targetLanguage: targetLan, sourceText: sourceText, targetText: translatedText)
}
.subscribe(onNext: {
UserDefaults.standard.addHistory(historyModel: $0)
})
.disposed(by: disposeBag)
//sourceTextInputView
sourceTextInputView.bind(viewModel.sourceTextInputViewModel)
Observable.merge(
viewModel.sourceTextInputViewModel.changedInputText.asObservable(),
viewModel.sourceTextInputViewModel.clearButtonTap.asObservable()
)
.map {
true
}
.bind(to: viewModel.translatedTextOutputViewModel.isHiddenView)
.disposed(by: disposeBag)
//translatedTextOutputView
translatedTextOutputView.bind(viewModel.translatedTextOutputViewModel)
let completedTranstor = Observable.combineLatest(
viewModel.selectLanguageViewModel.sourceLanguage
.asObservable(),
viewModel.selectLanguageViewModel.targetLanguage
.asObservable(),
viewModel.sourceTextInputViewModel.inputText,
viewModel.translatedTextOutputViewModel.translatedText
)
viewModel.translatedTextOutputViewModel.bookmarkButtonTap
.withLatestFrom(completedTranstor) { ($1.0, $1.1, $1.2, $1.3) }
.map { sourceLan, targetLan, sourceText, translatedText -> HistoryModel in
return HistoryModel(sourceLanguage: sourceLan, targetLanguage: targetLan, sourceText: sourceText, targetText: translatedText)
}
.subscribe(onNext: {
UserDefaults.standard.addBookmark(historyModel: $0)
})
.disposed(by: disposeBag)
viewModel.isAPIRequesting
.bind(to: indicatorView.rx.isAnimating)
.disposed(by: disposeBag)
}
private func attribute() {
view.backgroundColor = .secondarySystemBackground
navigationItem.title = "translator_title".localize
}
private func layout() {
[
scrollView,
indicatorView
].forEach {
view.addSubview($0)
}
scrollView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
indicatorView.snp.makeConstraints {
$0.centerX.centerY.equalToSuperview()
}
[
stackView
].forEach {
scrollView.addSubview($0)
}
stackView.snp.makeConstraints {
$0.edges.equalToSuperview()
$0.width.equalToSuperview()
}
}
private func presentSelectLanguageActionSheet() -> Observable<Language> {
let selectedLanguage = PublishSubject<Language>()
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let koAction = UIAlertAction(title: "korean".localize, style: .default) { _ in
selectedLanguage.onNext(.ko)
selectedLanguage.onCompleted()
}
alertController.addAction(koAction)
let enAction = UIAlertAction(title: "english".localize, style: .default) { _ in
selectedLanguage.onNext(.en)
selectedLanguage.onCompleted()
}
alertController.addAction(enAction)
let jpAction = UIAlertAction(title: "japanese".localize, style: .default) { _ in
selectedLanguage.onNext(.ja)
selectedLanguage.onCompleted()
}
alertController.addAction(jpAction)
let chAction = UIAlertAction(title: "chinese".localize, style: .default) { _ in
selectedLanguage.onNext(.ch)
selectedLanguage.onCompleted()
}
alertController.addAction(chAction)
let cancelAction = UIAlertAction(title: "cancel".localize, style: .cancel) { _ in
selectedLanguage.onCompleted()
}
alertController.addAction(cancelAction)
present(alertController, animated: true)
return selectedLanguage
}
}
typealias Alert = (title: String, message: String)
extension Reactive where Base: TranslatorView {
var showAlert: Binder<Alert> {
return Binder(base) { viewController, alert in
let alertController = UIAlertController(title: alert.title, message: alert.message, preferredStyle: .alert)
let action = UIAlertAction(title: "ok_title".localize, style: .cancel)
alertController.addAction(action)
base.present(alertController, animated: true, completion: nil)
}
}
}
import RxSwift
import Alamofire
enum NetworkResult<T> {
case success(T)
case failure(Error)
}
struct TranslateAPI {
func requestTranslate(
translateRequestModel: TranslateRequestModel,
// completionHandler: @escaping (String) -> Void
completionHandler: @escaping (Result<TranslateResponseModel, AFError>) -> Void
) {
let url = URL(string: "https://openapi.naver.com/v1/papago/n2mt")!
let headers: HTTPHeaders = [
"X-Naver-Client-Id": APIKeys.id, //네이버 개발자 사이트에서 발행한 키를 입력하세요.
"X-Naver-Client-Secret": APIKeys.Secret //네이버 개발자 사이트에서 발행한 키를 입력하세요.
]
AF
.request(url, method: .post, parameters: translateRequestModel, headers: headers)
.responseDecodable(of: TranslateResponseModel.self) { response in
completionHandler(response.result)
}
.resume()
}
}
'iOS 개발 > 번역기 앱(RxSwift)' 카테고리의 다른 글
번역기 앱 - 13. RxSwift의 메모리 누수 체크 (0) | 2022.02.09 |
---|---|
번역기 앱 - 12. 프로젝트 소감 (0) | 2022.02.02 |
번역기 앱 - 10. 번역 결과 View (0) | 2022.02.01 |
번역기 앱 - 9. 번역할 Text 입력 View (0) | 2022.02.01 |
번역기 앱 - 8. 언어 선택 View (0) | 2022.02.01 |