From 91788e998a44babd6678992711af646cac032b65 Mon Sep 17 00:00:00 2001 From: hooni Date: Mon, 15 Jul 2024 02:42:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat/#174=20AuthService=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum.xcodeproj/project.pbxproj | 12 ++++ .../Login/Service/AuthService.swift | 31 ++++++++++ .../Login/VIewModel/LoginViewModel.swift | 34 ++++++----- .../ViewController/LoginViewController.swift | 58 ++++++++++--------- 4 files changed, 93 insertions(+), 42 deletions(-) create mode 100644 KkuMulKum/Source/Onboarding/Login/Service/AuthService.swift diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 95951034..cb08e1f0 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 78B928752C29402E006D9942 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78B928742C29402E006D9942 /* Assets.xcassets */; }; 78B928782C29402E006D9942 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78B928762C29402E006D9942 /* LaunchScreen.storyboard */; }; 78BD61202C43F557005752FD /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 78BD611F2C43F557005752FD /* SwiftKeychainWrapper */; }; + 78BD61232C440AD5005752FD /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61222C440AD5005752FD /* AuthService.swift */; }; A3DD9C3D2C41BAD000E58A13 /* MeetingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */; }; A3DD9C3E2C41BAD000E58A13 /* MeetingDummyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */; }; A3DD9C3F2C41BAD000E58A13 /* MeetingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */; }; @@ -196,6 +197,7 @@ 78B928742C29402E006D9942 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 78B928772C29402E006D9942 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 78B928792C29402E006D9942 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 78BD61222C440AD5005752FD /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingTableViewCell.swift; sourceTree = ""; }; A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingDummyModel.swift; sourceTree = ""; }; A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingListView.swift; sourceTree = ""; }; @@ -508,6 +510,14 @@ path = KkuMulKum; sourceTree = ""; }; + 78BD61212C440AC8005752FD /* Service */ = { + isa = PBXGroup; + children = ( + 78BD61222C440AD5005752FD /* AuthService.swift */, + ); + path = Service; + sourceTree = ""; + }; A3DD9C332C41BAD000E58A13 /* Cell */ = { isa = PBXGroup; children = ( @@ -871,6 +881,7 @@ DD865B662C39210E00C351A2 /* Login */ = { isa = PBXGroup; children = ( + 78BD61212C440AC8005752FD /* Service */, 789873372C3D1B4800435E96 /* ViewController */, 789873362C3D1B3900435E96 /* VIewModel */, 789873352C3D1B3000435E96 /* View */, @@ -1371,6 +1382,7 @@ DE6D4D112C3F14D80005584B /* MeetingInfoBannerView.swift in Sources */, DE6D4D122C3F14D80005584B /* MeetingInfoView.swift in Sources */, DD30721E2C3C0CC800416D9F /* PromiseInfoResponseModel.swift in Sources */, + 78BD61232C440AD5005752FD /* AuthService.swift in Sources */, DD931B722C3DA92700526452 /* EnterReadyInfoButtonView.swift in Sources */, A3FB18512C3BF531001483E5 /* RegisterMeetingsResponseModel.swift in Sources */, 789AD4B32C3C0093002E2688 /* SocialLoginResponseModel.swift in Sources */, diff --git a/KkuMulKum/Source/Onboarding/Login/Service/AuthService.swift b/KkuMulKum/Source/Onboarding/Login/Service/AuthService.swift new file mode 100644 index 00000000..4c63c59e --- /dev/null +++ b/KkuMulKum/Source/Onboarding/Login/Service/AuthService.swift @@ -0,0 +1,31 @@ +// +// AuthServiceType.swift +// KkuMulKum +// +// Created by 이지훈 on 7/14/24. +// + +import Foundation + +protocol AuthServiceType { + + // TODO: 토큰 관리를 위한 메서드 (키체인 생성이후 구현예정) + func saveToken(_ token: String) + func getToken() -> String? + func clearToken() +} + +class AuthService: AuthServiceType { + func saveToken(_ token: String) { + + } + + func getToken() -> String? { + + return nil + } + + func clearToken() { + } + +} diff --git a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift index cfb801eb..499f2725 100644 --- a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift @@ -11,16 +11,22 @@ import AuthenticationServices import KakaoSDKUser import KakaoSDKAuth - enum LoginState { - case notLoggedIn case loggedIn(userInfo: String) + case notLoggedIn } class LoginViewModel: NSObject { + private let authService: AuthServiceType + var loginState: ObservablePattern = ObservablePattern(.notLoggedIn) var error: ObservablePattern = ObservablePattern("") - + + init(authService: AuthServiceType) { + self.authService = authService + super.init() + } + func performAppleLogin(presentationAnchor: ASPresentationAnchor) { let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.fullName, .email] @@ -31,7 +37,7 @@ class LoginViewModel: NSObject { controller.performRequests() } - func performKakaoLogin(presentationAnchor: UIWindow) { + func performKakaoLogin() { if UserApi.isKakaoTalkLoginAvailable() { UserApi.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error) @@ -68,26 +74,22 @@ class LoginViewModel: NSObject { } } -extension LoginViewModel: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { +extension LoginViewModel: ASAuthorizationControllerDelegate, + ASAuthorizationControllerPresentationContextProviding { func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { - guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { - print("Authorization failed: Credential is not of type ASAuthorizationAppleIDCredential") + guard let appleIDCredential = authorization.credential as? + ASAuthorizationAppleIDCredential else { + error.value = "Failed to get Apple ID Credential" return } - let userName = appleIDCredential.fullName?.givenName ?? "Apple user" - loginState.value = .loggedIn(userInfo: "Apple user: \(userName)") - - /// 액세스 토큰 출력 - if let identityToken = appleIDCredential.identityToken, - let tokenString = String(data: identityToken, encoding: .utf8) { - print("Apple Login Access Token: \(tokenString)") - } + let userIdentifier = appleIDCredential.user + loginState.value = .loggedIn(userInfo: "Apple user: \(userIdentifier)") } - + func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error diff --git a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift index 4be171fa..4b58a0fb 100644 --- a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift +++ b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift @@ -6,12 +6,20 @@ // import UIKit -import AuthenticationServices class LoginViewController: BaseViewController { - private let loginView = LoginView() - private let loginViewModel = LoginViewModel() + private let loginViewModel: LoginViewModel + + init() { + let authService = AuthService() + self.loginViewModel = LoginViewModel(authService: authService) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func loadView() { view = loginView @@ -26,24 +34,13 @@ class LoginViewController: BaseViewController { override func setupAction() { super.setupAction() - let appleTapGesture = UITapGestureRecognizer( - target: self, - action: #selector(appleLoginTapped) - ) + let appleTapGesture = UITapGestureRecognizer(target: self, action: #selector(appleLoginTapped)) loginView.appleLoginImageView.addGestureRecognizer(appleTapGesture) - let kakaoTapGesture = UITapGestureRecognizer( - target: self, - action: #selector(kakaoLoginTapped) - ) + let kakaoTapGesture = UITapGestureRecognizer(target: self, action: #selector(kakaoLoginTapped)) loginView.kakaoLoginImageView.addGestureRecognizer(kakaoTapGesture) - /// 더미 버튼 - loginView.dummyNextButton.addTarget( - self, - action: #selector(dummyNextButtonTapped), - for: .touchUpInside - ) + loginView.dummyNextButton.addTarget(self, action: #selector(dummyNextButtonTapped), for: .touchUpInside) } private func bindViewModel() { @@ -53,13 +50,13 @@ class LoginViewController: BaseViewController { print("Not logged in") case .loggedIn(let userInfo): print("Logged in: \(userInfo)") + owner.navigateToMainScreen() } } loginViewModel.error.bind(with: self) { owner, error in if !error.isEmpty { - // TODO: 추후 에러처리 추가예정 -> Keychain 연결 이후 - print("Error occurred: \(error)") + owner.showErrorAlert(message: error) } } } @@ -69,15 +66,15 @@ class LoginViewController: BaseViewController { } @objc private func kakaoLoginTapped() { - loginViewModel.performKakaoLogin(presentationAnchor: view.window!) + loginViewModel.performKakaoLogin() } - + // TODO: 추후 서버연결후 삭제예정 @objc private func dummyNextButtonTapped() { -// _ = NicknameViewController() -// let welcomeViewController = NicknameViewController() -// welcomeViewController.modalPresentationStyle = .fullScreen -// present(welcomeViewController, animated: true, completion: nil) + // _ = NicknameViewController() + // let welcomeViewController = NicknameViewController() + // welcomeViewController.modalPresentationStyle = .fullScreen + // present(welcomeViewController, animated: true, completion: nil) // TODO: 프로필 설정부터 네비게이션으로 플로우 동작 @@ -87,5 +84,14 @@ class LoginViewController: BaseViewController { navigationController.modalPresentationStyle = .fullScreen present(navigationController, animated: true) } + + private func navigateToMainScreen() { + // 로그인 성공 후 메인 화면으로 이동하는 로직 + } + + private func showErrorAlert(message: String) { + let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) + } } - From 833076b6bacbb2e58d85ef41f647fbfdaf8d0d72 Mon Sep 17 00:00:00 2001 From: hooni Date: Mon, 15 Jul 2024 04:55:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat/#174=20AuthService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum.xcodeproj/project.pbxproj | 24 +++++++++---------- ...swift => ProfileSetupViewController.swift} | 0 ...ofileView.swift => ProfileSetupView.swift} | 3 ++- ...odel.swift => ProfileSetupViewModel.swift} | 0 .../Onboarding/Welcome/View/WelcomeView.swift | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) rename KkuMulKum/Source/Onboarding/Profile/VIewController/{ProfileViewController.swift => ProfileSetupViewController.swift} (100%) rename KkuMulKum/Source/Onboarding/Profile/View/{ProfileView.swift => ProfileSetupView.swift} (97%) rename KkuMulKum/Source/Onboarding/Profile/ViewModel/{ProfileViewModel.swift => ProfileSetupViewModel.swift} (100%) diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index cb08e1f0..b512bc21 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -7,9 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 782B406F2C3DBF93008B0CA7 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B406E2C3DBF93008B0CA7 /* ProfileViewController.swift */; }; - 782B40722C3DBFA3008B0CA7 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B40712C3DBFA3008B0CA7 /* ProfileViewModel.swift */; }; - 782B40752C3DBFBA008B0CA7 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B40742C3DBFBA008B0CA7 /* ProfileView.swift */; }; + 782B406F2C3DBF93008B0CA7 /* ProfileSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B406E2C3DBF93008B0CA7 /* ProfileSetupViewController.swift */; }; + 782B40722C3DBFA3008B0CA7 /* ProfileSetupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B40712C3DBFA3008B0CA7 /* ProfileSetupViewModel.swift */; }; + 782B40752C3DBFBA008B0CA7 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B40742C3DBFBA008B0CA7 /* ProfileSetupView.swift */; }; 782B407B2C3E395A008B0CA7 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B407A2C3E395A008B0CA7 /* WelcomeView.swift */; }; 782B407D2C3E3984008B0CA7 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B407C2C3E3984008B0CA7 /* WelcomeViewController.swift */; }; 782B407F2C3E44B7008B0CA7 /* WelcomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782B407E2C3E44B7008B0CA7 /* WelcomeViewModel.swift */; }; @@ -175,9 +175,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 782B406E2C3DBF93008B0CA7 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; - 782B40712C3DBFA3008B0CA7 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; - 782B40742C3DBFBA008B0CA7 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 782B406E2C3DBF93008B0CA7 /* ProfileSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupViewController.swift; sourceTree = ""; }; + 782B40712C3DBFA3008B0CA7 /* ProfileSetupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupViewModel.swift; sourceTree = ""; }; + 782B40742C3DBFBA008B0CA7 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = ""; }; 782B407A2C3E395A008B0CA7 /* WelcomeView.swift */ = {isa = PBXFileReference; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 782B407C2C3E3984008B0CA7 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 782B407E2C3E44B7008B0CA7 /* WelcomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewModel.swift; sourceTree = ""; }; @@ -369,7 +369,7 @@ 782B406D2C3DBF7C008B0CA7 /* VIewController */ = { isa = PBXGroup; children = ( - 782B406E2C3DBF93008B0CA7 /* ProfileViewController.swift */, + 782B406E2C3DBF93008B0CA7 /* ProfileSetupViewController.swift */, ); path = VIewController; sourceTree = ""; @@ -377,7 +377,7 @@ 782B40702C3DBF97008B0CA7 /* ViewModel */ = { isa = PBXGroup; children = ( - 782B40712C3DBFA3008B0CA7 /* ProfileViewModel.swift */, + 782B40712C3DBFA3008B0CA7 /* ProfileSetupViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -385,7 +385,7 @@ 782B40732C3DBFA8008B0CA7 /* View */ = { isa = PBXGroup; children = ( - 782B40742C3DBFBA008B0CA7 /* ProfileView.swift */, + 782B40742C3DBFBA008B0CA7 /* ProfileSetupView.swift */, ); path = View; sourceTree = ""; @@ -1397,7 +1397,7 @@ DD4393772C412F4500EC1799 /* CreateMeetingView.swift in Sources */, 789873342C3D1A7B00435E96 /* LoginView.swift in Sources */, 782B40822C3E4925008B0CA7 /* NicknameViewModel.swift in Sources */, - 782B406F2C3DBF93008B0CA7 /* ProfileViewController.swift in Sources */, + 782B406F2C3DBF93008B0CA7 /* ProfileSetupViewController.swift in Sources */, A3FB18592C3BF77D001483E5 /* MeetingInfoResponseModel.swift in Sources */, DEA932182C3F180800FDF637 /* MeetingPromisesModel.swift in Sources */, DE159D342C406E1600425101 /* MyPageEtcSettingView.swift in Sources */, @@ -1454,7 +1454,7 @@ DD3072222C3C0DA300416D9F /* PromiseParticipantListResponseModel.swift in Sources */, DD3976862C41C2AD00E2A4C4 /* HomeView.swift in Sources */, 789873322C3D1A7B00435E96 /* LoginViewController.swift in Sources */, - 782B40722C3DBFA3008B0CA7 /* ProfileViewModel.swift in Sources */, + 782B40722C3DBFA3008B0CA7 /* ProfileSetupViewModel.swift in Sources */, DDAF1C8F2C3D6E3D008A37D3 /* BasePromiseSegmentedControl.swift in Sources */, DE32D1D22C3BF703006848DF /* LoginUserResponseModel.swift in Sources */, DE9E18892C3BC91000DB76B4 /* ResponseBodyDTO.swift in Sources */, @@ -1470,7 +1470,7 @@ DD3072242C3C0EB200416D9F /* MyPromiseReadyInfoRequestModel.swift in Sources */, DD3976872C41C2AD00E2A4C4 /* TodayPromiseView.swift in Sources */, 789873332C3D1A7B00435E96 /* LoginViewModel.swift in Sources */, - 782B40752C3DBFBA008B0CA7 /* ProfileView.swift in Sources */, + 782B40752C3DBFBA008B0CA7 /* ProfileSetupView.swift in Sources */, DED5DBEE2C34529A006ECE7E /* BaseView.swift in Sources */, A3DD9C402C41BAD000E58A13 /* MeetingListViewController.swift in Sources */, A3DD9C3F2C41BAD000E58A13 /* MeetingListView.swift in Sources */, diff --git a/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileViewController.swift b/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift similarity index 100% rename from KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileViewController.swift rename to KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift diff --git a/KkuMulKum/Source/Onboarding/Profile/View/ProfileView.swift b/KkuMulKum/Source/Onboarding/Profile/View/ProfileSetupView.swift similarity index 97% rename from KkuMulKum/Source/Onboarding/Profile/View/ProfileView.swift rename to KkuMulKum/Source/Onboarding/Profile/View/ProfileSetupView.swift index 55adf32a..b0b49e11 100644 --- a/KkuMulKum/Source/Onboarding/Profile/View/ProfileView.swift +++ b/KkuMulKum/Source/Onboarding/Profile/View/ProfileSetupView.swift @@ -13,7 +13,7 @@ import Then class ProfileSetupView: BaseView { let titleLabel = UILabel().then { $0.setText("프로필을 설정해 주세요", style: .head01, color: .gray8) - $0.textAlignment = .center + $0.textAlignment = .left } let profileImageView = UIImageView().then { @@ -30,6 +30,7 @@ class ProfileSetupView: BaseView { let skipButton = UIButton().then { $0.setTitle("지금은 넘어가기", style: .body05, color: .gray5) + $0.addUnderline() } let confirmButton = UIButton().then { diff --git a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileViewModel.swift b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift similarity index 100% rename from KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileViewModel.swift rename to KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift diff --git a/KkuMulKum/Source/Onboarding/Welcome/View/WelcomeView.swift b/KkuMulKum/Source/Onboarding/Welcome/View/WelcomeView.swift index f96f1fe6..2a4d05e1 100644 --- a/KkuMulKum/Source/Onboarding/Welcome/View/WelcomeView.swift +++ b/KkuMulKum/Source/Onboarding/Welcome/View/WelcomeView.swift @@ -54,7 +54,7 @@ class WelcomeView: BaseView { } descriptionLabel.snp.makeConstraints { - $0.top.equalTo(welcomeLabel.snp.bottom).offset(10) + $0.top.equalTo(welcomeLabel.snp.bottom).offset(18) $0.centerX.equalToSuperview() $0.leading.trailing.equalToSuperview().inset(20) $0.bottom.lessThanOrEqualTo(confirmButton.snp.top).offset(-20)