Contents 非表示
Clean Architectureとは
Clean Architecture = 「責務ごとにコードを層で分けて、依存関係を一方向に保つ」アーキテクチャ。
この構造を使うことで、
- UIロジックとビジネスロジックが分離できる
- APIやデータ構造が変わっても柔軟に対応できる
- テストもしやすい
っていう便利なアーキテクチャに仕上がる。
構造の全体像(ざっくり4層)

各レイヤーの役割(カンタン説明)
レイヤー | 役割のざっくりイメージ |
---|---|
Presentation | SwiftUIやViewModelで、ユーザーの操作を受け取る |
Domain | アプリの本質的な「やること」を定義(UseCaseなど) |
Data | データの取得・保存方法をまとめる(API, DBなど) |
External | 実際の通信処理やストレージ(URLSessionなど) |
依存関係のルール(重要)
- 上の層は下の層に依存しない =
ViewModel
はUseCase
を知ってるけど、UseCase
はViewModel
のことを一切知らない。 - 抽象プロトコル(interface)で依存方向を逆転させる
Clean Architectureで考えるコツ
- 「誰が何をしたいのか」からスタート(ユースケースを言語化)
- 「それをどう達成するか?」を層ごとに分解
- SwiftUIのコード量を減らして、責任を分散
簡単な天気アプリでざっくり構成と各レイヤの役割を理解する


