현재 멋쟁이사자처럼에서 진행하는 '테킷 앱스쿨: iOS 2기' 교육을 받고 있다.
이곳에서 '모공모공' 이라는 스터디에 참여하고 있는데, 모공모공 태그가 붙은 글들은 이 스터디에서 진행한 프로젝트나 공부한 주제들을 다룰 것이다 💭
오늘은 제목처럼 SwiftUI를 사용한 Log in 화면 구현 과정을 풀어내 보겠다 🧐
먼저, 요구사항은 다음과 같다.
이제 기본 뼈대 구현 방법은 제외하고, 각 요구사항 별로 내가 구현한 방법들을 설명해 보겠다 💡
아이디에는 Email 형식만 입력
형식이 맞지 않으면 Alert 창 띄우기
여기에서는 정규 표현식을 사용하는 메서드를 만들어서 다음의 코드와 같은 형식을 만족하지 않으면 Sign in 버튼을 눌렀을 때 Alert 창을 띄우도록 했다.
// 정규 표현식을 사용해 Email 형식 유무 체크
private func checkEmailForm(input: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: input)
}
// Email 형식을 준수하지 않았을 때 띄우는 Alert
.alert(isPresented: $isValid) {
Alert(title: Text("경고"), message: Text("이메일 형식이 올바르지 않습니다."), dismissButton: .default(Text("확인")))
}
아이디랑 비밀번호 칸이 두 개가 다 입력되어 있고, 토글 버튼이 true여야 회원가입 버튼이 활성화
이 요구사항은 간단한 조건식을 사용해서 구현해 보았다.
// 모든 TextField가 채워지고 토글버튼을 On 해야 버튼이 활성화 되는 조건식
.disabled(email.isEmpty || password.isEmpty || !toggling)
비밀번호 텍스트 필드 값은 암호 표시
이 요구사항은 간단하게 TextField 대신 SecureField를 사용해서 해결했다.
// 비밀번호 텍스트 필드 암호 표시
SecureField("Password", text: $password)
아이디와 비밀번호가 설정한 것과 일치하면 다른 뷰로 넘어가기
일치하지 않으면 Alert 창 띄우기
여기에서는 checkLogin이라는 메서드를 만들어서 이메일과 패스워드 둘 중 하나라도 설정한 값들과 맞지 않으면 미리 만들어 놓은 Bool 타입의 @State notCorrectLogin 변수가 true가 되도록 했다.
// 이메일, 패스워드의 일치 여부 확인
private func checkLogin(isEmail: String, isPassword: String) {
if isEmail != correctEmail || isPassword != correctPassword {
notCorrectLogin = true
}
}
계정 정보가 일치할 때 다른 뷰로 넘어가는 기능은 NavigationLink와 Bool 타입의 @State isActive 변수
를 사용해서 구현했다.
@State private var isActive: Bool = false
NavigationLink(destination: DetailView(), isActive: $isActive) {
EmptyView()
}
그리고 계정 정보가 일치하지 않았을 때 Sign in 버튼을 누르면 Alert 창을 띄우도록 했다.
// 계정 정보가 일치하지 않을 때 띄우는 Alert
.alert(isPresented: $notCorrectLogin) {
Alert(title: Text("주의\n"), message: Text("이메일, 또는 비밀번호가 일치하지 않습니다."), dismissButton: .default(Text("확인")))
}
화면의 빈 부분을 터치하면 키보드 숨기기
이 요구사항은 그 동안 해본 적 없는 생소한 부분이라서 구현하는 데 좀 더 시간이 걸렸다.
먼저, extension을 사용해서 View에 다음과 같은 메서드를 추가했다.
// UIKit에서도 활용하는 resignFirstResponder 메서드 추가
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
그리고 빈 곳을 탭 했을 때 키보드를 내리고 싶은 뷰 하단에 다음과 같이 onTapGesture 이벤트 메서드를 추가하면 요구사항이 구현된다.
.onTapGesture{
self.endTextEditing()
}
아이디 텍스트 필드에서 키보드의 Enter(Return) 버튼을 누르면 비밀번호 텍스트 필드로 넘어가기
이 요구사항에서는 @FocusState 변수를 옵셔널로 선언해서 각 TextField마다 submitLabel을 설정해두고 onSubmit을 통해 submit이 되면 자동으로 다음 TextField로 넘어가도록 하였다.
@FocusState private var focusedField: Field?
var body: some View {
NavigationStack {
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
.textContentType(.givenName)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
.textContentType(.familyName)
}
.onSubmit {
switch focusedField {
case .email:
focusedField = .password
default:
print("Done")
}
}
}
}
전체 코드
import SwiftUI
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ContentView: View {
enum Field {
case email
case password
}
@State private var email: String = ""
@State private var password: String = ""
@State private var toggling = false
@State private var isValid: Bool = false
@State private var notCorrectLogin: Bool = false
@State private var isShowingDetailView = false
@State private var isActive: Bool = false
@FocusState private var focusedField: Field?
private var correctEmail: String = "a_jb97@naver.com"
private var correctPassword: String = "mindol97"
var body: some View {
NavigationStack {
VStack(alignment: .leading) {
Text("Introduce your credentials")
.foregroundColor(.gray)
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .email)
.textContentType(.givenName)
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .password)
.textContentType(.familyName)
Toggle(isOn: $toggling) {
Text("Agree to terms and conditions")
.font(.subheadline)
}
}
.onSubmit {
switch focusedField {
case .email:
focusedField = .password
default:
print("Done")
}
}
.padding()
Button {
isValid = !checkEmailForm(input: email)
checkLogin(isEmail: email, isPassword: password)
if notCorrectLogin == false {
print("로그인 성공")
isActive = true
}
} label: {
Text("Sign in")
.frame(width: 300, height: 30)
}
.alert(isPresented: $isValid) {
Alert(title: Text("경고"), message: Text("이메일 형식이 올바르지 않습니다."), dismissButton: .default(Text("확인")))
}
.alert(isPresented: $notCorrectLogin) {
Alert(title: Text("주의\n"), message: Text("이메일, 또는 비밀번호가 일치하지 않습니다."), dismissButton: .default(Text("확인")))
}
.disabled(email.isEmpty || password.isEmpty || !toggling)
.buttonStyle(.borderedProminent)
.padding()
NavigationLink(destination: DetailView(), isActive: $isActive) {
EmptyView()
}
Spacer()
.navigationTitle("Log in")
}
.onTapGesture{
self.endTextEditing()
}
}
private func checkEmailForm(input: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: input)
}
private func checkLogin(isEmail: String, isPassword: String) {
if isEmail != correctEmail || isPassword != correctPassword {
notCorrectLogin = true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}