前回からの続きです。
前回はJetpack Composeでログイン画面を作成しました。
今回はiOSのログイン画面を作成していきます。
前回のAndroidの画面を参考に作成していきます。
完成イメージ
今回もわかりやすいようにLoginViewに使用したカラーを記載しておきます。
extension Color {
static let orangeStart = Color(red: 1.0, green: 0.709, blue: 0.4) // #FFB366
static let orangeEnd = Color(red: 1.0, green: 0.4157, blue: 0.239) // #FF6A3D
static let pink = Color(red: 1.0, green: 0.3607, blue: 0.6156) // #FF5C9D
static let cardBg = Color.white.opacity(0.85)
static let cardBorder = Color.white.opacity(0.12)
static let accentPurple = Color(red: 0.49, green: 0.34, blue: 0.76) // #7E57C2
}
UIの作成なので、ロジック部分はTODOにしています。
1. バックグラウンドとUI全体の構造
import SwiftUI
// MARK: - Main Login View
struct LoginView: View {
@State private var email: String = ""
@State private var password: String = ""
@State private var showPassword: Bool = false
@State private var isLoading: Bool = false
@State private var emailError: String? = nil
@State private var generalError: String? = nil
var body: some View {
ZStack {
// グラデーション設定
LinearGradient(gradient: Gradient(colors: [Color.orangeStart, Color.orangeEnd]),
startPoint: .topLeading,
endPoint: .bottomTrailing)
.ignoresSafeArea()
Circle()
.fill(Color.white.opacity(0.06))
.frame(width: 120, height: 120)
.position(x: 0, y: 0)
.offset(x: 30, y: 40)
.ignoresSafeArea()
VStack(spacing: 18) {
Spacer().frame(height: 26)
HeaderSection()
AuthCard(
email: $email,
password: $password,
showPassword: $showPassword,
isLoading: $isLoading,
emailError: $emailError,
generalError: $generalError
)
.padding(.horizontal, 16)
Spacer()
}
}
}
}
ここでは、バックグラウンドの設定やUI全体の構造を決めています。
VStack
でこの画面に表示するものを記載しております。
HeaderSection
とAuthCard
に分けてます。
SwiftUIではVStack(spacing: 18)
とすることでViewのスペースを決めることができるのでこちらで決めております。
2. HeaderSection
// MARK: - HeaderSection (丸2つ入りの角丸ボックス)
struct HeaderSection: View {
var body: some View {
VStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 28)
.fill(Color.white.opacity(0.18))
.frame(width: 110, height: 110)
.shadow(color: Color.black.opacity(0.12), radius: 6, x: 0, y: 2)
HStack(spacing: 8) {
Circle()
.fill(Color.pink.opacity(0.9))
.frame(width: 40, height: 40)
Circle()
.fill(Color.orangeEnd.opacity(0.9))
.frame(width: 40, height: 40)
}
}
Text("ようこそ")
.font(.system(size: 28, weight: .semibold))
.foregroundColor(.white)
}
}
}
Android同様にアイコンと『ようこそ』を表示しています。
前回のAdnroidと見比べてもらうと、Viewのサイズなど設定方法が異なるので書き方が変わっているが、同じようなことをしています。
3. PasswordField
// MARK: - PasswordField (表示切替つき)
struct PasswordField: View {
@Binding var password: String
@Binding var showPassword: Bool
var onCommit: (() -> Void)? = nil
var body: some View {
HStack(spacing: 12) {
Image(systemName: "lock.fill")
.foregroundColor(.black)
Group {
if showPassword {
TextField("パスワード", text: $password, onCommit: {
onCommit?()
})
.autocapitalization(.none)
.submitLabel(.done)
} else {
SecureField("パスワード", text: $password, onCommit: {
onCommit?()
})
.submitLabel(.done)
}
}
Button(action: { showPassword.toggle() }) {
Image(systemName: showPassword ? "eye.fill" : "eye.slash.fill")
.foregroundColor(.black)
}
}
.padding(12)
}
}
Androidではパスワードのマスクをする入力を確認する機能があるのですが、SwiftUIではないので、個別で作成しておきます。
TextField
とSecureField
をshowPassword
によって切り替えることで実装してます。
4. AuthCard
// MARK: - AuthCard (カード本体: 入力欄・ボタン・新規登録リンク)
struct AuthCard: View {
@Binding var email: String
@Binding var password: String
@Binding var showPassword: Bool
@Binding var isLoading: Bool
@Binding var emailError: String?
@Binding var generalError: String?
private var isValid: Bool {
return !email.trimmingCharacters(in: .whitespaces).isEmpty && password.count >= 6 && !isLoading
}
var body: some View {
VStack {
VStack(spacing: 8) {
HStack {
Text("ログイン")
.font(.system(size: 22, weight: .semibold))
.fontWeight(.bold)
Spacer()
}
Text("メールアドレスとパスワードでログイン")
.font(.system(size: 12, weight: .thin))
.frame(maxWidth: .infinity, alignment: .leading)
Spacer().frame(height: 4)
// エラー表示(TODO: 実データがあれば表示)
if let e = emailError {
Text(e).foregroundColor(.red).bold()
Spacer().frame(height: 4)
}
if let g = generalError {
Text(g).foregroundColor(.red).bold()
Spacer().frame(height: 4)
}
HStack(spacing: 12) {
Image(systemName: "envelope.fill").foregroundColor(.black)
TextField("メールアドレス", text: $email)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.submitLabel(.next)
}
.padding(12)
.background(Color.white.opacity(0.02))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color.black, lineWidth: 1)
)
Spacer().frame(height: 2)
PasswordField(password: $password, showPassword: $showPassword, onCommit: {
// TODO: ログイン実行
})
.background(Color.white.opacity(0.02))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color.black, lineWidth: 1)
)
Button(action: {
// TODO: ログイン処理を呼ぶ
}) {
HStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.9)
Text("読み込み中...")
.foregroundColor(.white)
.fontWeight(.semibold)
} else {
Text("ログイン")
.foregroundColor(.white)
.fontWeight(.semibold)
.font(.system(size: 16))
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.background(isValid ? Color.accentPurple : Color.gray.opacity(0.4))
.cornerRadius(12)
.scaleEffect(isValid ? 1.0 : 0.98)
.disabled(!isValid)
HStack {
Text("アカウントをお持ちでないですか?")
.font(.system(size: 12, weight: .regular))
Button(action: {
// TODO 新規登録画面へ遷移
}) {
Text("新規登録")
.font(.system(size: 12, weight: .regular))
.foregroundColor(Color.accentPurple)
}
}
}
.padding(18)
}
.background(Color.cardBg)
.cornerRadius(18)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.cardBorder, lineWidth: 1)
)
.shadow(color: Color.black.opacity(0.12), radius: 8, x: 0, y: 4)
}
}
こちらが画面の本体と言ってもいい部分です。
白枠のカード表示になります。
パスワードは表示できるように切り替えられるようになってます。
同じような見た目になったと思います。
テキストのサイズ、色、フォントなどはそこまで厳密に決めていないのでブレがあるかと思いますが、入力欄などはOS標準を使用しなるべくそれぞれのOSっぽさを出してます。
次回予告
次回は、FirebaseAuthを使って実際にログイン処理を実装していきます!