ファイル構成
- WeatherView.swift
- WeatherViewModel.swift
- FetchWeatherUseCase.swift
- WeatherRepository.swift
- WeatherAPIClient.swift
- Weather.swift
Clean Architecture構成図
ファイル名 | レイヤー | 役割 | 例え (ポケモン) |
---|---|---|---|
WeatherView.swift | Presentation | UI表示。ユーザーの入力をViewModelに伝える | バトル画面 |
WeatherViewModel.swift | Presentation | Viewの状態管理。UseCaseを呼び出しUI更新 | ポケモンのトレーナー |
FetchWeatherUseCase.swift | Domain | ビジネスロジックの中心。Repositoryを使ってデータ取得 | 「たいあたり」を出す命令 |
WeatherRepository.swift | Data | データの中継役。APIClientから取得しUseCaseへ渡す | ポケモン図鑑 |
WeatherAPIClient.swift | Data (Low-Level) | 実際にAPIへ通信してデータを取得する | 通信機器 |
Weather.swift | Entity/Model | APIのレスポンスを構造化(DTO) | ポケモンのステータス |
実装
import Foundation
struct Weather: Identifiable {
let id = UUID()
let cityName: String
let temperature: Double
let description: String
}
import SwiftUI
@MainActor
final class WeatherViewModel: ObservableObject {
@Published var city: String = ""
@Published var weather: Weather?
@Published var errorMessage: String?
@Published var isLoading: Bool = false
private let fetchWeatherUseCase: FetchWeatherUseCase
// DI経由で UseCase を注入
init(fetchWeatherUseCase: FetchWeatherUseCase) {
self.fetchWeatherUseCase = fetchWeatherUseCase
}
func fetchWeather() {
isLoading = true
Task {
do {
// UseCase経由で非同期に天気情報を取得
let result = try await fetchWeatherUseCase.execute(city: city)
weather = result
errorMessage = nil
isLoading = false
} catch (let error) {
weather = nil
errorMessage = error.localizedDescription
isLoading = false
}
}
}
}
import Foundation
// Presentation層から呼び出され、ビジネスロジックを提供
protocol FetchWeatherUseCase {
func execute(city: String) async throws -> Weather
}
// Repositoryを通じてデータ取得処理を実行
final class FetchWeatherUseCaseImpl: FetchWeatherUseCase {
private let repository: WeatherRepository
// DIによって Repository を注入
init(repository: WeatherRepository) {
self.repository = repository
}
// ユースケースの実行処理(都市名を指定して天気を取得)
func execute(city: String) async throws -> Weather {
try await repository.fetchWeather(for: city)
}
}
import Foundation
// Domain層やUseCaseはこのプロトコルに依存する
protocol WeatherRepository {
func fetchWeather(for city: String) async throws -> Weather
}
// APIClientからDTOを取得し、アプリ用のEntityに変換して返す
final class WeatherRepositoryImpl: WeatherRepository {
private let apiClient: WeatherAPIClient
// DIによりAPIクライアントを注入
init(apiClient: WeatherAPIClient) {
self.apiClient = apiClient
}
// DTO(APIレスポンス) → Entity(アプリ内モデル)への変換
func fetchWeather(for city: String) async throws -> Weather {
let dto = try await apiClient.fetchWeather(for: city)
return Weather(
cityName: dto.name,
temperature: dto.main.temp,
description: dto.weather.first?.description ?? "No description"
)
}
}
import Foundation
// OpenWeather APIのレスポンスをマッピングするDTO
// アプリ内部ではこのまま使わず、RepositoryでEntityへ変換する
struct WeatherDTO: Decodable {
struct Main: Decodable {
let temp: Double
}
struct WeatherInfo: Decodable {
let description: String
}
let name: String
let main: Main
let weather: [WeatherInfo]
}
// OpenWeather APIに通信して、WeatherDTOを取得するクラス(Data Layer)
final class WeatherAPIClient {
private let apiKey = "<#OpenWeatherAPIKey#>"
func fetchWeather(for city: String) async throws -> WeatherDTO {
let urlString = "https://api.openweathermap.org/data/2.5/weather?q=\(city)&appid=\(apiKey)&units=metric"
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(WeatherDTO.self, from: data)
}
}
import SwiftUI
struct WeatherView: View {
@StateObject var viewModel: WeatherViewModel
var body: some View {
NavigationView {
VStack(spacing: 16) {
// 🔍 検索
HStack {
TextField("都市名を入力", text: $viewModel.city)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.leading)
Button(action: {
viewModel.fetchWeather()
}) {
Image(systemName: "magnifyingglass")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Circle())
}
}
.padding(.horizontal)
// 🔄 ローディング
if viewModel.isLoading {
ProgressView("取得中...")
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
// 🌤 天気表示
if let weather = viewModel.weather {
WeatherCardView(weather: weather)
.transition(.opacity.combined(with: .move(edge: .top)))
.animation(.spring(), value: weather.cityName)
}
// ❗️エラー
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.padding(.top, 8)
}
Spacer()
}
.navigationTitle("🌦天気アプリ")
.padding()
}
}
}
// MARK: - WeatherCardView
struct WeatherCardView: View {
let weather: Weather
var body: some View {
VStack(spacing: 12) {
Image(systemName: iconFor(description: weather.description))
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.foregroundColor(.yellow)
Text(weather.cityName)
.font(.title)
.bold()
Text("\(weather.temperature, specifier: "%.1f")℃")
.font(.system(size: 36, weight: .semibold))
Text(weather.description.capitalized)
.font(.subheadline)
.foregroundColor(.gray)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color.white)
.cornerRadius(20)
.shadow(color: .gray.opacity(0.3), radius: 10, x: 0, y: 4)
.padding(.horizontal)
}
private func iconFor(description: String) -> String {
switch description.lowercased() {
case let desc where desc.contains("cloud"):
return "cloud.fill"
case let desc where desc.contains("rain"):
return "cloud.rain.fill"
case let desc where desc.contains("sun"):
return "sun.max.fill"
case let desc where desc.contains("clear"):
return "sun.max.fill"
default:
return "cloud.sun.fill"
}
}
}
アプリのエントリーポイントは以下のように
import SwiftUI
@main
struct <#プロジェクト名#>App: App {
var body: some Scene {
WindowGroup {
let apiClient = WeatherAPIClient()
let respository = WeatherRepositoryImpl(apiClient: apiClient)
let useCase = FetchWeatherUseCaseImpl(repository: respository)
let viewModel = WeatherViewModel(fetchWeatherUseCase: useCase)
WeatherView(viewModel: viewModel)
}
}