[Swift] RandomUser API Detail Screen

👉🏻 가로 무한 스크롤 컴포넌트에 상세화면 추가한 내용입니다.
This is the content of adding a detail screen to the horizontal infinite scroll component.

👉🏻 이전 포스트에서 아래의 코드를 수정하시면됩니다.
You can modify the code below from the previous post.

👉🏻UserDetailView코드는 새로 추가합니다.
Add new UserDetailView code.

👉🏻코드 / Code

✔️ UserDetailView

import SwiftUI

struct UserDetailView: View {
    let user: RandomUser  // RandomUser 모델이라고 가정
    
    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                // 큰 프로필 사진 / profile picture
                AsyncImage(url: URL(string: user.picture.large)) { phase in
                    switch phase {
                    case .success(let image):
                        image
                            .resizable()
                            .scaledToFit()
                            .frame(width: 180, height: 180)
                            .clipShape(Circle())
                            .shadow(radius: 8)
                    case .empty, .failure:
                        ProgressView()
                            .frame(width: 180, height: 180)
                    @unknown default:
                        EmptyView()
                    }
                }
                
                // 이름,?? ->nil이면 오른쪽 값을 사용
                // name, ?? -> if nil, use the right value
                Text(String("\(user.name.title ?? "") \(user.name.first) \(user.name.last)"))
                    .font(.title)
                    .fontWeight(.bold)
                
                // 기본 정보 / basic information
                VStack(alignment: .leading, spacing: 12) {
                    DetailRow(icon: "person.fill", title: "성별", value: user.gender.capitalized)
                    DetailRow(icon: "calendar", title: "나이", value: "\(user.dob.age)세")
                    DetailRow(icon: "envelope.fill", title: "이메일", value: user.email)
                    DetailRow(icon: "phone.fill", title: "전화번호", value: user.phone)
                    DetailRow(icon: "mobilephone", title: "휴대폰", value: user.cell)
                }
                .padding(.horizontal)
                
                // 주소 / address
                VStack(alignment: .leading, spacing: 8) {
                    Text("주소")
                        .font(.title3)
                        .fontWeight(.semibold)
                    
                        // street 있는 경우만 표시 (옵셔널 처리)
                        // Display only when there is a street (optional handling)
                        if let street = user.location?.street {
                            Text("\(street.number) \(street.name)")
                        } else {
                            Text("거리 정보 없음")
                        }
                        
                        Text("\(user.location?.city ?? ""), \(user.location?.state ?? "")")
                            .foregroundStyle(.secondary)
                        
                        Text(user.location?.country ?? "국가 정보 없음")
                            .foregroundStyle(.secondary)
                    
                    //Text("\(user.location.country) \(user.location.postcode)")
                }
                .padding()
                .frame(maxWidth: .infinity, alignment: .leading)
                .background(Color(.systemGray6))
                .clipShape(RoundedRectangle(cornerRadius: 12))
                .padding(.horizontal)
            }
            .padding(.vertical, 32)
        }
        .navigationTitle("\(user.name.first) \(user.name.last)")
        .navigationBarTitleDisplayMode(.inline)
    }
}

// basic information row
struct DetailRow: View {
    let icon: String
    let title: String
    let value: String
    
    var body: some View {
        HStack {
            Image(systemName: icon)
                .frame(width: 24)
                .foregroundStyle(.blue)
            
            Text(title)
                .foregroundStyle(.secondary)
            
            Spacer()
            
            Text(value)
        }
    }
}

✔️RandomUserModel

struct RandomUser: Codable, Identifiable, Equatable, Hashable { // hashaable추가 / add hashable
 
    let login: Login
    let name: Name
    let picture: Picture
    let gender: String
    
    // 추가 / add
    let dob: Dob
    let email: String
    let phone: String
    let cell: String
    let location: Location?
    
    // Identifiable
    var id: String { login.uuid }
    
    // MARK: - 내부 구조체
    struct Login: Codable, Equatable, Hashable {
        let uuid: String
        // 필요하면 username, password 등 추가 가능
        // You can add username, password, etc. if needed.
    }
    
    struct Name: Codable, Equatable, Hashable{
        let title: String?   // "Mr", "Mrs" ...
        let first: String
        let last: String
    }
    
    struct Picture: Codable, Equatable, Hashable{
        let large: String
        let medium: String?
        let thumbnail: String?
    }
    // dob 구조체 추가 (가장 흔한 형태) / add Dob struct
    struct Dob: Codable, Equatable, Hashable {
        let date: String     // 예: "1990-05-14T..." (ISO 8601 형식)
        let age: Int
    }
    // add location
    struct Location: Codable, Hashable {
            let street: Street
            let city: String
            let state: String
            let country: String
//            let postcode: StringOrInt // String 또는 Int 올 수 있음
//            let coordinates: Coordinates
//            let timezone: Timezone
            
            struct Street: Codable, Hashable {
                let number: Int
                let name: String
            }
            // ... 나머지
        }
}

✔️HorizontalUserScrollView

import SwiftUI

// 가로 무한 스크롤 컴포넌트
// Horizontal infinite scroll component
struct HorizontalUserScrollView: View {
    let title: String
    let url: String
    @StateObject var viewModel: RandomUserViewModel
    
    var body: some View {
        VStack(alignment: .leading) {
            // 섹션 제목 / section title
            Text(title)
                .font(.title2)
                .fontWeight(.bold)
                .padding(.horizontal)
            
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(spacing: 20) {
                    ForEach(viewModel.users) { user in
                        NavigationLink(value: user) {
                        VStack {
                            Text("\(user.name.first) \(user.name.last)")
                                .font(.headline)
                                .multilineTextAlignment(.center)
                            
                            AsyncImage(url: URL(string: user.picture.large)) { phase in
                                switch phase {
                                case .success(let image):
                                    image
                                        .resizable()
                                        .scaledToFit()
                                        .frame(width: 120, height: 120)
                                        .clipShape(Circle())
                                case .empty, .failure:
                                    ProgressView()
                                        .frame(width: 120, height: 120)
                                @unknown default:
                                    EmptyView()
                                }
                            }
                        }
                        .frame(width: 160)
                        .padding()
                        .background(Color(.secondarySystemBackground))
                        .cornerRadius(16)
                        .shadow(radius: 3)
                    }
                        
                        // 마지막 사용자 카드가 나타날 때 추가 로드
                        // Load additional cards when the last user card appears
                        // NavigationLink 바깥
                        .onAppear {
                            if user == viewModel.users.last {
                                viewModel.fetchMoreUsers(url:url)  // 추가 로드 (append)
                                // viewModel.fetchMoreUsers(url: "https://randomuser.me/api/?results=20")  // 추가 로드 (append)
                            }
                        }
                    }
                    
                    // 로딩 인디케이터 / loading indicator
                    if viewModel.isLoadingMore {
                        ProgressView("불러오는 중…")
                            .frame(width: 160, height: 240)
                            .padding()
                    }
                }
                .padding(.horizontal)
            }
        }
        // 섹션이 화면에 나타날 때 초기 데이터 로드 (한 번만!)
        // Load initial data when the section appears on screen (only once!)
        .onAppear {
            if viewModel.users.isEmpty {
                Task {
                    // 전체 교체 (초기 로드 전용)
                    // Full replacement (initial load only)
                    await viewModel.loadInitialUsers(url:url)
                    // await viewModel.loadInitialUsers(url:"https://randomuser.me/api/?results=20")  // 전체 교체 (초기 로드 전용)
                }
            }
        }
        // 스택이동 UserDetailView
        .navigationDestination(for: RandomUser.self) { user in
                    UserDetailView(user: user)
        }
    }
}

👉🏻스크린 샷 / ScreenShot

Leave a Reply