0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで作るログイン画面(UI編)〜ゼロからの KMP × Firebase〜

Posted at

前回からの続きです。

前回はJetpack Composeでログイン画面を作成しました。

今回はiOSのログイン画面を作成していきます。

前回のAndroidの画面を参考に作成していきます。

完成イメージ

Simulator Screenshot - iPhone 16 Pro - 2025-09-28 at 11.43.18.png

今回もわかりやすいようにLoginViewに使用したカラーを記載しておきます。

LoginView.swift
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全体の構造

LoginView.swift
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でこの画面に表示するものを記載しております。
HeaderSectionAuthCardに分けてます。
SwiftUIではVStack(spacing: 18)とすることでViewのスペースを決めることができるのでこちらで決めております。

2. HeaderSection

LoginView.swift
// 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

LoginView.swift
// 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ではないので、個別で作成しておきます。
TextFieldSecureFieldshowPasswordによって切り替えることで実装してます。

4. AuthCard

LoginView.swift
// 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を使って実際にログイン処理を実装していきます!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?