Moya 도입, 네트워크 구조 개선
Endpoint, APIService에 모든 네트워크 통신에 대한 메서드를 다 구현
Endpoint - URL
enum Endpoint {
case user
case user_withdraw
case user_update_fcm_token
case user_update_mypage
case queue
case queue_onqueue
...
}
extension Endpoint {
var url: URL {
switch self {
case .user: return .makeEndpoint("user")
case .user_withdraw: return .makeEndpoint("user/withdraw")
case .user_update_fcm_token: return .makeEndpoint("user/update_fcm_token")
case .user_update_mypage: return .makeEndpoint("user/update/mypage")
case .queue: return .makeEndpoint("queue")
case .queue_onqueue: return .makeEndpoint("queue/onqueue")
...
}
}
}
extension URL {
static let baseURL = "http://test.monocoding.com:35484/"
static func makeEndpoint(_ endpoint: String) -> URL {
URL(string: baseURL + endpoint)!
}
}
APIService - HTTPHeaders, Parameters, Request
import Alamofire
...
static func signUpUserInfo(idToken: String, completion: @escaping (Error?, Int?) -> Void) {
let headers: HTTPHeaders = [
"idtoken": idToken,
"Content-Type": "application/x-www-form-urlencoded"
]
let FCMtoken = UserDefaults.standard.string(forKey: "FCMToken") ?? ""
let phoneNumber = UserDefaults.standard.string(forKey: "phoneNumber") ?? ""
let nick = UserDefaults.standard.string(forKey: "nickName") ?? ""
let birth = UserDefaults.standard.string(forKey: "birth") ?? ""
let email = UserDefaults.standard.string(forKey: "email") ?? ""
let gender = UserDefaults.standard.integer(forKey: "gender")
let parameters : Parameters = [
"phoneNumber": phoneNumber,
"FCMtoken": FCMtoken,
"nick": nick,
"birth": birth,
"email": email,
"gender": gender
]
AF.request(Endpoint.user.url.absoluteString, method: .post, parameters: parameters, headers: headers).responseString { response in
let statusCode = response.response?.statusCode
switch response.result {
case .success(let value):
print("[signUpUserInfo] response success", value)
completion(nil, statusCode)
case .failure(let error):
print("[signUpUserInfo] response error", error)
completion(error, statusCode)
}
}
}
...
API에도 목적이 존재하는 만큼 자체적인 기준을 세워서 역할/책임을 조금 더 분리 필요.
이후에 서버와 커뮤니케이션을 할 때, 용이하거나 변경 지점이 생기시더라도 금방 유지보수가 가능
Target (baseURL, path, method, task, headers)
API (request)
Models (Request body)
버튼 활성화, `RxSwift` 적용
ViewModel에서 입력(Input)과 출력(Output)을 정의
- View에서 받는 입력은 Input 구조체 안에 정의 (text, 버튼 이벤트)
- 로직을 통해서 나온 결과 출력은 Output 구조체에 정의 (버튼 활성화 상태, 화면 전환 이벤트)
var validText = BehaviorRelay<String>(value: "")
struct Input {
let text: ControlProperty<String?>
let tap: ControlEvent<Void>
}
struct Output {
let validStatus: Observable<Bool>
let validText: BehaviorRelay<String>
let sceneTransition: ControlEvent<Void>
}
map
기능을 통해 정규식 유효성 검사share()
연산자를 사용하여 하나의 시퀀스에서 방출되는 아이템을 공유해 사용
func phoneNumberTransform(input: Input) -> Output {
let result = input.text
.orEmpty
.map { $0.isValidPhoneNumber() }
.share(replay: 1, scope: .whileConnected)
return Output(validStatus: result, validText: validText, sceneTransition: input.tap)
}
func certificationTransform(input: Input) -> Output {
let result = input.text
.orEmpty
.map { $0.isVaildVerificationCode() }
.share(replay: 1, scope: .whileConnected)
return Output(validStatus: result, validText: validText, sceneTransition: input.tap)
}
- 유효성 검사가 진행되는 값을 버튼 배경색, 버튼 활성화 상태에 바인딩
let input = ValidationViewModel.Input(text: authView.inputTextField.rx.text, tap: authView.nextButton.rx.tap)
let output = viewModel.phoneNumberTransform(input: input)
output.validStatus
.map { $0 ? R.color.green() : R.color.gray6() }
.bind(to: authView.nextButton.rx.backgroundColor)
.disposed(by: disposeBag)
output.validStatus
.bind(to: authView.nextButton.rx.isEnabled)
.disposed(by: disposeBag)
output.validText
.asDriver()
.drive(authView.inputTextField.rx.text)
.disposed(by: disposeBag)
output.sceneTransition
.subscribe { _ in
sceneTransition()
}.disposed(by: disposeBag)
[SceneDelegate] 로그인/회원가입 유무에 따른 UI Life Cycle 분기 처리
- 회원정보를 앱내 스토리지(저장소)에 저장해두고 필요할때 불러와서 처리하기 위해 토큰 값을 UserDefaults에 저장.
- 로그인과 회원가입 분기처리는 로그인 여부에 달려있기에, 서버로부터 로그인 시 발급받은 토큰을 SceneDelegate에서 앱 실행 시에 토큰 유무에 따라 UI Life Cycle 분기 처리
idToken 값으로 분기 처리를 하기 위해, User의 정보를 API에서 호출했는데 API에서 데이터를 받아오는 과정에서 black Screen이 뜬 뒤, View가 로드된다.
코드
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: scene)
let idToken = UserDefaults.standard.string(forKey: "idToken") ?? ""
print("SceneDelegate idToken", idToken)
if idToken == "" { // 전화번호 인증 X
convertNavRootViewController(VerificationViewController())
} else { // 전화번호 인증 O
APIService.getUserInfo(idToken: idToken) { user, error, statusCode in
switch statusCode {
case 200:
self.convertRootViewController(MainTapController())
case 401:
print("SceneDelegate", statusCode ?? 0)
Helper.getIDTokenRefresh {
print("SceneDelegate 토큰 갱신 error"); return
} onSuccess: {
print("SceneDelegate 토큰 갱신 성공")
self.convertRootViewController(MainTapController())
}
default:
print("SceneDelegate default error", statusCode ?? 0)
self.convertNavRootViewController(NickNameViewController())
}
}
}
}
-
로그인 완료
-
회원가입 완료
-
회원 탈퇴 완료
굳이 API 호출을 하지 않고 3가지의 상황에 따라 UserDefaults에 상황별 String값을 저장해주고, SceneDelegate에서 해당 Key값을 통해 UI Life Cycle 분기 처리 진행
코드
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: scene)
let startView = UserDefaults.standard.string(forKey: "startView")
print("------> startView = \(startView ?? "전화번호인증 하러가야함")")
if startView == "successLogin" { // 로그인 완료
convertNavRootViewController(NickNameViewController())
} else if startView == "alreadySignUp" { // 회원가입 완료
convertRootViewController(MainTapController())
} else { // 회원탈퇴 완료 및 앱 첫 실행
convertNavRootViewController(VerificationViewController())
}
}