[SwiftUI] SwiftUI로 Log in 화면 구현하기

Jade·2023년 7월 5일
0

모공모공 스터디

목록 보기
1/1
post-thumbnail

현재 멋쟁이사자처럼에서 진행하는 '테킷 앱스쿨: iOS 3기' 교육을 받고 있다.
이곳에서 '모공모공' 이라는 스터디에 참여하고 있는데, 모공모공 태그가 붙은 글들은 이 스터디에서 진행한 프로젝트나 공부한 주제들을 다룰 것이다 💭


오늘은 제목처럼 SwiftUI를 사용한 Log in 화면 구현 과정을 풀어내 보겠다 🧐

먼저, 요구사항은 다음과 같다.

  • 아이디에는 Email 형식만 입력
    - 형식이 맞지 않으면 Alert 창 띄우기
  • 아이디랑 비밀번호 칸이 두 개가 다 입력되어 있고, 토글 버튼이 true여야 회원가입 버튼이 활성화
  • 비밀번호 텍스트 필드 값은 암호 표시
  • 아이디와 비밀번호가 설정한 것과 일치하면 다른 뷰로 넘어가기
    - 일치하지 않으면 Alert 창 띄우기
  • 화면의 빈 부분을 터치하면 키보드 숨기기
  • 아이디 텍스트 필드에서 키보드의 Enter(Return) 버튼을 누르면 비밀번호 텍스트 필드로 넘어가기

이제 기본 뼈대 구현 방법은 제외하고, 각 요구사항 별로 내가 구현한 방법들을 설명해 보겠다 💡


아이디에는 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()
    }
}
profile
응애 iOS 개발자

0개의 댓글