_legacy/mobile/swiftui-patterns/SKILL.md
SwiftUI開発パターン・ベストプラクティス。状態管理、ナビゲーション、レイアウト、アニメーション、パフォーマンス最適化など、モダンなSwiftUIアプリケーション開発の実践的なガイド。
npx skillsauth add gaku52/claude-code-skills swiftui-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
SwiftUIアプリケーション開発における実践的なパターンとベストプラクティスを提供します。
対象:
このSkillでできること:
このガイドで学べること: SwiftUI状態管理、ナビゲーション設計、レイアウトシステム、パフォーマンス最適化 公式で確認すべきこと: 最新のSwiftUIアップデート、新しいAPIとモディファイア、iOS新機能
SwiftUI Documentation - Apple公式SwiftUIドキュメント
SwiftUI by Example - 実践的なSwiftUI学習リソース
Human Interface Guidelines - iOSデザインガイドライン
Combine Framework - リアクティブプログラミング
使用場面:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
ベストプラクティス:
使用場面:
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Setting", isOn: $isOn)
}
}
struct ParentView: View {
@State private var setting = false
var body: some View {
ToggleView(isOn: $setting)
}
}
ベストプラクティス:
使用場面:
class TimerManager: ObservableObject {
@Published var seconds = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.seconds += 1
}
}
func stop() {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject private var timerManager = TimerManager()
var body: some View {
VStack {
Text("\(timerManager.seconds)")
Button("Start") { timerManager.start() }
Button("Stop") { timerManager.stop() }
}
}
}
ベストプラクティス:
使用場面:
struct SettingsView: View {
@ObservedObject var settings: AppSettings
var body: some View {
Form {
Toggle("Notifications", isOn: $settings.notificationsEnabled)
Toggle("Dark Mode", isOn: $settings.darkModeEnabled)
}
}
}
使用場面:
class UserSession: ObservableObject {
@Published var isLoggedIn = false
@Published var username = ""
}
@main
struct MyApp: App {
@StateObject private var session = UserSession()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(session)
}
}
}
struct ProfileView: View {
@EnvironmentObject var session: UserSession
var body: some View {
Text("Hello, \(session.username)")
}
}
ベストプラクティス:
基本パターン:
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Settings", value: Route.settings)
NavigationLink("Profile", value: Route.profile)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .settings:
SettingsView()
case .profile:
ProfileView()
}
}
}
}
}
enum Route: Hashable {
case settings
case profile
}
プログラマティックナビゲーション:
struct MainView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("Go to Detail") {
path.append(DetailRoute.detail(id: 1))
}
Button("Go Deep") {
path.append(DetailRoute.detail(id: 1))
path.append(DetailRoute.subDetail(id: 2))
}
Button("Pop to Root") {
path.removeLast(path.count)
}
}
.navigationDestination(for: DetailRoute.self) { route in
DetailView(route: route)
}
}
}
}
Sheet:
struct ContentView: View {
@State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet = true
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}
FullScreenCover:
struct ContentView: View {
@State private var showingFullScreen = false
var body: some View {
Button("Show Full Screen") {
showingFullScreen = true
}
.fullScreenCover(isPresented: $showingFullScreen) {
FullScreenView()
}
}
}
VStack - 垂直配置:
VStack(alignment: .leading, spacing: 16) {
Text("Title")
.font(.headline)
Text("Subtitle")
.font(.subheadline)
Text("Body")
.font(.body)
}
HStack - 水平配置:
HStack(alignment: .center, spacing: 8) {
Image(systemName: "star.fill")
Text("Favorite")
Spacer()
Text("100")
}
ZStack - 重ね配置:
ZStack(alignment: .bottomTrailing) {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
Text("Overlay")
.padding()
.background(.ultraThinMaterial)
}
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let rows = computeRows(proposal: proposal, subviews: subviews)
let height = rows.reduce(0) { $0 + $1.height } + CGFloat(rows.count - 1) * spacing
return CGSize(width: proposal.width ?? 0, height: height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let rows = computeRows(proposal: proposal, subviews: subviews)
var y = bounds.minY
for row in rows {
var x = bounds.minX
for index in row.indices {
subviews[index].place(at: CGPoint(x: x, y: y), proposal: .unspecified)
x += subviews[index].sizeThatFits(.unspecified).width + spacing
}
y += row.height + spacing
}
}
private func computeRows(proposal: ProposedViewSize, subviews: Subviews) -> [[Int]] {
// Flow layout implementation
[]
}
}
適切な使用例:
struct AdaptiveView: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > 600 {
HStack {
Sidebar()
Content()
}
} else {
VStack {
Content()
}
}
}
}
}
避けるべきパターン:
// ❌ 不必要なGeometryReader
GeometryReader { geometry in
Text("Hello")
.frame(width: geometry.size.width) // .frame(maxWidth: .infinity)で十分
}
// ✅ より良い方法
Text("Hello")
.frame(maxWidth: .infinity)
// Model
struct User: Identifiable {
let id: UUID
var name: String
var email: String
}
// ViewModel
class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var error: Error?
private let repository: UserRepository
init(repository: UserRepository = .shared) {
self.repository = repository
}
@MainActor
func loadUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await repository.fetchUsers()
} catch {
self.error = error
}
}
}
// View
struct UserListView: View {
@StateObject private var viewModel = UserListViewModel()
var body: some View {
List(viewModel.users) { user in
UserRow(user: user)
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
.task {
await viewModel.loadUsers()
}
.alert(error: $viewModel.error)
}
}
// State
struct AppState {
var users: [User] = []
var isLoading = false
}
// Action
enum AppAction {
case loadUsers
case usersLoaded([User])
case usersFailed(Error)
}
// Reducer
func appReducer(state: inout AppState, action: AppAction) {
switch action {
case .loadUsers:
state.isLoading = true
case .usersLoaded(let users):
state.users = users
state.isLoading = false
case .usersFailed:
state.isLoading = false
}
}
// Store
class Store: ObservableObject {
@Published private(set) var state = AppState()
func send(_ action: AppAction) {
appReducer(state: &state, action: action)
}
}
EquatableView:
struct ExpensiveView: View, Equatable {
let data: ComplexData
var body: some View {
// 重い描画処理
ComplexRenderingView(data: data)
}
static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
lhs.data.id == rhs.data.id
}
}
struct ParentView: View {
@State private var counter = 0
let data: ComplexData
var body: some View {
VStack {
Text("Counter: \(counter)")
Button("Increment") { counter += 1 }
// dataが変わらない限り再描画されない
EquatableView(data: data)
.equatable()
}
}
}
// ✅ 大量のアイテムにはLazyVStack
ScrollView {
LazyVStack {
ForEach(0..<1000) { index in
RowView(index: index)
}
}
}
// ❌ 全て一度に描画される
ScrollView {
VStack {
ForEach(0..<1000) { index in
RowView(index: index)
}
}
}
class ViewModel: ObservableObject {
// ✅ 必要なプロパティのみPublished
@Published var displayText: String = ""
// ❌ 頻繁に変わる内部状態をPublishedにしない
private var internalCounter = 0
func updateDisplay() {
internalCounter += 1
// 10回に1回だけUIを更新
if internalCounter % 10 == 0 {
displayText = "Count: \(internalCounter)"
}
}
}
struct AnimatedView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.scaleEffect(scale)
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
}
}
extension AnyTransition {
static var slideAndFade: AnyTransition {
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
)
}
}
struct ContentView: View {
@State private var showDetail = false
var body: some View {
VStack {
if showDetail {
DetailView()
.transition(.slideAndFade)
}
}
.animation(.easeInOut, value: showDetail)
}
}
struct MatchedView: View {
@State private var isExpanded = false
@Namespace private var animation
var body: some View {
if isExpanded {
VStack {
Circle()
.fill(.blue)
.matchedGeometryEffect(id: "circle", in: animation)
.frame(width: 200, height: 200)
Text("Expanded")
}
} else {
Circle()
.fill(.blue)
.matchedGeometryEffect(id: "circle", in: animation)
.frame(width: 50, height: 50)
}
}
}
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.white)
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// 使用例
Text("Card Content")
.cardStyle()
struct Section<Content: View, Header: View>: View {
let header: Header
let content: Content
init(@ViewBuilder content: () -> Content, @ViewBuilder header: () -> Header) {
self.content = content()
self.header = header()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
header
.font(.headline)
content
}
.padding()
.cardStyle()
}
}
// 使用例
Section {
Text("Content here")
} header: {
Text("Title")
}
import XCTest
import ViewInspector
@testable import MyApp
final class CounterViewTests: XCTestCase {
func testInitialState() throws {
let view = CounterView()
let text = try view.inspect().find(text: "Count: 0")
XCTAssertNotNil(text)
}
func testIncrement() throws {
let view = CounterView()
try view.inspect().find(button: "Increment").tap()
let text = try view.inspect().find(text: "Count: 1")
XCTAssertNotNil(text)
}
}
import SnapshotTesting
import XCTest
final class SnapshotTests: XCTestCase {
func testUserCard() {
let view = UserCard(user: .mock)
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13)))
}
}
原因: 親Viewの状態変更
解決策:
// ❌ 問題のあるコード
struct ParentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("\(counter)")
ExpensiveChildView() // 毎回再作成される
}
}
}
// ✅ 改善したコード
struct ParentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("\(counter)")
ExpensiveChildView()
.equatable() // Equatableに準拠させる
}
}
}
解決策:
// ✅ LazyVStackとonAppear活用
ScrollView {
LazyVStack {
ForEach(items) { item in
RowView(item: item)
.onAppear {
if item == items.last {
loadMore()
}
}
}
}
}
解決策:
// ✅ pathを明示的に管理
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
// ...
}
.onDisappear {
// 必要に応じてクリーンアップ
path = NavigationPath()
}
}
}
関連Skills:
更新履歴:
tools
Fundamentals of modern web development. Framework selection (React, Vue, Next.js), project architecture, state management, routing, build tools, and CSS strategy best practices.
development
# React Development — Complete Guide > A comprehensive guide to building modern React applications with TypeScript. Covers fundamentals through advanced patterns, Hooks mastery, TypeScript integration, performance optimization, and algorithm internals. ## Target Audience - Developers new to React who want a solid foundation - Intermediate React developers looking to deepen their understanding of Hooks and TypeScript patterns - Engineers who want to understand React's internal algorithms (Virt
development
# Node.js Development Skill > A practical guide collection for Node.js development. Covers all aspects of Node.js application development, including Express, NestJS, asynchronous patterns, and performance optimization. ## Overview This skill covers the following topics: - **Express & NestJS**: When to use a lightweight framework vs. an enterprise framework - **Asynchronous Patterns**: Promise, async/await, Event Emitter, Streams, Worker Threads, Cluster - **Performance Optimization**: Memory
development
# Backend Development — Complete Guide > A comprehensive guide to backend engineering. Covers the fundamentals of HTTP, REST API design, databases, authentication, environment configuration, and algorithm proofs — everything needed to build robust server-side systems. ## Target Audience - Developers new to backend engineering - Frontend engineers expanding toward full-stack development - Engineers looking to solidify their understanding of server-side fundamentals ## Prerequisites - Basic p