Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
21 dk
Yayın Tarihi
2025-06-10
Kelime Sayısı
2.049kelime

Kahveni hazırla - bu içerikli bir makale!

SwiftUI Custom Component Library: Tasarım Sistemi Oluşturma

Özet

SwiftUI ile reusable component library oluşturma. Design tokens, tema sistemi, ViewModifier pattern ve Swift Package olarak dağıtım.

  • Design token'lar (DSSpacing, DSRadius, DSShadow) tasarım kararlarının tek kaynağı olur.
  • Light/Dark DSTheme, EnvironmentKey ile dsTheme olarak view hiyerarşisine yayılır.
  • DSButton, DSTextField, DSCard gibi component'lar tema ve token'lara bağlı olarak üretilir.
  • Package.swift ile AppDesignSystem SPM kütüphanesi olarak paketlenip dağıtılır.
SwiftUI Custom Component Library: Tasarım Sistemi Oluşturma

Tutarlı bir UI, profesyonel uygulamaların ayırt edici özelliğidir. Apple, Google, Airbnb — hepsi kendi tasarım sistemlerine sahip. Bu rehberde SwiftUI ile production-grade bir component library oluşturacağız: design tokens'dan tema sistemine, custom modifier'lardan SPM ile dağıtıma kadar.


İçindekiler


1. Design Tokens

Design token'lar, tasarım kararlarınızın tek kaynağıdır (single source of truth).

swift
1// Sources/DesignSystem/Tokens/DSSpacing.swift
2public enum DSSpacing {
3 /// 4pt
4 public static let xxs: CGFloat = 4
5 /// 8pt
6 public static let xs: CGFloat = 8
7 /// 12pt
8 public static let sm: CGFloat = 12
9 /// 16pt
10 public static let md: CGFloat = 16
11 /// 24pt
12 public static let lg: CGFloat = 24
13 /// 32pt
14 public static let xl: CGFloat = 32
15 /// 48pt
16 public static let xxl: CGFloat = 48
17 /// 64pt
18 public static let xxxl: CGFloat = 64
19}
20 
21public enum DSRadius {
22 public static let sm: CGFloat = 8
23 public static let md: CGFloat = 12
24 public static let lg: CGFloat = 16
25 public static let xl: CGFloat = 24
26 public static let full: CGFloat = 9999
27}
28 
29public enum DSShadow {
30 public struct Token {
31 let color: Color
32 let radius: CGFloat
33 let x: CGFloat
34 let y: CGFloat
35 }
36 
37 public static let sm = Token(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
38 public static let md = Token(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
39 public static let lg = Token(color: .black.opacity(0.15), radius: 16, x: 0, y: 8)
40 public static let xl = Token(color: .black.opacity(0.2), radius: 24, x: 0, y: 12)
41}

Token Karşılaştırma

Token Katmanı
Örnek
Açıklama
Primitive
blue-500: #3B82F6
Ham değerler
Semantic
primary: blue-500
Anlam taşıyan
Component
button.bg: primary
Bileşene özel

2. Tema Sistemi

swift
1// Sources/DesignSystem/Theme/DSTheme.swift
2public protocol DSTheme {
3 var colors: DSColorPalette { get }
4 var typography: DSTypography { get }
5 var spacing: DSSpacing.Type { get }
6}
7 
8public struct DSColorPalette {
9 public let primary: Color
10 public let primaryVariant: Color
11 public let secondary: Color
12 public let background: Color
13 public let surface: Color
14 public let error: Color
15 public let onPrimary: Color
16 public let onBackground: Color
17 public let onSurface: Color
18 public let border: Color
19 
20 public init(
21 primary: Color, primaryVariant: Color, secondary: Color,
22 background: Color, surface: Color, error: Color,
23 onPrimary: Color, onBackground: Color, onSurface: Color,
24 border: Color
25 ) {
26 self.primary = primary
27 self.primaryVariant = primaryVariant
28 self.secondary = secondary
29 self.background = background
30 self.surface = surface
31 self.error = error
32 self.onPrimary = onPrimary
33 self.onBackground = onBackground
34 self.onSurface = onSurface
35 self.border = border
36 }
37}
38 
39// Light tema
40public struct LightTheme: DSTheme {
41 public let colors = DSColorPalette(
42 primary: Color(hex: "3B82F6"),
43 primaryVariant: Color(hex: "2563EB"),
44 secondary: Color(hex: "8B5CF6"),
45 background: Color(hex: "FFFFFF"),
46 surface: Color(hex: "F8FAFC"),
47 error: Color(hex: "EF4444"),
48 onPrimary: .white,
49 onBackground: Color(hex: "0F172A"),
50 onSurface: Color(hex: "334155"),
51 border: Color(hex: "E2E8F0")
52 )
53 public let typography = DSTypography()
54 public let spacing = DSSpacing.self
55 
56 public init() {}
57}
58 
59// Dark tema
60public struct DarkTheme: DSTheme {
61 public let colors = DSColorPalette(
62 primary: Color(hex: "60A5FA"),
63 primaryVariant: Color(hex: "93C5FD"),
64 secondary: Color(hex: "A78BFA"),
65 background: Color(hex: "0F172A"),
66 surface: Color(hex: "1E293B"),
67 error: Color(hex: "F87171"),
68 onPrimary: Color(hex: "0F172A"),
69 onBackground: Color(hex: "F1F5F9"),
70 onSurface: Color(hex: "CBD5E1"),
71 border: Color(hex: "334155")
72 )
73 public let typography = DSTypography()
74 public let spacing = DSSpacing.self
75 
76 public init() {}
77}
78 
79// Environment key
80private struct ThemeKey: EnvironmentKey {
81 static let defaultValue: any DSTheme = LightTheme()
82}
83 
84extension EnvironmentValues {
85 public var dsTheme: any DSTheme {
86 get { self[ThemeKey.self] }
87 set { self[ThemeKey.self] = newValue }
88 }
89}

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?


3. Typography System

swift
1public struct DSTypography {
2 public enum Style {
3 case largeTitle // 34pt Bold
4 case title1 // 28pt Bold
5 case title2 // 22pt Bold
6 case title3 // 20pt Semibold
7 case headline // 17pt Semibold
8 case body // 17pt Regular
9 case callout // 16pt Regular
10 case subheadline // 15pt Regular
11 case footnote // 13pt Regular
12 case caption // 12pt Regular
13 
14 var font: Font {
15 switch self {
16 case .largeTitle: return .system(size: 34, weight: .bold, design: .rounded)
17 case .title1: return .system(size: 28, weight: .bold, design: .rounded)
18 case .title2: return .system(size: 22, weight: .bold)
19 case .title3: return .system(size: 20, weight: .semibold)
20 case .headline: return .system(size: 17, weight: .semibold)
21 case .body: return .system(size: 17, weight: .regular)
22 case .callout: return .system(size: 16, weight: .regular)
23 case .subheadline: return .system(size: 15, weight: .regular)
24 case .footnote: return .system(size: 13, weight: .regular)
25 case .caption: return .system(size: 12, weight: .regular)
26 }
27 }
28 }
29}
30 
31// ViewModifier olarak
32struct DSText: ViewModifier {
33 let style: DSTypography.Style
34 
35 func body(content: Content) -> some View {
36 content.font(style.font)
37 }
38}
39 
40extension View {
41 public func dsTypography(_ style: DSTypography.Style) -> some View {
42 modifier(DSText(style: style))
43 }
44}

4. Button Components

swift
1public struct DSButton: View {
2 public enum Style { case primary, secondary, outline, ghost, destructive }
3 public enum Size { case sm, md, lg }
4 
5 let title: String
6 let style: Style
7 let size: Size
8 let icon: String?
9 let isLoading: Bool
10 let action: () -> Void
11 
12 public init(
13 _ title: String, style: Style = .primary, size: Size = .md,
14 icon: String? = nil, isLoading: Bool = false, action: @escaping () -> Void
15 ) {
16 self.title = title; self.style = style; self.size = size
17 self.icon = icon; self.isLoading = isLoading; self.action = action
18 }
19 
20 @Environment(\.dsTheme) private var theme
21 
22 public var body: some View {
23 Button(action: action) {
24 HStack(spacing: DSSpacing.xs) {
25 if isLoading {
26 ProgressView()
27 .progressViewStyle(.circular)
28 .tint(foregroundColor)
29 } else {
30 if let icon {
31 Image(systemName: icon)
32 }
33 Text(title)
34 .font(fontSize)
35 .fontWeight(.semibold)
36 }
37 }
38 .frame(maxWidth: style == .ghost ? nil : .infinity)
39 .padding(.horizontal, horizontalPadding)
40 .padding(.vertical, verticalPadding)
41 .background(backgroundColor)
42 .foregroundStyle(foregroundColor)
43 .clipShape(RoundedRectangle(cornerRadius: DSRadius.md))
44 .overlay(
45 RoundedRectangle(cornerRadius: DSRadius.md)
46 .stroke(borderColor, lineWidth: style == .outline ? 1.5 : 0)
47 )
48 }
49 .disabled(isLoading)
50 .opacity(isLoading ? 0.8 : 1)
51 }
52 
53 private var backgroundColor: Color {
54 switch style {
55 case .primary: return theme.colors.primary
56 case .secondary: return theme.colors.secondary
57 case .outline, .ghost: return .clear
58 case .destructive: return theme.colors.error
59 }
60 }
61 
62 private var foregroundColor: Color {
63 switch style {
64 case .primary: return theme.colors.onPrimary
65 case .secondary: return .white
66 case .outline: return theme.colors.primary
67 case .ghost: return theme.colors.onSurface
68 case .destructive: return .white
69 }
70 }
71 
72 private var borderColor: Color {
73 style == .outline ? theme.colors.primary : .clear
74 }
75 
76 private var fontSize: Font {
77 switch size {
78 case .sm: return .system(size: 14, weight: .semibold)
79 case .md: return .system(size: 16, weight: .semibold)
80 case .lg: return .system(size: 18, weight: .semibold)
81 }
82 }
83 
84 private var horizontalPadding: CGFloat {
85 switch size { case .sm: return 12; case .md: return 16; case .lg: return 24 }
86 }
87 
88 private var verticalPadding: CGFloat {
89 switch size { case .sm: return 8; case .md: return 12; case .lg: return 16 }
90 }
91}

5. Input Components

swift
1public struct DSTextField: View {
2 let label: String
3 @Binding var text: String
4 let placeholder: String
5 let error: String?
6 let icon: String?
7 
8 @FocusState private var isFocused: Bool
9 @Environment(\.dsTheme) private var theme
10 
11 public init(
12 _ label: String, text: Binding<String>,
13 placeholder: String = "", error: String? = nil, icon: String? = nil
14 ) {
15 self.label = label; self._text = text
16 self.placeholder = placeholder; self.error = error; self.icon = icon
17 }
18 
19 public var body: some View {
20 VStack(alignment: .leading, spacing: DSSpacing.xs) {
21 Text(label)
22 .font(.system(size: 14, weight: .medium))
23 .foregroundStyle(theme.colors.onSurface)
24 
25 HStack(spacing: DSSpacing.xs) {
26 if let icon {
27 Image(systemName: icon)
28 .foregroundStyle(isFocused ? theme.colors.primary : theme.colors.onSurface.opacity(0.5))
29 }
30 TextField(placeholder, text: $text)
31 .focused($isFocused)
32 }
33 .padding(.horizontal, DSSpacing.md)
34 .padding(.vertical, DSSpacing.sm)
35 .background(theme.colors.surface)
36 .clipShape(RoundedRectangle(cornerRadius: DSRadius.md))
37 .overlay(
38 RoundedRectangle(cornerRadius: DSRadius.md)
39 .stroke(borderColor, lineWidth: isFocused ? 2 : 1)
40 )
41 
42 if let error {
43 Text(error)
44 .font(.system(size: 12))
45 .foregroundStyle(theme.colors.error)
46 }
47 }
48 }
49 
50 private var borderColor: Color {
51 if error != nil { return theme.colors.error }
52 if isFocused { return theme.colors.primary }
53 return theme.colors.border
54 }
55}

6. Card Components

swift
1public struct DSCard<Content: View>: View {
2 let content: Content
3 let padding: CGFloat
4 
5 @Environment(\.dsTheme) private var theme
6 
7 public init(padding: CGFloat = DSSpacing.md, @ViewBuilder content: () -> Content) {
8 self.padding = padding
9 self.content = content()
10 }
11 
12 public var body: some View {
13 content
14 .padding(padding)
15 .background(theme.colors.surface)
16 .clipShape(RoundedRectangle(cornerRadius: DSRadius.lg))
17 .shadow(
18 color: DSShadow.md.color,
19 radius: DSShadow.md.radius,
20 x: DSShadow.md.x,
21 y: DSShadow.md.y
22 )
23 }
24}

7. ViewModifier Patterns

swift
1// Shimmer loading effect
2public struct ShimmerModifier: ViewModifier {
3 @State private var phase: CGFloat = 0
4 let isActive: Bool
5 
6 public func body(content: Content) -> some View {
7 if isActive {
8 content
9 .redacted(reason: .placeholder)
10 .overlay(
11 GeometryReader { geo in
12 LinearGradient(
13 colors: [.clear, .white.opacity(0.4), .clear],
14 startPoint: .leading, endPoint: .trailing
15 )
16 .frame(width: geo.size.width * 0.6)
17 .offset(x: phase * geo.size.width * 1.6 - geo.size.width * 0.3)
18 }
19 .clipped()
20 )
21 .onAppear {
22 withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
23 phase = 1
24 }
25 }
26 } else {
27 content
28 }
29 }
30}
31 
32extension View {
33 public func shimmer(isActive: Bool = true) -> some View {
34 modifier(ShimmerModifier(isActive: isActive))
35 }
36}

8. Animasyon Sistemi

swift
1public enum DSAnimation {
2 public static let springSnappy = Animation.spring(response: 0.3, dampingFraction: 0.7)
3 public static let springSmooth = Animation.spring(response: 0.5, dampingFraction: 0.8)
4 public static let springBouncy = Animation.spring(response: 0.4, dampingFraction: 0.6)
5 public static let easeOut = Animation.easeOut(duration: 0.25)
6 public static let easeInOut = Animation.easeInOut(duration: 0.3)
7}
8 
9// Kullanım
10Button("Kaydet") { }
11 .scaleEffect(isPressed ? 0.95 : 1)
12 .animation(DSAnimation.springSnappy, value: isPressed)

9. SPM ile Dağıtım

swift
1// Package.swift
2// swift-tools-version: 5.9
3import PackageDescription
4 
5let package = Package(
6 name: "AppDesignSystem",
7 platforms: [.iOS(.v16), .macOS(.v13)],
8 products: [
9 .library(name: "AppDesignSystem", targets: ["AppDesignSystem"]),
10 ],
11 targets: [
12 .target(
13 name: "AppDesignSystem",
14 resources: [.process("Resources")]
15 ),
16 .testTarget(
17 name: "AppDesignSystemTests",
18 dependencies: ["AppDesignSystem"]
19 ),
20 ]
21)

10. Preview Catalog

swift
1#Preview("Buttons") {
2 VStack(spacing: 16) {
3 DSButton("Primary", style: .primary) { }
4 DSButton("Secondary", style: .secondary) { }
5 DSButton("Outline", style: .outline) { }
6 DSButton("Ghost", style: .ghost, icon: "plus") { }
7 DSButton("Loading", isLoading: true) { }
8 DSButton("Destructive", style: .destructive, icon: "trash") { }
9 }
10 .padding()
11 .environment(\.dsTheme, LightTheme())
12}

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

Etiketler

#SwiftUI#Design System#Components#ViewModifier#SPM#iOS
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik