่žขๅน•ๆˆชๅœ–

user_list_app_9_21

ๆ–‡ไปถๆžถๆง‹

.
โ”œโ”€โ”€ Feature/
โ”‚   โ”œโ”€โ”€ Data/ 
โ”‚   โ”‚   โ”œโ”€โ”€ Network/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ CloudUserApi.swift
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ CloudUserRequest.swift
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ CloudUserResponse.swift
โ”‚   โ”‚   โ””โ”€โ”€ Service/
โ”‚   โ”‚       โ””โ”€โ”€ CloudApiService.swift
โ”‚   โ”œโ”€โ”€ Domain/ 
โ”‚   โ”‚   โ”œโ”€โ”€ Entities/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ UserEntity.swift
โ”‚   โ”‚   โ””โ”€โ”€ UseCases/
โ”‚   โ”‚       โ”œโ”€โ”€ CloudUserUseCase.swift
โ”‚   โ”‚       โ””โ”€โ”€ CloudSyncUseCase.swift
โ”‚   โ”œโ”€โ”€ Presentation/ 
โ”‚   โ”‚   โ””โ”€โ”€ CloudUserViewModel.swift
โ”‚   โ””โ”€โ”€ UI/
โ”‚       โ”œโ”€โ”€ Components/
โ”‚       โ”‚   โ”œโ”€โ”€ UserRowView.swift
โ”‚       โ”‚   โ”œโ”€โ”€ UserRowView.swift
โ”‚       โ”‚   โ””โ”€โ”€ UserRowView.swift
โ”‚       โ””โ”€โ”€ UserListView.swift
โ””โ”€โ”€ UserApp.swift

App Entry Point

UserApp.swift ``` @main struct UserApp: App { var body: some Scene { WindowGroup { UserListView() } } } #Preview { UserListView() } ```

UI Layer

Feature/UI/UserListView.swift ``` import SwiftUI struct UserListView: View { @StateObject private var viewModel = CloudUserViewModel() var body: some View { NavigationView { ZStack { if viewModel.isLoading { ProgressView("Loading users...") } else if let error = viewModel.errorMessage { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 48)) .foregroundColor(.orange) Text("Error") .font(.headline) Text(error) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) Button("Retry") { Task { await viewModel.loadUsers() } } .buttonStyle(.borderedProminent) } } else { List(viewModel.filteredUsers) { user in NavigationLink(destination: UserDetailView(user: user)) { UserRowView(user: user) } } .searchable(text: $viewModel.searchText, prompt: "Search users") } } .navigationTitle("Users") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { Task { await viewModel.syncUsers() } } label: { Image(systemName: "arrow.clockwise") } .disabled(viewModel.isLoading) } } } .task { await viewModel.loadUsers() } } } ```
Feature/UI/Components/UserRowView.swift ``` import SwiftUI struct UserRowView: View { let user: UserEntity var body: some View { VStack(alignment: .leading, spacing: 8) { Text(user.name) .font(.headline) HStack { Image(systemName: "person.circle") .foregroundColor(.blue) Text(user.username) .font(.subheadline) .foregroundColor(.secondary) } HStack { Image(systemName: "building.2") .foregroundColor(.purple) Text(user.company) .font(.subheadline) .foregroundColor(.secondary) } } .padding(.vertical, 4) } } ```
Feature/UI/Components/UserDetailView.swift ``` import SwiftUI struct UserDetailView: View { let user: UserEntity var body: some View { List { Section("Personal Information") { DetailRow(icon: "person.fill", title: "Name", value: user.name) DetailRow(icon: "at", title: "Username", value: user.username) DetailRow(icon: "envelope.fill", title: "Email", value: user.email) DetailRow(icon: "phone.fill", title: "Phone", value: user.phone) DetailRow(icon: "globe", title: "Website", value: user.website) } Section("Location") { DetailRow(icon: "location.fill", title: "City", value: user.city) } Section("Company") { DetailRow(icon: "building.2.fill", title: "Company", value: user.company) VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "quote.opening") .foregroundColor(.blue) Text("Catch Phrase") .font(.subheadline) .foregroundColor(.secondary) } Text(user.catchPhrase) .font(.body) .italic() .padding(.leading, 24) } } } .navigationTitle(user.name) .navigationBarTitleDisplayMode(.inline) } } ```
Feature/UI/Components/DetailRow.swift ``` import SwiftUI struct DetailRow: View { let icon: String let title: String let value: String var body: some View { HStack { Image(systemName: icon) .foregroundColor(.blue) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.caption) .foregroundColor(.secondary) Text(value) .font(.body) } } } } ```

Presentation Layer

Feature/Presentation/CloudUserViewModel.swift ``` import Combine import Foundation class CloudUserViewModel: ObservableObject { @Published var users: [UserEntity] = [] @Published var isLoading = false @Published var errorMessage: String? @Published var searchText = "" private let getUsersUseCase = CloudUserUseCaseImpl() private let syncUseCase = CloudSyncUseCaseImpl() var filteredUsers: [UserEntity] { if searchText.isEmpty { return users } return users.filter { user in user.name.localizedCaseInsensitiveContains(searchText) || user.username.localizedCaseInsensitiveContains(searchText) || user.email.localizedCaseInsensitiveContains(searchText) || user.company.localizedCaseInsensitiveContains(searchText) } } func loadUsers() async { isLoading = true errorMessage = nil do { users = try await getUsersUseCase.execute() } catch { errorMessage = error.localizedDescription } isLoading = false } func syncUsers() async { isLoading = true errorMessage = nil do { try await syncUseCase.execute() users = try await getUsersUseCase.execute() } catch { errorMessage = error.localizedDescription } isLoading = false } } ```

Domain Layer

Feature/Domain/Entities/UserEntity.swift ``` struct UserEntity: Identifiable { let id: Int let name: String let username: String let email: String let city: String let company: String let phone: String let website: String let catchPhrase: String } ```
Feature/Domain/UseCases/CloudUserUseCase.swift ``` protocol CloudUserUseCase { func execute() async throws -> [UserEntity] } class CloudUserUseCaseImpl: CloudUserUseCase { private let service: CloudApiService init(service: CloudApiService = CloudApiServiceImpl()) { self.service = service } func execute() async throws -> [UserEntity] { let responses = try await service.getUsers() return responses.map { response in UserEntity( id: response.id, name: response.name, username: response.username, email: response.email, city: response.address.city, company: response.company.name, phone: response.phone, website: response.website, catchPhrase: response.company.catchPhrase ) } } } ```
Feature/Domain/UseCases/CloudSyncUseCase.swift ``` protocol CloudSyncUseCase { func execute() async throws } class CloudSyncUseCaseImpl: CloudSyncUseCase { private let service: CloudApiService init(service: CloudApiService = CloudApiServiceImpl()) { self.service = service } func execute() async throws { try await service.syncUsers() } } ```

Data Layer

Feature/Data/Network/CloudUserRequest.swift ``` struct CloudUserRequest: Codable { var endpoint: String = "/users" } ```
Feature/Data/Network/CloudUserResponse.swift ``` struct CloudUserResponse: Codable { let id: Int let name: String let username: String let email: String let address: Address let phone: String let website: String let company: Company struct Address: Codable { let street: String let suite: String let city: String let zipcode: String let geo: Geo struct Geo: Codable { let lat: String let lng: String } } struct Company: Codable { let name: String let catchPhrase: String let bs: String } } ```
Feature/Data/Network/CloudUserApi.swift ``` import Foundation protocol CloudUserApi { func fetchUsers() async throws -> [CloudUserResponse] } class CloudUserApiImpl: CloudUserApi { private let baseURL = "https://jsonplaceholder.typicode.com" func fetchUsers() async throws -> [CloudUserResponse] { guard let url = URL(string: "\(baseURL)/users") else { throw URLError(.badURL) } let (data, _) = try await URLSession.shared.data(from: url) let users = try JSONDecoder().decode([CloudUserResponse].self, from: data) return users } } ```
Feature/Data/Service/CloudApiService.swift ``` protocol CloudApiService { func getUsers() async throws -> [CloudUserResponse] func syncUsers() async throws } class CloudApiServiceImpl: CloudApiService { private let api: CloudUserApi init(api: CloudUserApi = CloudUserApiImpl()) { self.api = api } func getUsers() async throws -> [CloudUserResponse] { return try await api.fetchUsers() } func syncUsers() async throws { _ = try await api.fetchUsers() } } ```