๐๐ป ๊ฐ๋ก ๋ฌดํ ์คํฌ๋กค ์ปดํฌ๋ํธ์ ์์ธํ๋ฉด ์ถ๊ฐํ ๋ด์ฉ์
๋๋ค.
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

