Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
21 dk okuma
Yayın Tarihi
...
Kelime Sayısı
2.039kelime

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

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

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

# 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ı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

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

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz