├── .gitignore
├── Behavioral patterns
├── .gitkeep
├── Command
│ ├── Example
│ │ ├── Command.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── Command
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── cat.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── Image.png
│ │ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ │ ├── ImageEditCommand.swift
│ │ │ ├── ImageEditor.swift
│ │ │ ├── Info.plist
│ │ │ ├── SceneDelegate.swift
│ │ │ └── ViewController.swift
│ ├── README.md
│ └── Resources
│ │ ├── diagram1.png
│ │ ├── diagram2.png
│ │ ├── diagram3.png
│ │ ├── diagram4.png
│ │ ├── diagram5.png
│ │ ├── diagram6.png
│ │ ├── diagram7.png
│ │ └── simulator.gif
├── Iterator
│ ├── Example.playground
│ │ ├── Contents.swift
│ │ ├── contents.xcplayground
│ │ └── playground.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ ├── README.md
│ └── Resources
│ │ └── diagram.png
├── Memento
│ ├── Example
│ │ ├── Memento.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── Memento
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── CareTaker.swift
│ │ │ ├── ContentView.swift
│ │ │ ├── EmojiPickerView.swift
│ │ │ ├── Emojis.swift
│ │ │ ├── Memento.swift
│ │ │ ├── MementoApp.swift
│ │ │ └── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── README.md
│ └── Resources
│ │ ├── emoji_picker.png
│ │ ├── simulator.gif
│ │ └── structure.png
├── State
│ ├── Example
│ │ ├── State.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── State
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── ContentView.swift
│ │ │ ├── DeliveryState.swift
│ │ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ │ └── StateApp.swift
│ ├── README.md
│ └── Resources
│ │ ├── client_code.png
│ │ ├── diagram.png
│ │ └── simulator.gif
├── Strategy
│ └── README.md
└── Visitor
│ ├── Example
│ ├── Visitor.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── Visitor
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ │ ├── Info.plist
│ │ ├── MarkdownPlainTextViewController.swift
│ │ ├── SceneDelegate.swift
│ │ ├── TextEditorElement.swift
│ │ ├── ViewController.swift
│ │ └── Visitor.swift
│ ├── README.md
│ └── Resources
│ ├── TextEditorElement.png
│ ├── diagram.png
│ ├── diagram2.png
│ └── record.gif
├── Creational patterns
├── .gitkeep
├── AbstractFactory
│ ├── Example
│ │ ├── AbstractFactory.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── AbstractFactory
│ │ │ ├── AbstractFactoryApp.swift
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── apeach.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── apeach.png
│ │ │ └── con.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── con.png
│ │ │ ├── ContentView.swift
│ │ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ │ ├── ThemeFactory
│ │ │ ├── ApeachThemeFactory.swift
│ │ │ ├── ConThemeFactory.swift
│ │ │ ├── MuziThemeFactory.swift
│ │ │ └── ThemeFactory.swift
│ │ │ └── Utils
│ │ │ └── Color+Hex.swift
│ ├── README.md
│ └── Resources
│ │ ├── diagram.png
│ │ └── simulator_preview.gif
├── Builder
│ ├── Example
│ │ ├── Builder.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── Builder
│ │ │ ├── AlertBuilder.swift
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ │ ├── Info.plist
│ │ │ ├── SceneDelegate.swift
│ │ │ └── ViewController.swift
│ ├── README.md
│ └── Resources
│ │ ├── diagram.png
│ │ └── simulator_preview.gif
├── FactoryMethod
│ └── README.md
├── FactoryPattern
│ ├── Example
│ │ ├── FactoryPattern.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── swiftpm
│ │ │ │ └── Package.resolved
│ │ └── FactoryPattern
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ │ ├── Cells
│ │ │ ├── CellFactory.swift
│ │ │ ├── NavigationCell.swift
│ │ │ └── ToggleCell.swift
│ │ │ ├── Info.plist
│ │ │ ├── Items
│ │ │ ├── NavigationItem.swift
│ │ │ ├── SettingItem.swift
│ │ │ └── ToggleItem.swift
│ │ │ ├── SceneDelegate.swift
│ │ │ └── ViewController.swift
│ └── README.md
└── Singleton
│ └── README.md
├── README.md
└── Structural patterns
├── .gitkeep
├── Adapter
├── Example
│ ├── Adapter.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Adapter.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Adapter
│ │ ├── Adapters
│ │ │ ├── AppleMapAdapter.swift
│ │ │ ├── MapService.swift
│ │ │ └── NaverMapAdapter.swift
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ ├── Info.plist
│ │ ├── MapViewController.swift
│ │ └── SceneDelegate.swift
│ ├── Podfile
│ └── Podfile.lock
├── README.md
└── Resources
│ ├── diagram.png
│ └── preview.gif
├── Bridge
├── Example
│ ├── Bridge.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── Bridge
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── BridgeApp.swift
│ │ ├── ContentView.swift
│ │ ├── GlobalToast.swift
│ │ ├── PaymentMethod.swift
│ │ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
└── README.md
├── Composite
├── Example
│ ├── Composite.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── Composite
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── CompositeApp.swift
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ ├── SettingComponent.swift
│ │ ├── SettingItems
│ │ ├── Composite+Audio
│ │ │ ├── Composite+Audio.swift
│ │ │ ├── Composite+Notification
│ │ │ │ ├── Composite+Notification.swift
│ │ │ │ ├── Leaf+ScheduledSummary.swift
│ │ │ │ └── Leaf+ShowPreview.swift
│ │ │ └── Composite+Sounds
│ │ │ │ ├── Composite+Sounds.swift
│ │ │ │ └── Leaf+SilentMode.swift
│ │ ├── Composite+Network
│ │ │ ├── Composite+Network.swift
│ │ │ ├── Compsite+Wifi
│ │ │ │ ├── Composite+Wifi.swift
│ │ │ │ ├── Leaf+AskToJoinNetwork.swift
│ │ │ │ └── Leaf+Hotspot.swift
│ │ │ └── Leaf+AirplaneMode.swift
│ │ └── Compsite+Setting.swift
│ │ └── SettingsView.swift
├── README.md
└── Resources
│ ├── composite pattern structure.png
│ └── preview.gif
├── Facade
├── README.md
└── Resources
│ └── facade_example_image.png
└── Proxy
├── CachingProxy
└── README.md
├── ProtectionProxy
├── Example
│ ├── ProtectionProxy.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── ProtectionProxy
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── ContentView.swift
│ │ ├── Document.swift
│ │ ├── PDFKitView.swift
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ ├── ProtectionProxyApp.swift
│ │ ├── UserMode.swift
│ │ └── sample.pdf
└── README.md
├── README.md
└── VirtualProxy
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,cocoapods
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,cocoapods
3 |
4 | ### CocoaPods ###
5 | ## CocoaPods GitIgnore Template
6 |
7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing
8 | # - Also handy if you have a large number of dependant pods
9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
10 | Pods/
11 |
12 | ### Xcode ###
13 | ## User settings
14 | xcuserdata/
15 |
16 | ## Xcode 8 and earlier
17 | *.xcscmblueprint
18 | *.xccheckout
19 |
20 | ### Xcode Patch ###
21 | *.xcodeproj/*
22 | !*.xcodeproj/project.pbxproj
23 | !*.xcodeproj/xcshareddata/
24 | !*.xcodeproj/project.xcworkspace/
25 | !*.xcworkspace/contents.xcworkspacedata
26 | /*.gcno
27 | **/xcshareddata/WorkspaceSettings.xcsettings
28 |
29 | # End of https://www.toptal.com/developers/gitignore/api/xcode,cocoapods
30 |
--------------------------------------------------------------------------------
/Behavioral patterns/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/.gitkeep
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Command
4 | //
5 | // Created by 이승기 on 3/20/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Assets.xcassets/cat.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Image.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Assets.xcassets/cat.imageset/Image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Example/Command/Assets.xcassets/cat.imageset/Image.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/ImageEditCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageEditCommand.swift
3 | // Command
4 | //
5 | // Created by 이승기 on 3/20/24.
6 | //
7 |
8 | import UIKit
9 | import CoreImage
10 |
11 | protocol ImageEditCommand {
12 | func execute(with originalImage: UIImage?)
13 | func undo()
14 | }
15 |
16 |
17 | // MARK: - 이미지에 corner radius 조절하는 콘크리트 커맨드
18 |
19 | class ApplyCornerRadiusCommand: ImageEditCommand {
20 | private weak var imageView: UIImageView?
21 | private let cornerRadius: CGFloat
22 | private var previousCornerRadius: CGFloat = 0
23 |
24 | init(imageView: UIImageView, cornerRadius: CGFloat) {
25 | self.imageView = imageView
26 | self.cornerRadius = cornerRadius
27 | self.previousCornerRadius = imageView.layer.cornerRadius // 이전 상태 저장
28 | }
29 |
30 | func execute(with originalImage: UIImage?) {
31 | guard let imageView = imageView else { return }
32 | previousCornerRadius = imageView.layer.cornerRadius // 실행 전 상태 저장
33 | DispatchQueue.main.async {
34 | imageView.layer.cornerRadius = self.cornerRadius
35 | imageView.clipsToBounds = true
36 | }
37 | }
38 |
39 | func undo() {
40 | guard let imageView = imageView else { return }
41 | DispatchQueue.main.async {
42 | imageView.layer.cornerRadius = self.previousCornerRadius
43 | imageView.clipsToBounds = self.previousCornerRadius > 0
44 | }
45 | }
46 | }
47 |
48 |
49 | // MARK: - 이미지 밝기 조절하는 콘크리트 커맨드
50 |
51 | class AdjustBrightnessCommand: ImageEditCommand {
52 | private let imageView: UIImageView
53 | private let brightness: Float // -1.0에서 1.0 사이의 범위
54 | private var previousImage: UIImage?
55 |
56 | init(imageView: UIImageView, brightness: Float) {
57 | self.imageView = imageView
58 | self.brightness = brightness
59 | }
60 |
61 | func execute(with originalImage: UIImage?) {
62 | self.previousImage = imageView.image // 실행 전 이미지 상태를 저장합니다.
63 | guard let originalImage = originalImage, let cgimg = originalImage.cgImage else { return }
64 | let ciImage = CIImage(cgImage: cgimg)
65 | let filter = CIFilter(name: "CIColorControls")
66 | filter?.setValue(ciImage, forKey: kCIInputImageKey)
67 | filter?.setValue(brightness, forKey: kCIInputBrightnessKey)
68 |
69 | let context = CIContext(options: nil)
70 | if let output = filter?.outputImage, let cgimgresult = context.createCGImage(output, from: output.extent) {
71 | let processedImage = UIImage(cgImage: cgimgresult)
72 | DispatchQueue.main.async {
73 | self.imageView.image = processedImage // 처리된 이미지를 화면에 표시합니다.
74 | }
75 | }
76 | }
77 |
78 | func undo() {
79 | DispatchQueue.main.async {
80 | self.imageView.image = self.previousImage // 이전 이미지 상태로 되돌립니다.
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/ImageEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageEditor.swift
3 | // Command
4 | //
5 | // Created by 이승기 on 3/20/24.
6 | //
7 |
8 | import UIKit
9 | import CoreImage
10 |
11 |
12 | // MARK: - Invoker
13 |
14 | class ImageEditor {
15 | private var imageView: UIImageView
16 | var originalImage: UIImage?
17 | private var commandHistory: [ImageEditCommand] = []
18 | private var previousImage: UIImage?
19 |
20 | init(imageView: UIImageView) {
21 | self.imageView = imageView
22 | self.originalImage = imageView.image
23 | }
24 |
25 | func execute(command: ImageEditCommand) {
26 | previousImage = imageView.image
27 | command.execute(with: originalImage)
28 | commandHistory.append(command)
29 | }
30 |
31 | func undo() {
32 | guard !commandHistory.isEmpty else { return }
33 | let lastCommand = commandHistory.removeLast()
34 | lastCommand.undo()
35 | if let previous = previousImage {
36 | imageView.image = previous
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Command
4 | //
5 | // Created by 이승기 on 3/20/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Example/Command/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Command
4 | //
5 | // Created by 이승기 on 3/20/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class ViewController: UIViewController {
11 |
12 | // MARK: - Properties
13 |
14 | @IBOutlet weak var contentView: UIView!
15 | @IBOutlet weak var catImageView: UIImageView!
16 | @IBOutlet weak var brightnessSlider: UISlider!
17 |
18 | var imageEditor: ImageEditor!
19 | var originalImage = UIImage(named: "cat")
20 |
21 |
22 | // MARK: - LifeCycle
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | self.imageEditor = ImageEditor(imageView: catImageView)
28 | contentView.layer.cornerRadius = 20
29 | catImageView.layer.shadowColor = UIColor.black.cgColor
30 | catImageView.layer.shadowOpacity = 0.2
31 | catImageView.layer.shadowOffset = .zero
32 | catImageView.layer.shadowRadius = 12
33 | }
34 |
35 |
36 | // MARK: - Actions
37 |
38 | @IBAction func didTapUndoButton(_ sender: Any) {
39 | imageEditor.undo()
40 | }
41 |
42 | @IBAction func didChangeCornerRadiusValue(_ sender: UISlider) {
43 | let command = ApplyCornerRadiusCommand(imageView: catImageView, cornerRadius: CGFloat(sender.value))
44 | imageEditor.execute(command: command)
45 | }
46 |
47 | @IBAction func didChangeBrightnessValue(_ sender: UISlider) {
48 | let command = AdjustBrightnessCommand(imageView: catImageView, brightness: sender.value)
49 | imageEditor.execute(command: command)
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/README.md:
--------------------------------------------------------------------------------
1 | # Command pattern
2 |
3 | 커맨드 패턴은 어떠한 요청에 대한 로직을 캡슐화 하는 것을 의미합니다.
4 |
5 | 뭔소리지? 싶으실겁니다. (저도 마찬가지입니다)
6 |
7 | 자 예를들어볼게요. 우리 보통 버튼을 만들면 그 버튼을 눌렀을 때 실행되는 로직이 버튼이랑 밀접하게 연관되어있죠?
8 |
9 | 아래 그림처럼 말이죠!
10 |
11 | 
12 |
13 | 근데 만약 나의 아이폰을 흔들어재꼈을 때에도 같은 동작을 수행하고 싶다고 해볼게요.
14 |
15 | 그럼 버튼을 클릭했을 때의 실행되는 로직과 똑같은 로직을 또 구현해줘야할 겁니다.
16 |
17 | 이런게 몇 개 안된다면 그냥 복붙해서 사용해도 괜찮겠지만 많은 버튼과 중복되는 액션들이 존재한다면 유지보수가 쉽지 않을겁니다.
18 |
19 | 
20 |
21 |
22 |
23 | 그래서 이러한 문제를 해결할 수 있는 패턴으로 커맨드 패턴이 존재합니다!
24 |
25 | 위에서 말했듯 어떠한 로직을 캡슐화 시킨다 그랬었죠?
26 |
27 | 따라서 어떠한 요청에 대해서 직접 로직을 실행하는게 아니라! 커맨드를 통해서 실행하는 것 입니다!
28 |
29 | 
30 |
31 | 커맨드 패턴의 기저는 이렇고 구현은 보통 단일 커맨드 인터페이스를 생성하고 콘크리트 커맨드들(커맨드 인터페이스를 채택하는 객체들)을 만들어서 로직을 실행시킵니다.
32 |
33 | 
34 |
35 |
36 |
37 | ## 독립된 객체에서 로직을 어떻게 실행 시킨다는거임?
38 |
39 | 여기에서 의문점이 생기신 분들도 계실겁니다.
40 |
41 | 잠만.. 로직을 캡슐화 한다고 했지.. 그럼 Action 저게 어떠한 로직을 의미하는거고.. 그렇다면 만약 로직이 ```UIImageView.image = UIImage(...)``` 와 같이UI 요소를 업데이트 하는 로직이라면 캡슐화 되어있어서 어떻게 UIImageView 같은거에 접근 못하는거 아님??
42 |
43 | 
44 |
45 | 네! 맞습니다.
46 |
47 | 캡슐화 된 로직이 완전히 독립적일 수는 없습니다.
48 |
49 | 그렇기에 아래 그림처럼 커맨드 패턴에서는 외부로부터 의존성을 주입받는 경우가 흔합니다.
50 |
51 | 여기에서 UIImageView처럼 주입받는 녀석을 보통 "리시버" 라고 부릅니다.
52 |
53 | 
54 |
55 |
56 |
57 | ## 구조
58 |
59 | 커맨드 패턴의 전통적인 구조를 보면서 앞서 설명한 것들을 조금 정리하는 시간을 가져보도록 하죠.
60 |
61 | 
62 |
63 | 전반적인 구조는 위와 같습니다.
64 |
65 | 초반에 얘기했던 Command 추상화 객체,
66 |
67 | 그리고 그걸 채택하여 구현하는 Concrete Command들,
68 |
69 | UIImageView와 같이 로직에 포함되는 리시버 등등 보이네요.
70 |
71 |
72 |
73 | ### 근데 Invoker는 뭘까요?
74 |
75 | Invoker는 말 그대로 호출자 역할을 하는 녀석 입니다.
76 |
77 | Invoker는 커맨드를 생성하는 책임을 가지진 않고 Command를 주입 받아서 대신 실행 시켜주는 녀석인 것이죠.
78 |
79 | 얘가 필요한 이유는 커맨드에 대한 객체의 참조를 유지시켜주기 위함 입니다.
80 |
81 | 만약 Invoker 없이 사용한다면 ConcreteCommandA() 이렇게 커맨드를 생성한 후에 execute()을 바로 호출할 수 있겠죠?
82 |
83 | ```Swift
84 | func action() {
85 | let command = ConcreteCommandA()
86 | command.execute()
87 | }
88 | ```
89 |
90 | 이렇게 된다면 action() 함수가 종료됨과 동시에 command 객체도 함께 메모리에서 해제되어 다른 곳에서 해당 커맨드를 참조하거나 재사용할 수 없게 되는 상황을 의미합니다.
91 |
92 | 따라서 이를 방지하기 위해서 Invoker가 존재하는 것이죠.
93 |
94 | 이런 기능 뿐만 아니라 Invoker는 클라이언트로부터 호출받은 excute(command: Command)를 실행할 때마다 주입받은 command를 배열로 저장해서 각 커맨드를 undo() 시킬 수도 있습니다.
95 |
96 | (다만 이렇게 하려면 Command protocol에 execute()뿐만 아니라 undo() 함수도 정의되어 있어야함)
97 |
98 |
99 |
100 | ## IRL (In Real Life)
101 |
102 | 이정도면 커멘드 패턴이 대략 어떻게 작동하는지에 대해서는 감 잡으셨을거라 생각합니다.
103 |
104 | 그럼 실제로 커맨드 패턴을 이용해서 프로젝트를 만들어 볼까요?
105 |
106 |
107 |
108 | 저는 간단하게 이미지를 수정할 수 있는 로직을 커맨드 패턴으로 캡슐화 한 후 Invokder도 구현해서 undo 기능까지 구현해 보겠습니다.
109 |
110 | 우선 Command protocol을 만들어 주겠습니다.
111 |
112 | execute() 뿐만 아니라 되돌리기 기능을 위해서 undo() 함수도 정의해 주겠습니다.
113 |
114 | ```Swift
115 | protocol ImageEditCommand {
116 | func execute(with originalImage: UIImage?)
117 | func undo()
118 | }
119 | ```
120 |
121 |
122 | 자 다음으로 ConcreteCommand들 구현해 주겠습니다.
123 |
124 | (자세한 구현은 예시 프로젝트 파일에서 확인 부탁드릴게요🙏)
125 |
126 | ```Swift
127 | class ApplyCornerRadiusCommand: ImageEditCommand {
128 | private weak var imageView: UIImageView? // <- 얘가 이제 Receiver
129 | private let cornerRadius: CGFloat
130 | private var previousCornerRadius: CGFloat = 0
131 |
132 | init(imageView: UIImageView, cornerRadius: CGFloat) {
133 | self.imageView = imageView
134 | self.cornerRadius = cornerRadius
135 | self.previousCornerRadius = imageView.layer.cornerRadius // 이전 상태 저장
136 | }
137 |
138 | func execute(with originalImage: UIImage?) {
139 | // 이미지 corner radius 수정해주는 로직
140 | }
141 |
142 | func undo() {
143 | // 뒤로가기 로직
144 | }
145 | }
146 |
147 | class AdjustBrightnessCommand: ImageEditCommand {
148 | private let imageView: UIImageView // <- Receiver
149 | private let brightness: Float // -1.0에서 1.0 사이의 범위
150 | private var previousImage: UIImage?
151 |
152 | init(imageView: UIImageView, brightness: Float) {
153 | self.imageView = imageView
154 | self.brightness = brightness
155 | }
156 |
157 | func execute(with originalImage: UIImage?) {
158 | // 이미지 corner radius 수정해주는 로직
159 | }
160 |
161 | func undo() {
162 | // 뒤로가기 로직
163 | }
164 | }
165 | ```
166 |
167 |
168 |
169 | 마지막으로 Invoker를 구현해 주겠습니다.
170 |
171 | ```Swift
172 | class ImageEditor {
173 | private var imageView: UIImageView
174 | var originalImage: UIImage?
175 | private var commandHistory: [ImageEditCommand] = []
176 | private var previousImage: UIImage?
177 |
178 | init(imageView: UIImageView) {
179 | self.imageView = imageView
180 | self.originalImage = imageView.image
181 | }
182 |
183 | func execute(command: ImageEditCommand) {
184 | previousImage = imageView.image
185 | command.execute(with: originalImage)
186 | commandHistory.append(command)
187 | }
188 |
189 | func undo() {
190 | guard !commandHistory.isEmpty else { return }
191 | let lastCommand = commandHistory.removeLast()
192 | lastCommand.undo()
193 | if let previous = previousImage {
194 | imageView.image = previous
195 | }
196 | }
197 | }
198 | ```
199 |
200 | 이 ImageEditor라는 Invoker를 구현해줬는데요, 앞서 설명한대로 execute()를 대신 실행해줄 때마다 commandHistory 배열에 저장해주면서 참조를 유지해주고 있습니다.
201 |
202 | 이렇게 하면 커맨드 배열 history를 가지고 undo()를 이용해서 뒤로가기 기능도 구현 가능하겠죠?
203 |
204 |
205 |
206 | 이제 클라이언트 쪽에서는 아래와같이 사용해주면 됩니다.
207 |
208 | 보시면 커맨드를 생성해주는건 클라이언트의 몫이고, 실행 자체는 ImageEditor(Invoker) 를 통해서 대신 호출하는 것을 보실 수 있습니다.
209 |
210 | ```Swift
211 | class ViewController: UIViewController {
212 | var imageEditor: ImageEditor!
213 | var originalImage = UIImage(named: "cat")
214 |
215 | override func viewDidLoad() {
216 | override func viewDidLoad()
217 | self.imageEditor = ImageEditor(imageView: catImageView)
218 | }
219 |
220 | @IBAction func didTapUndoButton(_ sender: Any) {
221 | imageEditor.undo()
222 | }
223 |
224 | @IBAction func didChangeCornerRadiusValue(_ sender: UISlider) {
225 | let command = ApplyCornerRadiusCommand(imageView: catImageView, cornerRadius: CGFloat(sender.value))
226 | imageEditor.execute(command: command)
227 | }
228 |
229 | @IBAction func didChangeBrightnessValue(_ sender: UISlider) {
230 | let command = AdjustBrightnessCommand(imageView: catImageView, brightness: sender.value)
231 | imageEditor.execute(command: command)
232 | }
233 | }
234 | ```
235 |
236 | ### 실행 화면
237 |
238 | 
239 |
240 | ## 마치며
241 |
242 | 어떤가요? 실제로 구현해보시면 생각보다 간단하단걸 아실 수 있을겁니다!
243 |
244 | 다만 다양한 상황에서 리시버를 수정하는 로직을 완전히 캡슐화 하기란 조금 어려울 수도 있겠다고 느꼈는데요
245 |
246 | 이런 부분들은 다른 패턴들을 같이 조합하면 더 유연한 구조로 만들 수 있다고 하니까 지금 당장 만들러 가보세요~! 🏃♂️
247 |
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram1.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram2.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram3.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram4.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram5.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram6.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/diagram7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/diagram7.png
--------------------------------------------------------------------------------
/Behavioral patterns/Command/Resources/simulator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Command/Resources/simulator.gif
--------------------------------------------------------------------------------
/Behavioral patterns/Iterator/Example.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct MigrationIterator: IteratorProtocol, Sequence {
4 |
5 | let steps: [MigrationStep]
6 | var currentIndex = 0
7 |
8 | mutating
9 | func next() -> MigrationStep? {
10 | if hasNext() {
11 | let step = steps[currentIndex]
12 | currentIndex += 1
13 | return step
14 | } else {
15 | return nil
16 | }
17 | }
18 |
19 | func hasNext() -> Bool {
20 | return currentIndex < steps.count
21 | }
22 | }
23 |
24 | struct MigrationCollection: Sequence {
25 | private var steps: [MigrationStep]
26 | private let currentVersion: Int
27 | private let targetVersion: Int
28 |
29 | init(steps: [MigrationStep], currentVersion: Int, targetVersion: Int) {
30 | self.steps = steps
31 | self.currentVersion = currentVersion
32 | self.targetVersion = targetVersion
33 | }
34 |
35 | func makeIterator() -> MigrationIterator {
36 | let filteredSteps = steps.filter { $0.migrationVersion > currentVersion && $0.migrationVersion <= targetVersion }
37 | return MigrationIterator(steps: filteredSteps)
38 | }
39 | }
40 |
41 | protocol MigrationStep {
42 | var migrationVersion: Int { get }
43 | func migrate()
44 | }
45 |
46 | struct V1_to_V2: MigrationStep {
47 | let migrationVersion: Int = 1
48 | func migrate() {
49 | print("버전 1 에서 버전2 로 마이그레이션 완료.")
50 | }
51 | }
52 |
53 | struct V2_to_V3: MigrationStep {
54 | let migrationVersion: Int = 2
55 | func migrate() {
56 | print("버전 2 에서 버전3 로 마이그레이션 완료.")
57 | }
58 | }
59 |
60 | struct V3_to_V4: MigrationStep {
61 | let migrationVersion: Int = 3
62 | func migrate() {
63 | print("버전 3 에서 버전4 로 마이그레이션 완료.")
64 | }
65 | }
66 |
67 | struct V4_to_V5: MigrationStep {
68 | let migrationVersion: Int = 4
69 | func migrate() {
70 | print("버전 4 에서 버전5 로 마이그레이션 완료.")
71 | }
72 | }
73 |
74 | let collection = MigrationCollection(steps: [
75 | V1_to_V2(),
76 | V2_to_V3(),
77 | V3_to_V4(),
78 | V4_to_V5()
79 | ], currentVersion: 2, targetVersion: 4)
80 |
81 | var iterator = collection.makeIterator()
82 | for next in iterator {
83 | next.migrate()
84 | }
85 |
--------------------------------------------------------------------------------
/Behavioral patterns/Iterator/Example.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Behavioral patterns/Iterator/Example.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Behavioral patterns/Iterator/README.md:
--------------------------------------------------------------------------------
1 | # Iterator Pattern
2 |
3 | Iterator 패턴 즉 반복자 패턴의 정의부터 보고 가겠습니다.
4 |
5 | > 반복자 패턴은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공합니다.
6 |
7 | by 헤드퍼스트 디자인 패턴
8 |
9 | 자 이게 뭔 소리일까요?
10 |
11 | 우리가 평소에 Collection의 타입이 **Dictionary**이든 **Array**이든 상관없이 ```for in``` 구문이나 ```forEach```를 사용해왔었죠?
12 |
13 | 이렇게 타입에 상관 없이 반복문을 사용할 수 있었던 것이 Iterator 패턴을 사용하고 있었기 때문 이라고 얘기할 수 있는 것이죠.
14 |
15 | 그럼 정의와 함께 비교해보자면
16 | > 반복자 패턴은 컬렉션의 구현 방법을 노출하지 않으면서...
17 |
18 | 이건 컬렉션의 타입이 Dictionary이든 Array이든 컬렉션 타입 상관 없이 라고 해석할 수 있겠군요
19 |
20 | 왜냐하면 ```forEach```나 ```for in```구문을 사용하지 않는다면 어떻게 접근할까요?
21 |
22 | 접근하는 방법에 대해서 알고있어야 할 겁니다. Dictionary가 [Int: String] 이런 타입으로 구현 되어있었다면 key 값의 범위를 알고 어떻게 접근해야하는지에 대해서 알고있어야 한다거나를 얘기하는 것이죠.
23 |
24 | 하지만 ```forEach```나 ```for in```을 이용하면 이게 어떻게 구현되어있어서 어떻게 각각 요소에 접근해야하는지 알 필요 있었나요? 없었죠! 바로 이걸 의미하는 겁니다.
25 |
26 | 이렇게 컬렉션이 어떻게 구현 되어있는지 몰라도! 모든 컬렉션 요소에 접근할 수 있도록 도와주는 것이 바로! Iterator(반복자) 패턴이다~ 이렇게 애해해 주시면 되겠습니다.
27 |
28 |
29 |
30 | ## 전통적인 구현 방식
31 |
32 | 우선 Iterator 패턴의 구조는 아래와같습니다.
33 |
34 | 
35 |
36 | ConcreteCollection을 구현한 후 내부에서 makeIterator를 호출해서
37 |
38 | 아래와 같이 while문 등을 이용해서 사용하는 것이죠.
39 |
40 | ```Swift
41 | let collection = ConcreateCollection(items: [...])
42 | let iterator = collection.makeIterator()
43 |
44 | while let item = iterator.next() {
45 | item.doSeomthing()
46 | }
47 | ```
48 |
49 | 이렇게 하면 IterableCollection을 채택한 또 다른 ConcreteCollection만 구현한다면 while 문 쪽은 변경하지 않고 iterator만 교체해주면서 사용할 수 있겠죠?
50 |
51 | Iterator가 없었다면 Collection 타입에 따라서 while문 쪽(순회하는 로직)이 자주 변경이 될 텐데 Iterator를 이용하면서 Collection을 감추고 while문 쪽을 변경하지 않으면서 확장할 수 있게 된거죠.
52 |
53 | 이를 Iterator패턴을 이용해서 자주 변경되는 순회 로직을 캡슐화 시켰다~ 라고 말할 수 있는 겁니다.
54 |
55 | 어떻게 보면 Iterator가 통일된 방식으로 컬렉션의 요소를 한번씩 대신 순회 해주는 셔틀? 느낌으로 봐도 상관없을 듯 합니다 🫠
56 |
57 |
58 |
59 |
60 | 자! 그럼 대충 구조는 알았으니 Iterator와 IterableCollection을 직접 구현해 봅시다.
61 |
62 | 그냥 구현하면 재미없으니 순차적으로 DBMigration 하는 로직을 Iterator를 이용해서 구현해볼게요.
63 |
64 |
65 |
66 | ### Iterator 구현
67 |
68 | Iterator를 구현해주기 전에 Iterator에서 collection Item으로 사용할 MigrationStep들을 문저 구현해 주겠습니다.
69 |
70 | ```Swift
71 | protocol MigrationStep {
72 | func migrate()
73 | }
74 |
75 | struct V1_to_V2: MigrationStep {
76 | func migrate() {
77 | print("버전 1 에서 버전2 로 마이그레이션 완료")
78 | }
79 | }
80 |
81 | struct V2_to_V3: MigrationStep {
82 | func migrate() {
83 | print("버전 2 에서 버전3 로 마이그레이션 완료")
84 | }
85 | }
86 | ```
87 |
88 | 이어서 위에서 봤던 그림과 같이 Iterator 프로토콜을 만들고 채택해 주겠습니다.
89 |
90 | 사실 Iterator를 직접 구현하지 않아도 되는게 Foundation에 이미 포함되어있는 프로토콜 입니다.
91 |
92 | 이름은 IteratorProtocol 이죠.
93 |
94 | 이걸 채택하면 next() 를 구현해야할 겁니다.
95 |
96 | 그런데 haxNext()는 없는데요, 개인적인 추측이지만 Swift에는 optional이 있기 때문에 이를 이용하면 굳이 hasNext()를 구현해주지 않고도 옵셔널을 언래핑 해서 값이 없으면 nil을 리턴해 주는 방식으로 자연스럽게 사용할 수 있기 때문에 hasNext() 함수가 Iterator Protocol에 포함이 되어있지 않은게 아닌가 싶네요!
97 |
98 | 일단 저는 구현해 주겠습니다.
99 |
100 | ```Swift
101 | struct MigrationIterator: IteratorProtocol {
102 |
103 | let steps: [MigrationStep]
104 | var currentIndex = 0
105 |
106 | mutating
107 | func next() -> MigrationStep? {
108 | if hasNext() {
109 | let step = steps[currentIndex]
110 | currentIndex += 1
111 | return step
112 | } else {
113 | return nil
114 | }
115 | }
116 |
117 | func hasNext() -> Bool {
118 | return currentIndex < steps.count
119 | }
120 | }
121 | ```
122 |
123 | 자 이렇게 구현하면 Iterator를 가져와서 반복문 돌리는 곳에서는 next()를 계속 호출해서 옵셔널이 아닐 때까지 요소를 하나씩 가져와서 사용할 수 있겠죠?
124 |
125 |
126 |
127 | ### Collection 구현
128 |
129 | 마지막으로 Collection쪽 구현해 보겠습니다.
130 |
131 | 여기에서도 IterableCollection을 프로토콜을 직접 작성할 수 있겠지만 똑같은 역할 하는 Seqeunce라는 프로토콜이 있는데 얘를 그냥 채택하자구요. 그러면 makeIterator() 라는 함수를 꼭 구현해줘야할겁니다. 아래처럼요!
132 |
133 | ```Swift
134 | struct MigrationCollection {
135 | private var steps: [MigrationStep]
136 |
137 | init(steps: [MigrationStep]) {
138 | self.steps = steps
139 | }
140 |
141 | func makeIterator() -> MigrationIterator {
142 | return MigrationIterator(steps: steps)
143 | }
144 | }
145 | ```
146 |
147 | 자 모든 준비가 끝났으니 사용해 볼까요??
148 |
149 | ```Swift
150 | let collection = MigrationCollection(steps: [
151 | V1_to_V2(),
152 | V2_to_V3()
153 | ])
154 |
155 | var iterator = collection.makeIterator()
156 | for next in iterator {
157 | next.migrate()
158 | }
159 |
160 | // 실행 결과
161 | // 버전 1 에서 버전2 로 마이그레이션 완료
162 | // 버전 2 에서 버전3 로 마이그레이션 완료
163 | ```
164 |
165 | 짜잔~~
166 |
167 | 아까 IterableCollection 프로토콜 대신 Sequence(같은 역할하는데 이미 만들어진 거)를 채택해줬기 때문에 아래와같이 사용도 가능합니다.
168 |
169 | ```Swift
170 | iterator.forEach { item in ... }
171 |
172 | //또는
173 |
174 | for item in iterator { ... }
175 | ```
176 |
177 |
178 |
179 | ## IRL (In Real Life)
180 |
181 | 사실 위에서 구현한 것은 아래와같이 사용한 것과 별반 다른게 없긴 합니다.
182 |
183 | 왜냐하면 MigrationCollection도 어차피 item을 배열로 가지고 있고 이 배열도 어차피 Iterator 패턴이 적용된 Collection이기 때문에 Iterator 패턴을 적용한 Collection을 한 번 더 감싼 그런 느낌?
184 |
185 | ```Swift
186 | let items = [V1_to_V2(), V2_to_V3()]
187 | items.forEach { ... }
188 | ```
189 |
190 | 어쨌든 Iterator 패턴이 서로 다른 타입의 컬렉션의 순회를 캡슐화 해서 확장을 도와주는 것을 기저로 하고있는데 뭐 이미 Foundation 수준에서 구현도 되어있고.. 저는 Iterator 패턴을 좀 다른 방식으로 응용해 보겠습니다.
191 |
192 | 일반적으로 생각해 봤을때 Migration은 버전 ```1 -> 2```, ```2 -> 3``` ... 이런 식으로 하게 됩니다.
193 |
194 | 근데 만약 DB 버전 ```1``` 사용자들이 중간에 ```1 -> 2``` 마이그레이션이 포함된 업데이트를 다운받지 않은 상태에서 ```2 -> 3``` 마이그레이션을 해버릴 경우 참사가 일어나겠죠?
195 |
196 | 따라서 현재 버전이 ```1```인 사용자들에게 ```1 -> 2``` 마이그레이션을 꼭 거친 후에 ```2 -> 3``` 마이그레이션을 하도록 해야합니다.
197 |
198 | 자 이련 경우 Iterator 패턴을 유용하게 사용할 수 있습니다!!
199 |
200 | 그럼 바로 구현해 봅시다!
201 |
202 | 우선 Migration Step에 migrationVersion 프로퍼티를 추가해 주도록 하겠습니다.
203 |
204 | ```Swift
205 | protocol MigrationStep {
206 | var migrationVersion: Int { get }
207 | func migrate()
208 | }
209 | ```
210 |
211 | 핵심은 다음인데 바로 아래처럼 MigrationCollection에 현재 버전과 타켓 버전을 초기화 인자로 받고 makeIterator() 할 때 마이그레이션에 필요한 MigrationStep만 필터링 해 주는 것이죠.
212 | (필요시 version에 따라서 정렬도 해줘야함)
213 |
214 | ```Swift
215 | struct MigrationCollection: Sequence {
216 | private var steps: [MigrationStep]
217 | private let currentVersion: Int
218 | private let targetVersion: Int
219 |
220 | init(steps: [MigrationStep], currentVersion: Int, targetVersion: Int) {
221 | self.steps = steps
222 | self.currentVersion = currentVersion
223 | self.targetVersion = targetVersion
224 | }
225 |
226 | func makeIterator() -> MigrationIterator {
227 | let filteredSteps = steps.filter { $0.migrationVersion > currentVersion && $0.migrationVersion <= targetVersion }
228 | return MigrationIterator(steps: filteredSteps)
229 | }
230 | }
231 | ```
232 |
233 | 그리고 아래와같이 사용해 보면! 시작 버전과 끝 버전에 맞게 마이그레이션 된 모습 보이시죠?
234 |
235 | 이렇게 써먹을 수도 있겠습니다.
236 |
237 | ```Swift
238 | let collection = MigrationCollection(steps: [
239 | V1_to_V2(),
240 | V2_to_V3(),
241 | V3_to_V4(),
242 | V4_to_V5()
243 | ], currentVersion: 2, targetVersion: 4)
244 |
245 | var iterator = collection.makeIterator()
246 | for next in iterator {
247 | next.migrate()
248 | }
249 |
250 | // 실행 결과
251 | // 버전 3 에서 버전4 로 마이그레이션 완료.
252 | // 버전 4 에서 버전5 로 마이그레이션 완료.
253 | ```
254 |
255 | steps에 모든 마이그레이션 객체를 초기화 해서 배열로 넣는 것이 메모리 측면에서 낭비가 발생할 수 있는 환경이라면 MigrationStep과 enum의 rawValue를 잘 조합해서 필요한 MigrationStep만 초기화 시켜서 사용할 수도 있습니다.
256 |
257 | 사실 뭐 정답이 없는거라 현재 환경과 취향것 응용해서 사용하시면 좋을것 같습니다 🙂
258 |
--------------------------------------------------------------------------------
/Behavioral patterns/Iterator/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Iterator/Resources/diagram.png
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/CareTaker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Originator.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | final class CareTaker: ObservableObject {
11 |
12 | // MARK: - Properties
13 |
14 | @Published var settingsHistory: [Memento] = []
15 | private var currentIndex = 0 {
16 | didSet {
17 | self.currentSettings = settingsHistory[currentIndex]
18 | }
19 | }
20 |
21 | @Published var currentSettings: Memento
22 |
23 |
24 | // MARK: - Initializers
25 |
26 | init(initialSettings: Memento) {
27 | self.currentSettings = initialSettings
28 | settingsHistory.append(initialSettings)
29 | }
30 |
31 |
32 | // MARK: - Methods
33 |
34 | func save(settings: Memento) {
35 | if currentIndex < settingsHistory.count - 1 {
36 | settingsHistory = Array(settingsHistory.prefix(upTo: currentIndex + 1))
37 | }
38 | settingsHistory.append(settings)
39 | currentIndex = settingsHistory.count - 1
40 | }
41 |
42 | func undo() {
43 | if isUndoAvailable() {
44 | currentIndex -= 1
45 | }
46 | }
47 |
48 | func redo() {
49 | if isRedoAvailable() {
50 | currentIndex += 1
51 | }
52 | }
53 |
54 | func isUndoAvailable() -> Bool {
55 | currentIndex > 0 ? true : false
56 | }
57 |
58 | func isRedoAvailable() -> Bool {
59 | currentIndex < settingsHistory.count - 1 ? true : false
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | // MARK: - Properties
13 |
14 | @StateObject var careTaker: CareTaker
15 |
16 |
17 | // MARK: - Views
18 |
19 | var body: some View {
20 | ZStack {
21 | VStack {
22 | HStack() {
23 | Text("History")
24 | .font(.system(size: 24, weight: .bold, design: .rounded))
25 | .padding()
26 |
27 | ScrollView {
28 | VStack {
29 | ForEach(Array(careTaker.settingsHistory.reversed().enumerated()), id: \.offset) { _, setting in
30 | Text("\(setting.hat) \(setting.face) \(setting.cloth) \(setting.shoes)")
31 | }
32 | }
33 | .frame(maxWidth: .infinity)
34 | .padding()
35 | }
36 | .frame(maxWidth: .infinity, maxHeight: 144)
37 | .background(RoundedRectangle(cornerRadius: 10).fill(Color.gray.opacity(0.1)))
38 | .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
39 | .padding()
40 | }
41 |
42 | Spacer()
43 | }
44 |
45 | VStack {
46 | EmojiPickerView(emojis: Hat.allCases.map { $0.emoji },
47 | selectedEmoji: $careTaker.currentSettings.hat) { newEmoji in
48 | careTaker.currentSettings.hat = newEmoji
49 | careTaker.save(settings: careTaker.currentSettings)
50 | }
51 |
52 | EmojiPickerView(emojis: Face.allCases.map { $0.emoji },
53 | selectedEmoji: $careTaker.currentSettings.face) { newEmoji in
54 | careTaker.currentSettings.face = newEmoji
55 | careTaker.save(settings: careTaker.currentSettings)
56 | }
57 |
58 | EmojiPickerView(emojis: Cloth.allCases.map { $0.emoji },
59 | selectedEmoji: $careTaker.currentSettings.cloth) { newEmoji in
60 | careTaker.currentSettings.cloth = newEmoji
61 | careTaker.save(settings: careTaker.currentSettings)
62 | }
63 |
64 | EmojiPickerView(emojis: Shoes.allCases.map { $0.emoji },
65 | selectedEmoji: $careTaker.currentSettings.shoes) { newEmoji in
66 | careTaker.currentSettings.shoes = newEmoji
67 | careTaker.save(settings: careTaker.currentSettings)
68 | }
69 | }
70 |
71 | VStack {
72 | Spacer()
73 |
74 | HStack(spacing: 12) {
75 | Button {
76 | careTaker.undo()
77 | } label: {
78 | Image(systemName: "arrow.uturn.left")
79 | .font(.system(size: 20))
80 | .foregroundStyle(Color.black)
81 | .opacity(careTaker.isUndoAvailable() ? 1.0 : 0.5)
82 | }
83 | .disabled(!careTaker.isUndoAvailable())
84 |
85 | Button {
86 | careTaker.redo()
87 | } label: {
88 | Image(systemName: "arrow.uturn.right")
89 | .font(.system(size: 20))
90 | .foregroundStyle(Color.black)
91 | .opacity(careTaker.isRedoAvailable() ? 1.0 : 0.5)
92 | }
93 | .disabled(!careTaker.isRedoAvailable())
94 | }
95 | .padding()
96 | .background(RoundedRectangle(cornerRadius: 10).fill(Color.gray.opacity(0.1)))
97 | .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
98 | .padding()
99 | }
100 | }
101 | }
102 | }
103 |
104 |
105 | // MARK: - Preview
106 |
107 | #Preview {
108 | let initialSetting = Memento(hat: Hat.ribbon.emoji,
109 | face: Face.smile.emoji,
110 | cloth: Cloth.tShirt.emoji,
111 | shoes: Shoes.sneakers.emoji)
112 | return ContentView(careTaker: CareTaker(initialSettings: initialSetting))
113 | }
114 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/EmojiPickerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiPickerView.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct EmojiPickerView: View {
11 |
12 | // MARK: - Properties
13 |
14 | let emojis: [String]
15 | @Binding var selectedEmoji: String
16 | @State private var currentIndex = 0
17 | let didChangeSelection: (String) -> Void
18 |
19 |
20 | // MARK: - Views
21 |
22 | var body: some View {
23 | HStack(spacing: 20) {
24 | Button {
25 | currentIndex -= 1
26 | didChangeSelection(emojis[currentIndex])
27 | } label: {
28 | Image(systemName: "arrowtriangle.backward.fill")
29 | .font(.system(size: 28))
30 | .foregroundStyle(Color.gray.opacity(0.5))
31 | .opacity(currentIndex == 0 ? 0.5 : 1.0)
32 | }
33 | .disabled(currentIndex == 0)
34 |
35 | Text(selectedEmoji)
36 | .font(.system(size: 50))
37 |
38 | Button {
39 | currentIndex += 1
40 | didChangeSelection(emojis[currentIndex])
41 | } label: {
42 | Image(systemName: "arrowtriangle.right.fill")
43 | .font(.system(size: 28))
44 | .foregroundStyle(Color.gray.opacity(0.5))
45 | .opacity(currentIndex == (emojis.count - 1) ? 0.5 : 1.0)
46 | }
47 | .disabled(currentIndex == (emojis.count - 1))
48 | }
49 | .onChange(of: currentIndex) { index in
50 | selectedEmoji = emojis[index]
51 | }
52 | .onChange(of: selectedEmoji) { newValue in
53 | currentIndex = emojis.firstIndex(of: selectedEmoji)!
54 | }
55 | }
56 | }
57 |
58 | // MARK: - Preview
59 |
60 | #Preview {
61 | struct ContentView: View {
62 | @State private var selectedEmoji = Hat.cap.emoji
63 | var body: some View {
64 | EmojiPickerView(emojis: Hat.allCases.map { $0.emoji },
65 | selectedEmoji: $selectedEmoji,
66 | didChangeSelection: { _ in })
67 | }
68 | }
69 |
70 | return ContentView()
71 | }
72 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Emojis.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmojiIngredients.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Hat: CaseIterable {
11 | case ribbon
12 | case army
13 | case cap
14 | case magician
15 | case graduation
16 |
17 | var emoji: String {
18 | switch self {
19 | case .ribbon:
20 | return "👒"
21 | case .army:
22 | return "🪖"
23 | case .cap:
24 | return "🧢"
25 | case .magician:
26 | return "🎩"
27 | case .graduation:
28 | return "🎓"
29 | }
30 | }
31 | }
32 |
33 | enum Face: CaseIterable {
34 | case smile
35 | case wink
36 | case laughing
37 | case surprised
38 | case sad
39 |
40 | var emoji: String {
41 | switch self {
42 | case .smile:
43 | return "😊"
44 | case .wink:
45 | return "😉"
46 | case .laughing:
47 | return "😆"
48 | case .surprised:
49 | return "😯"
50 | case .sad:
51 | return "😢"
52 | }
53 | }
54 | }
55 |
56 | enum Cloth: CaseIterable {
57 | case tShirt
58 | case pinkTop
59 | case swimmingWear
60 | case dress
61 | case coat
62 |
63 | var emoji: String {
64 | switch self {
65 | case .tShirt:
66 | return "👕"
67 | case .pinkTop:
68 | return "👚"
69 | case .swimmingWear:
70 | return "🩱"
71 | case .dress:
72 | return "👗"
73 | case .coat:
74 | return "🧥"
75 | }
76 | }
77 | }
78 |
79 | enum Shoes: CaseIterable {
80 | case sneakers
81 | case boots
82 | case highHeels
83 | case sandals
84 | case loafers
85 |
86 | var emoji: String {
87 | switch self {
88 | case .sneakers:
89 | return "👟"
90 | case .boots:
91 | return "👢"
92 | case .highHeels:
93 | return "👠"
94 | case .sandals:
95 | return "🩴"
96 | case .loafers:
97 | return "👞"
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Memento.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Memento.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Memento: Equatable {
11 | var hat: String
12 | var face: String
13 | var cloth: String
14 | var shoes: String
15 | }
16 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/MementoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MementoApp.swift
3 | // Memento
4 | //
5 | // Created by 이승기 on 3/19/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct MementoApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | let initialSetting = Memento(hat: Hat.ribbon.emoji,
15 | face: Face.smile.emoji,
16 | cloth: Cloth.tShirt.emoji,
17 | shoes: Shoes.sneakers.emoji)
18 | ContentView(careTaker: CareTaker(initialSettings: initialSetting))
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Example/Memento/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/README.md:
--------------------------------------------------------------------------------
1 | # Memento pattern
2 |
3 | 메멘토는 위키백과에 의하면 "기억의 증표"를 뜻한다고 합니다.
4 |
5 | 메멘토의 핵심 의도도 이와 비슷한데요! 바로 상태의 저장과 복원을 도와주는 디자인 패턴이 바로 메멘토인 것이죠.
6 |
7 | 우리 그림판에서 그림 그리고 마음에 안 들면 undo, redo 이런거 자주 사용하잖아요?
8 |
9 | 바로 이런 기능을 메멘토를 이용하여 구현할 수 있습니다!
10 |
11 | 메멘토의 특징을 하나 더 얘기해 보자면 메멘토를 이용하면 객체의 세부사항을 공개하지 않아도 된다는 건데요.
12 |
13 | 이게 무슨 말인지는 아직 잘 와닿지 않을 겁니다.
14 |
15 | 이는 메멘토의 구조와 사용 예시를 같이 보면서 이해해 보자구요~
16 |
17 |
18 |
19 | ## 구조
20 |
21 | 
22 |
23 | 우선 구조는 위와 같습니다.
24 |
25 | ### Originator
26 |
27 | Originator는 상태를 저장하고 복원할 수 있는 역할을 합니다.
28 |
29 | 그냥 클라이언트 쪽이라고 생각해도 무방할 것 같습니다.
30 |
31 | 개발하는 환경에 따라서 다르겠지만 뷰가 있는 환경이라면 뷰에서 이 origiantor를 구현하고 있어도 무방하다고 생각합니다.
32 |
33 | (뷰에서 저장 버튼을 누르면 save() 하고, 복구 버튼 누르면 restore() 바로 할 수 있기 때문에 간단한 구조에서는 굳이 레이어를 하나 더 나누는 것이 의미가 있나 싶음)
34 |
35 | ### Memento
36 |
37 | 메멘토는 스냅샷 역할을 하는 객체입니다.
38 |
39 | 그러니까 동영상의 한 장면 이라고 생각하시면 좋을것 같습니다.
40 |
41 | 이런 장면이란 단위가 존재하기 때문에 undo() redo() 액션이 존재할 수 있는거겠죠?
42 |
43 | 다른 다이어그램에는 getState()같은것도 있는데 어떻게 구조화 해서 사용하느냐에 따라서 다른 부분입니다. 너무 정해진 구조에 얽메이지 마셔도 됩니다! (Originator, Caretaker도 마찬가지)
44 |
45 | ### Caretaker
46 |
47 | Caretaker는 보이는 것처럼 history를 가지고있죠? 요소는 Memento이구요,
48 |
49 | 즉 memento(스냅샷) 기록들을 모두 가지고 있고 이를 undo(), redo() 할 수 있게 도와주는 실질적인 객체인 셈이죠.
50 |
51 | 앞에서 저는 Originator를 그냥 뷰에서 구현해서 사용한다고 했었죠? 이런 상황에서 CareTaker를 뷰모델의 역할 정도로 이해하시면 될 것 같습니다.
52 |
53 |
54 |
55 | ## IRL (In Real Life)
56 |
57 | 메멘토가 뭐하는 녀석인지도, 구조도 알았으니 한 번 메멘토 패턴을 이용해서 뭔가 만들어 봅시다!
58 |
59 | 저는 이모티콘으로 옷입히기 게임이나 함 만들어보겠습니다.
60 |
61 | 
62 |
63 | 일단 상태를 가지고있는 스냅샷 즉 메멘토 객체를 만들어 주겠습니다.
64 |
65 | ```Swift
66 | struct Memento: Equatable {
67 | var hat: String
68 | var face: String
69 | var cloth: String
70 | var shoes: String
71 | }
72 | ```
73 |
74 |
75 |
76 | 그리고 Caretaker 역할을 할 녀석을 만들어줘보죠.
77 |
78 | 저는 이 Caretaker를 SwiftUI View에서 바로 가져다 사용할 것이기 때문에 ObservableObject로 구현해 줬습니다.
79 |
80 | ```Swift
81 | final class CareTaker: ObservableObject {
82 |
83 | // MARK: - Properties
84 |
85 | @Published var settingHistory: [Memento] = []
86 | private var currentIndex = 0 {
87 | didSet {
88 | self.currentSettings = settingHistory[currentIndex]
89 | }
90 | }
91 |
92 | @Published var currentSettings: Memento
93 |
94 |
95 | // MARK: - Initializers
96 |
97 | init(initialSettings: Memento) {
98 | self.currentSettings = initialSettings
99 | settingHistory.append(initialSettings)
100 | }
101 | }
102 | ```
103 |
104 | 그리고 여기에 save() undo() redo() 로직을 구현해 줘 보겠습니다.
105 |
106 | ```Swift
107 | func save(settings: Memento) {
108 | if currentIndex < settingsHistory.count - 1 {
109 | settingsHistory = Array(settingsHistory.prefix(upTo: currentIndex + 1))
110 | }
111 | settingsHistory.append(settings)
112 | currentIndex = settingsHistory.count - 1
113 | }
114 |
115 | func undo() {
116 | if isUndoAvailable() {
117 | currentIndex -= 1
118 | }
119 | }
120 |
121 | func redo() {
122 | if isRedoAvailable() {
123 | currentIndex += 1
124 | }
125 | }
126 |
127 | func isUndoAvailable() -> Bool {
128 | currentIndex > 0 ? true : false
129 | }
130 |
131 | func isRedoAvailable() -> Bool {
132 | currentIndex < settingsHistory.count - 1 ? true : false
133 | }
134 | ```
135 |
136 | save 함수 안에 거추장스러운 if 분기가 있는데 저건 undo를 몇 번 실행하면 redo할 수 있는 스냅샷들이 currentIndex 후 배열 뒷 부분에 존재할텐데 새로운 스냅샷이 생기면 그 뒷 부분에 존재하던 스냅샷들은 필요 없으니까 버려주는 역할을 합니다.
137 |
138 | 어쨌든 필수 구현 요소들은 다 구현할 것 같네요.
139 |
140 | 그럼 이어서 뷰들도 만들고 적절히 Caretaker 함수들 호출해 주고(자세한건 Example폴더 내에 있음) 실행해 보면 아래와같이 메멘토 패턴을 이용해서 undo(), redo() 기능을 구현한 모습을 보실 수 있습니다.
141 |
142 | 
143 |
144 | 꼭 옷 입히기가 아니더라도 스냅샷을 만들어 되돌아가기 등등 (예를들면 인스타그램 스토리 만들기) 다양한 경우에 메멘토 패턴을 활용해 볼 수 있으니까 지금 당장 만들러 가보세요~
145 |
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Resources/emoji_picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Memento/Resources/emoji_picker.png
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Resources/simulator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Memento/Resources/simulator.gif
--------------------------------------------------------------------------------
/Behavioral patterns/Memento/Resources/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Memento/Resources/structure.png
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // State
4 | //
5 | // Created by 이승기 on 3/25/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | // MARK: - Properties
13 |
14 | @StateObject var deliveryContext = DeliveryContext(initialState: Preparing())
15 |
16 |
17 | // MARK: - Views
18 |
19 | var body: some View {
20 | ZStack {
21 | VStack(spacing: 12) {
22 | Image(systemName: deliveryContext.state.imageName)
23 | .font(.system(size: 40))
24 | .foregroundStyle(.gray.opacity(0.5))
25 |
26 | Text(deliveryContext.state.description)
27 | }
28 |
29 | VStack {
30 | Spacer()
31 |
32 | HStack {
33 | Button {
34 | deliveryContext.previous()
35 | } label: {
36 | prevButton()
37 | }
38 |
39 | Spacer()
40 |
41 | Button {
42 | deliveryContext.next()
43 | } label: {
44 | nextButton()
45 | }
46 | }
47 | .padding()
48 | }
49 | }
50 | }
51 |
52 | private func nextButton() -> some View {
53 | HStack {
54 | Text("다음 상태로")
55 | Image(systemName: "arrow.right")
56 | }
57 | .padding()
58 | .background(RoundedRectangle(cornerRadius: 10).fill(Color.black))
59 | .foregroundStyle(Color.white)
60 | }
61 |
62 | private func prevButton() -> some View {
63 | HStack {
64 | Image(systemName: "arrow.left")
65 | Text("이전 상태로")
66 | }
67 | .padding()
68 | .foregroundStyle(Color.black)
69 | }
70 | }
71 |
72 | #Preview {
73 | ContentView()
74 | }
75 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/DeliveryState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeliveryState.swift
3 | // State
4 | //
5 | // Created by 이승기 on 3/25/24.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol DeliveryState {
11 | var imageName: String { get }
12 | var description: String { get }
13 | func updateToNextState(context: DeliveryContext)
14 | func updateToPreviousState(context: DeliveryContext)
15 | }
16 |
17 | class DeliveryContext: ObservableObject {
18 | @Published var state: DeliveryState = Preparing()
19 |
20 | init(initialState: DeliveryState) {
21 | self.state = initialState
22 | }
23 |
24 | func setState(state: DeliveryState) {
25 | self.state = state
26 | }
27 |
28 | func next() {
29 | state.updateToNextState(context: self)
30 | }
31 |
32 | func previous() {
33 | state.updateToPreviousState(context: self)
34 | }
35 | }
36 |
37 | struct Preparing: DeliveryState {
38 | var imageName: String = "shippingbox.fill"
39 | var description: String = "배송 준비중 입니다."
40 |
41 | func updateToNextState(context: DeliveryContext) {
42 | print("배송중 상태로 업데이트")
43 | context.setState(state: OutForDelivery())
44 | }
45 |
46 | func updateToPreviousState(context: DeliveryContext) {
47 | print("이전 상태는 없습니다.")
48 | }
49 | }
50 |
51 | struct OutForDelivery: DeliveryState {
52 | var imageName: String = "truck.box.fill"
53 | var description: String = "배송 중입니다."
54 |
55 | func updateToNextState(context: DeliveryContext) {
56 | print("배송완료 상태로 업데이트")
57 | context.setState(state: Delivered())
58 | }
59 |
60 | func updateToPreviousState(context: DeliveryContext) {
61 | print("준비중 상태로 업데이트")
62 | context.setState(state: Preparing())
63 | }
64 | }
65 |
66 | struct Delivered: DeliveryState {
67 | var imageName: String = "bell.and.waves.left.and.right.fill"
68 | var description: String = "배송이 완료되었습니다."
69 |
70 | func updateToNextState(context: DeliveryContext) {
71 | print("배송이 이미 완료되었습니다.")
72 | }
73 |
74 | func updateToPreviousState(context: DeliveryContext) {
75 | print("배송중 상태로 업데이트")
76 | context.setState(state: OutForDelivery())
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Example/State/StateApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StateApp.swift
3 | // State
4 | //
5 | // Created by 이승기 on 3/25/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct StateApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/README.md:
--------------------------------------------------------------------------------
1 | # State Pattern
2 |
3 | 이번에는 상태 패턴에 대해서 알아보겠습니다.
4 |
5 | 상태 패턴이라고 하면 이름 때문에 SwiftUI의 Published 객체나 Combine, Rx의 스트림이 있고 그걸 구독해서 사용하는 것을 상태 패턴이라고 오해하실 수 있습니다.
6 |
7 | 뭐 "상태 어떻게 정의하느냐에 따라서 달라질 수 있겠지만 지금 여기에서 얘기하려는 상태 패턴은 조금 다르니까 기존에 알고 있던 상태와는 별개로 생각해서 봐주시길 바랍니다!!
8 |
9 |
10 |
11 | ### 예시
12 |
13 | 한가지 예시를 들어보겠습니다.
14 |
15 | 고객으로부터 주문을 받아서 주문 상태를 업데이트해 주는 화면이 있다고 가정해 볼게요.
16 |
17 | 그러면 주문 상태를 단계별로 업데이트 시켜줄 수 있어야겠죠?
18 |
19 | 주문 상태를 다음과 같이 구분하고 업데이트 시켜줘 볼게요. 어떻게 할 수 있을까요?
20 |
21 | - 준비 중
22 | - 배송 중
23 | - 배송 완료
24 |
25 | 이 상태에서 다음, 이전 상태로 되돌리기 버튼이 있다고 해보겠습니다.
26 |
27 | 그렇다면 준비 중, 배송 중, 배송 완료 각각의 상태에 따라서 "다음", "이전 상태로 되돌리기" 버튼의 역할이 다음과 같이 달라질 겁니다.
28 |
29 | *준비 중*
30 |
31 | - 다음 -> 배송 중 상태로 업데이트
32 | - 이전 상태로 되돌리기 -> 액션 없음
33 |
34 | *배송 중*
35 |
36 | - 다음 -> 배송 완료 상태로 업데이트
37 | - 이전 상태로 되돌리기 -> 준비 중 상태로 업데이트
38 |
39 | *배송 완료*
40 |
41 | - 다음 -> 액션 없음
42 | - 이전 상태로 되돌리기 -> 배송 중 상태로 업데이트
43 |
44 | 이걸 코드로 구현한다면 어떻게 할 수 있을까요?
45 |
46 | ```Swift
47 | didTapNextButton(deliveryState: DeliveryState) {
48 | switch deliveryState {
49 | case .preparing:
50 | // 배송 중 상태로 업데이트 로직
51 | case .outForDelivery:
52 | // 배송 완료 상태로 업데이트
53 | case .delivered:
54 | // 액션 없음
55 | }
56 | }
57 |
58 | // 위와 로직 비슷하게 작성
59 | didTapPreviousButton(deliveryState: DeliveryState) { ... }
60 | ```
61 |
62 | 이렇게 할 수 있을까요?
63 |
64 | 코드가 짧으니 그냥 보기엔 별 문제 없어보입니다.
65 |
66 | 하지만 실제 상황에서는 이런 조건 사이사이 코드가 훨씬 길어지면 가독성이 정말 많이 떨어지고 케이스가 추가해 줌에 따라서 항상 저 스위치 문을 수정해 줘야 해서 OCP도 자연스럽게 위배하게 될 겁니다.
67 |
68 | 그렇다면 어떻게 해결할 수 있을까요?
69 |
70 |
71 |
72 | ## 그래서 상태 패턴이 존재합니다
73 |
74 | 상태패턴은 위와같이 각 상태에 해당하는 행동들을 캡슐화하는 것을 의미합니다!
75 |
76 | 상태 패턴에는 크게 두 가지 요소가 존재합니다.
77 |
78 | State와 Context인데요 State는 상태 자체와 그 상태에 작동하는 행동들의 구현체이고요, Context는 현재 state와 state를 업데이트 해줄 수 있는 기능들을 가지고 있습니다.
79 |
80 | 그러니까 Context 내에서 여러 개의 State를 교체해 가면서 사용하는 느낌인 것이죠.
81 |
82 | 다이어그램으로 보면 아래와 같습니다.
83 |
84 | 
85 |
86 |
87 |
88 | 위에서 얘기한 배송 상태 업데이트 로직을 상태 패턴을 가지고 구현해 보겠습니다.
89 |
90 | 일단은 State 추상화 객체와 이를 교체해서 사용할 수 있는 Context를 구현해 주겠습니다.
91 |
92 | ```Swift
93 | protocol DeliveryState {
94 | func updateToNextState(context: DeliveryContext)
95 | func updateToPreviousState(context: DeliveryContext)
96 | }
97 |
98 | class DeliveryContext {
99 | var state: DeliveryState = Preparing()
100 |
101 | init(initialState: DeliveryState) {
102 | self.state = initialState
103 | }
104 |
105 | func setState(state: DeliveryState) {
106 | self.state = state
107 | }
108 |
109 | func next() {
110 | state.updateToNextState(context: self)
111 | }
112 |
113 | func previous() {
114 | state.updateToPreviousState(context: self)
115 | }
116 | }
117 | ```
118 |
119 | 그리고 각 상태의 구조체나 클래스를 만들어서 위에서 만든 프로토콜을 채택 후 구현해 줍니다.
120 |
121 | ```Swift
122 | struct Preparing: DeliveryState {
123 | func updateToNextState(context: DeliveryContext) {
124 | print("배송중 상태로 업데이트")
125 | context.setState(state: OutForDelivery())
126 | }
127 |
128 | func updateToPreviousState(context: DeliveryContext) {
129 | print("이전 상태는 없습니다.")
130 | }
131 | }
132 |
133 | struct OutForDelivery: DeliveryState {
134 | func updateToNextState(context: DeliveryContext) { ... }
135 | func updateToPreviousState(context: DeliveryContext) { ... }
136 | }
137 |
138 | struct Delivered: DeliveryState {
139 | func updateToNextState(context: DeliveryContext) { ... }
140 | func updateToPreviousState(context: DeliveryContext) { ... }
141 | }
142 | ```
143 |
144 | 이렇게 하면 사용할 준비는 모두 완료된 겁니다! 간단하죠?
145 |
146 | 여기에서 클라이언트는 DeliveryContext를 초기화 하고 state를 직접 교체해 가면서 사용하거나 Context에서 제공하는 previous(), next() 함수를 통해서 state을 변경해서 사용하면 됩니다. (꼭 next, previous를 구현해야 하는 것은 아닙니다)
147 |
148 |
149 |
150 | 어쨌든 상태 패턴의 핵심은 각 상태에 따른 행동들을 캡슐화하고, 캡슐화된 각 상태를 교체해 가면서 같은 메서드를 호출하더라고 현재 어떤 상태냐에 따라서 다른 행동들을 실행할 수 있도록 하는 것입니다.
151 |
152 | 그리고 처음 예시처럼 if나 switch 조건문을 만들지 않고도 캡슐화를 유지하면서 다음 상태나 이전 상태로 돌아갈 수 있는 것의 핵심은 결국 각각의 State 구현체가 이전 상태와 다음 상태를 알고 있기 때문에 가능한 것입니다.
153 |
154 | ```Swift
155 | struct OutForDelivery: DeliveryState {
156 | func updateToNextState(context: DeliveryContext) {
157 | context.setState(state: Delivered()) // <- 이렇게 다음 상태를 알고
158 | }
159 |
160 | func updateToPreviousState(context: DeliveryContext) {
161 | print("준비중 상태로 업데이트")
162 | context.setState(state: Preparing()) // <- 이전 상태를 알기때문에 조건문 없이 유연하게 확장 가능한 것임.
163 | }
164 | }
165 | ```
166 |
167 |
168 |
169 | ## 실행 화면
170 |
171 | 위에서 설명드린 코드는 이해를 돕기 위해서 생략한 부분이 좀 있어 전체 코드를 보고싶으시다면 예시 프로젝트 참고 부탁드립니다.
172 |
173 | 
174 |
--------------------------------------------------------------------------------
/Behavioral patterns/State/Resources/client_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/State/Resources/client_code.png
--------------------------------------------------------------------------------
/Behavioral patterns/State/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/State/Resources/diagram.png
--------------------------------------------------------------------------------
/Behavioral patterns/State/Resources/simulator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/State/Resources/simulator.gif
--------------------------------------------------------------------------------
/Behavioral patterns/Strategy/README.md:
--------------------------------------------------------------------------------
1 | # Strategy Pattern
2 |
3 | 이번에는 전략 패턴에 대해서 알아보겠습니다.
4 |
5 | 전략 패턴은 정말 별거 없습니다. POP(Protocol Oriented Programming)을 지키며 코드를 짜오시던 분들께는 특히 더요.
6 |
7 | 전략 패턴의 핵심부터 얘기해보자면 자주 변경되는 알고리즘을 protocol로 추상화해서 이를 구현하는 구현체 내에 각기 다른 알고리즘을 캡슐화 한 뒤 런타임에 입맛에 골라서 알고리즘을 사용해라~ 이런 말인데..
8 |
9 | 이 말도 좀 헷갈리네요. 코드로 보여드리자면 이렇습니다.
10 |
11 | 기존 코드
12 | ```Swift
13 | struct Decoder {
14 | func decode(data: Data, type: String) {
15 | if type == "json" {
16 | // json 디코딩 알고리즘
17 | } else if type == "xml" {
18 | // xml 디코딩 알고리즘
19 | }
20 | }
21 | }
22 | ```
23 |
24 | 이런 코드를 아래와같이 추상화된 객체에 의존해라~ 이 말입니다.
25 |
26 | ```Swift
27 | protocol DecodeStrategy {
28 | func decode(data: Data) // <- 이렇게 알고리즘을 캡슐화 할 수 있게 추상화
29 | }
30 |
31 | struct JsonDecodeStrategy: DecodeStrategy {
32 | func decode(data: Data) {
33 | // json 디코딩 알고리즘
34 | }
35 | }
36 |
37 | struct XMLDecodeStrategy: DecodeStrategy {
38 | func decode(data: Data) {
39 | // xml 디코딩 알고리즘
40 | }
41 | }
42 |
43 | struct Decoder {
44 | let strategy: DecodeStrategy
45 |
46 | init(strategy: DecodeStrategy) {
47 | self.strategy = strategy
48 | }
49 |
50 | func decode(data: Data) {
51 | strategy.decode(data: data)
52 | }
53 | }
54 | ```
55 |
56 | 읭?? 걍 추상화 된 객체에 의존한 흔하디 흔한 방식 아냐?
57 |
58 | 예 맞습니다.
59 |
60 | 근데 strategy 패턴에서 강조하는 것이 있습니다.
61 |
62 | 바로 👉알고리즘👈 을 캡슐화 해야한다는 것 입니다.
63 |
64 | 즉 알고리즘을 캡슐화 하지 않고 다른 어떠한 로직을 추상화된 객체의 구현체로 캡슐화 한다는거는 strategy 패턴이 아니란 소린데..
65 |
66 | 솔직히 그냥 말장난 같습니다.
67 |
68 | 아니면 POP가 익숙한 swift 사용자에게 특히 그렇게 느껴지는 것일지도 모르겠네요!
69 |
70 | 어쨌던 Strategy 패턴은 그냥 이런 사용 양식을 거창하게 Strategy라고 부르는구나.. 정도로 이해해도 괜찮지 않을까요?
71 |
72 |
73 |
74 | ## Wrap up
75 |
76 | 따라서 전략 패턴의 정의를 정리하자면 다음과 같겠습니다.
77 |
78 | > 자주 변경되는 알고리즘을 각각 캡슐화 해서 런타임에 알고리즘을 교체할 수 있게 도와주는 패턴
79 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "2ad69c0deaec8838f2417eb6e8d288b30a36442bc22243bc0f5ee813f88e20a7",
3 | "pins" : [
4 | {
5 | "identity" : "snapkit",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/SnapKit/SnapKit",
8 | "state" : {
9 | "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
10 | "version" : "5.7.1"
11 | }
12 | },
13 | {
14 | "identity" : "then",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/devxoul/Then",
17 | "state" : {
18 | "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a",
19 | "version" : "3.0.0"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Visitor
4 | //
5 | // Created by 이승기 on 3/16/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | // MARK: UISceneSession Lifecycle
19 |
20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
21 | // Called when a new scene session is being created.
22 | // Use this method to select a configuration to create the new scene with.
23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
24 | }
25 |
26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
27 | // Called when the user discards a scene session.
28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/MarkdownPlainTextViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownPlainTextViewController.swift
3 | // Visitor
4 | //
5 | // Created by 이승기 on 3/16/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 | import Then
11 |
12 | final class MarkdownPlainTextViewController: UIViewController {
13 |
14 | // MARK: - UI
15 |
16 | private let textView = UITextView().then {
17 | $0.textColor = .label
18 | $0.font = .systemFont(ofSize: 20)
19 | $0.textAlignment = .left
20 | $0.isScrollEnabled = true
21 | $0.isEditable = false // 사용자가 텍스트를 편집하지 못하게 설정
22 | $0.backgroundColor = .clear // UILabel과 같은 배경색을 사용하려면
23 | }
24 |
25 | // MARK: - Properties
26 |
27 | private let markdownText: String
28 |
29 |
30 | // MARK: - LifeCycle
31 |
32 | init(markdownText: String) {
33 | self.markdownText = markdownText
34 | super.init(nibName: nil, bundle: nil)
35 |
36 | textView.text = markdownText
37 | }
38 |
39 | required init?(coder: NSCoder) {
40 | fatalError("init(coder:) has not been implemented")
41 | }
42 |
43 | override func viewDidLoad() {
44 | super.viewDidLoad()
45 |
46 | setupView()
47 | setupLayout()
48 | }
49 |
50 | private func setupView() {
51 | view.backgroundColor = .systemBackground
52 | view.addSubview(textView) // textView를 뷰에 추가
53 | }
54 |
55 | private func setupLayout() {
56 | textView.snp.makeConstraints {
57 | $0.top.equalTo(view.safeAreaLayoutGuide.snp.top).inset(24)
58 | $0.horizontalEdges.equalToSuperview().inset(24)
59 | $0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).inset(24) // UITextView의 높이를 설정
60 | }
61 | }
62 | }
63 |
64 |
65 | // MARK: - Preview
66 |
67 | #Preview {
68 | MarkdownPlainTextViewController(markdownText: "sample text")
69 | }
70 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Visitor
4 | //
5 | // Created by 이승기 on 3/16/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | guard let scene = (scene as? UIWindowScene) else { return }
16 | let window = UIWindow(windowScene: scene)
17 | window.rootViewController = ViewController()
18 | window.makeKeyAndVisible()
19 | self.window = window
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/TextEditorElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextEditorElement.swift
3 | // Visitor
4 | //
5 | // Created by 이승기 on 3/16/24.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol TextEditorElement {
11 | var textField: UITextField { get set }
12 | func render() -> UIView
13 | func accept(visitor: Visitor)
14 | }
15 |
16 |
17 |
18 |
19 | class TextElement: TextEditorElement {
20 | var textField = UITextField()
21 |
22 | func render() -> UIView {
23 | textField.font = .systemFont(ofSize: 16, weight: .regular)
24 | textField.placeholder = "내용을 입력해 주세요"
25 | return textField
26 | }
27 |
28 | func accept(visitor: Visitor) {
29 | visitor.visit(e: self)
30 | }
31 | }
32 |
33 | class HeaderElement: TextEditorElement {
34 | var textField = UITextField()
35 |
36 | func render() -> UIView {
37 | textField.font = .systemFont(ofSize: 32, weight: .bold)
38 | textField.placeholder = "내용을 입력해 주세요"
39 | return textField
40 | }
41 |
42 | func accept(visitor: Visitor) {
43 | visitor.visit(e: self)
44 | }
45 | }
46 |
47 | class BoldTextElement: TextEditorElement {
48 | var textField = UITextField()
49 |
50 | func render() -> UIView {
51 | textField.font = .systemFont(ofSize: 16, weight: .bold)
52 | textField.placeholder = "내용을 입력해 주세요"
53 | return textField
54 | }
55 |
56 | func accept(visitor: Visitor) {
57 | visitor.visit(e: self)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Example/Visitor/Visitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Visitor.swift
3 | // Visitor
4 | //
5 | // Created by 이승기 on 3/16/24.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol Visitor {
11 | func visit(e: TextElement)
12 | func visit(e: HeaderElement)
13 | func visit(e: BoldTextElement)
14 | }
15 |
16 |
17 |
18 |
19 |
20 | class MarkdownExporter: Visitor {
21 | var markdown = ""
22 |
23 | func visit(e: TextElement) {
24 | markdown += "\n\(e.textField.text ?? "")"
25 | }
26 |
27 | func visit(e: HeaderElement) {
28 | markdown += "\n#\(e.textField.text ?? "")"
29 | }
30 |
31 | func visit(e: BoldTextElement) {
32 | markdown += "\n**\(e.textField.text ?? "")**"
33 | }
34 | }
35 |
36 | class XMLExporter: Visitor {
37 | var xml = ""
38 |
39 | func visit(e: TextElement) {
40 | xml += "\(escape(e.textField.text ?? ""))\n"
41 | }
42 |
43 | func visit(e: HeaderElement) {
44 | xml += "\(escape(e.textField.text ?? ""))\n"
45 | }
46 |
47 | func visit(e: BoldTextElement) {
48 | xml += "\(escape(e.textField.text ?? ""))\n"
49 | }
50 |
51 | private func escape(_ string: String) -> String {
52 | let escapeMap: [String: String] = [
53 | "&": "&",
54 | "<": "<",
55 | ">": ">",
56 | "\"": """,
57 | "'": "'"
58 | ]
59 | var escapedString = string
60 | for (unescaped, escaped) in escapeMap {
61 | escapedString = escapedString.replacingOccurrences(of: unescaped, with: escaped)
62 | }
63 | return escapedString
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/README.md:
--------------------------------------------------------------------------------
1 | # Visitor
2 |
3 | visitor 패턴은 어떠한 기능을 객체들로부터 분리해서 관리할 수 있도록 도와주는 패턴입니다.
4 |
5 | 정의만으로는 이해가 잘 가지 않으니 예를들어서 텍스트 에디터를 만들었다고 가정해 볼게요.
6 |
7 | 텍스트 에디터에 다양한 스타일도 넣고 싶어서 TextEditorElement라는 프로토콜을 만들고 이 프로토콜을 채택하는 Header, Text, BoldText 도 만들었다고 해볼게요!
8 | 
9 |
10 | 그렇게 각 스타일에 맞게 꾸며서 잘 사용하고 있었습니다.
11 |
12 | 그런데 이 텍스트 에디터를 마크다운 문법으로 export 할 수 있도록 만들라는 요청이 떨어졌다고 해보겠습니다.
13 |
14 | 보통같으면 그냥 TextEditorElement 에
15 | ```func exportToMarkdown() -> String```
16 | 이렇게 추가해서 사용하거나 위 함수만을 가지고있는 protocol을 따로 만들어서 Concrete Element에 채택해서 사용할 것 같은데요
17 |
18 | Visitor를 이용하여 구현하면 markdown 뿐만 아니라 더 다양한 방식의 exporter를 구축해야할 때 위 아이디어보다 더 유연하게 사용할 수 있게됩니다.
19 |
20 | 이는 Visitor 패턴의 특징인 기존 객체를 변경하지 않고 기능을 확장할 수 있다는 점에서 기인합는데요, 이거에 대해서도 뒤에서 다시 한 번 다뤄보도록 하죠.
21 |
22 |
23 |
24 | ## Visitor 패턴의 종특, IRL
25 |
26 | Visotor 패턴을 구현하려면 이 패턴의 종특에 대해서 알아봐야겠죠?
27 |
28 | - Visitor 패턴에는 내가 기능을 확장 시키려는 대상인 Element
29 | - Element에 확장해서 구현하려던 구현체를 가지고있는 Visitor 이렇게 존재하게 됩니다.
30 |
31 | Visitor는 기본적으로 Protocol이고 이를 채택하는 Concnrete visitor들을 구현한 후 Element에 visitor를 수용할 수 있는 ```accept(v: Visitor)``` 를 구현한 후 이 함수에서 visitor의 구현체인 ```visit(e: Element)```를 호출함과 동시에 self를 넘겨주면 됩니다.
32 | 이를
33 | [더블 디스패치](https://en.wikipedia.org/wiki/Double_dispatch)
34 | 라고 하는데..
35 |
36 | 말이 좀 어렵죠? 코드 짜면서 조금씩 이해해 보자구요..
37 |
38 |
39 |
40 | ## IRL (In Real Life)
41 |
42 | 우리는 Markdown으로 TextElement를 export 할 수 있게 구현하려는 거니까 ConcreteVisitor가 Visitor 프로토콜을 채택한 MakrdownExporter 뭐 이렇게 구현할 수 있겠죠?
43 |
44 | 그리고 이 MakrdownExporter에 구현체를 구현해주면 됩니다.
45 |
46 | ```Swift
47 | protocol Visitor {
48 | func visit(e: TextElement)
49 | func visit(e: HeaderElement)
50 | func visit(e: BoldTextElement)
51 | }
52 |
53 | class MarkdownExporter: Visitor {
54 | var markdown = ""
55 |
56 | func visit(e: TextElement) {
57 | markdown += "\n\(e.textField.text ?? "")"
58 | }
59 |
60 | func visit(e: HeaderElement) {
61 | markdown += "\n#\(e.textField.text ?? "")"
62 | }
63 |
64 | func visit(e: BoldTextElement) {
65 | markdown += "\n**\(e.textField.text ?? "")**"
66 | }
67 | }
68 | ```
69 |
70 | 자 이렇게 visitor를 구현해줬으면 이 visitor를 사용하는 쪽도 구현해봐야겠죠?
71 | 바로 Element 쪽 말이죠!
72 |
73 | Element에는 외부에서 Visitor를 주입받고 본인을 다시 visitor에게 넘겨주는 방식인데요, 통상적으로 함수의 이름은 ```accept(visitor: Visitor)``` 로 많이 쓰입니다.
74 |
75 | 어쨌든 Element에 이 visitor 즉 방문자를 받아줄 수 있는 창구를 구현해 줘보겠습니다.
76 |
77 | ```Swift
78 | protocol TextEditorElement {
79 | var textField: UITextField { get set }
80 | func render() -> UIView
81 | func accept(visitor: Visitor)
82 | }
83 | ```
84 |
85 | 그리고 이를 구현하는 쪽에서는 ```visitor.visit(e: self)``` 이런식으로 Visitor에서 구현한 구현체를 호출해줍니다.
86 |
87 | ```Swift
88 | class TextElement: TextEditorElement {
89 | var textField = UITextField()
90 |
91 | func render() -> UIView {
92 | textField.font = .systemFont(ofSize: 16, weight: .regular)
93 | textField.placeholder = "내용을 입력해 주세요"
94 | return textField
95 | }
96 |
97 | func accept(visitor: Visitor) { // 👈 일케
98 | visitor.visit(e: self)
99 | }
100 | }
101 | ```
102 |
103 | 그럼 만약 TextElement의 accept를 호출하면서 MarkdownExporter를 주입하면 자연스럽게 아까 위에서 구현했던 MarkdownExporter의 ```func visit(e: TextElement)```함수가 호출 되겠죠?
104 |
105 | 구조가 좀 헷갈릴 거라 생각이 드는데요, 시각적으로 표현하자면 아래와 같습니다.
106 | 
107 |
108 | 이 시점에서 이 복잡한 구조를 굳이 사용해야하나..? 라는 의심이 점점 드셨을 겁니다.
109 | 자 그러면 만약 이 상태에서 XML exporter로 구현한다고 해보겠습니다.
110 |
111 | 그럼 아래와같은 구조로 만들어볼 수 있겠죠?
112 | 
113 |
114 | 자 방금 새로운 XMLExpoter를 추가하는데 TextEditorElement는 건들이지도 않고 단순히 XMLExpoter를 새로 구현해준 후 accept()에 XMLExporter를 넣은것 만으로 기능 확장을 하였습니다!
115 |
116 | 이것이 바로 Visitor 패턴의 미덕.. 이랄까나요?
117 |
118 |
119 |
120 |
121 | 어쨌든 사용하는 모습을 보여드리자면 이렇습니다.
122 |
123 | ```Swift
124 | let markdownExporter = MarkdownExporter()
125 | markdownElements.forEach { element in
126 | element.accept(visitor: markdownExporter)
127 | }
128 | ```
129 |
130 | MarkdownExporter라는 visitor를 만들어서 각 element에 하나씩 돌아가면서 **'방문'** 하는 모습 보이죠? 이래서 visitor 라는 이름이 지어진 게 아닌가 싶네요!
131 |
132 |
133 |
134 |
135 | ## 예제 프로젝트 실행 결과
136 |
137 | 
138 |
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Resources/TextEditorElement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Visitor/Resources/TextEditorElement.png
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Visitor/Resources/diagram.png
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Resources/diagram2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Visitor/Resources/diagram2.png
--------------------------------------------------------------------------------
/Behavioral patterns/Visitor/Resources/record.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Behavioral patterns/Visitor/Resources/record.gif
--------------------------------------------------------------------------------
/Creational patterns/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/.gitkeep
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/AbstractFactoryApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AbstractFactoryApp.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct AbstractFactoryApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/apeach.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "apeach.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "original"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/apeach.imageset/apeach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/apeach.imageset/apeach.png
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/con.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "con.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "original"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/con.imageset/con.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/AbstractFactory/Example/AbstractFactory/Assets.xcassets/con.imageset/con.png
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | // MARK: - Properties
13 |
14 | enum Theme: String, CaseIterable {
15 | case muji
16 | case apeach
17 | case con
18 |
19 | var factory: ThemeFactory {
20 | switch self {
21 | case .muji:
22 | return MuziThemeFactory()
23 | case .apeach:
24 | return AppeachThemeFactory()
25 | case .con:
26 | return ConThemeFactory()
27 | }
28 | }
29 | }
30 |
31 | @State private var currentTheme: Theme = .muji
32 |
33 |
34 | // MARK: - Views
35 |
36 | var body: some View {
37 | VStack {
38 | HStack {
39 | currentTheme.factory.createOpponentChatBubble(text: "안녕하세요")
40 | Spacer()
41 | }
42 |
43 | HStack {
44 | Spacer()
45 | currentTheme.factory.createMyChatBubble(text: "안녕하세요☺️")
46 | }
47 |
48 | HStack {
49 | Spacer()
50 | currentTheme.factory.createMyChatBubble(text: "아래에서 segment에서 테마를 선택할 수 있어요")
51 | }
52 |
53 | Spacer()
54 |
55 | Picker("테마를 선택해 주세요", selection: $currentTheme) {
56 | ForEach(Theme.allCases, id: \.self) { theme in
57 | Text(theme.rawValue)
58 | }
59 | }
60 | .pickerStyle(.segmented)
61 | }
62 | .padding(12)
63 | .frame(maxWidth: .infinity, maxHeight: .infinity)
64 | .background(
65 | currentTheme.factory.createBackground()
66 | .ignoresSafeArea()
67 | )
68 | }
69 | }
70 |
71 | #Preview {
72 | ContentView()
73 | }
74 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/ThemeFactory/ApeachThemeFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApeachThemeFactory.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AppeachThemeFactory: ThemeFactory {
11 | func createOpponentChatBubble(text: String) -> AnyView {
12 | AnyView(
13 | Text(text)
14 | .padding(8)
15 | .background(Color(hex: "#F9E1E7"))
16 | .foregroundStyle(Color(hex: "#6A696A"))
17 | .clipShape(Capsule())
18 | )
19 | }
20 |
21 | func createMyChatBubble(text: String) -> AnyView {
22 | AnyView(
23 | Text(text)
24 | .padding(8)
25 | .background(Color.white)
26 | .foregroundStyle(Color(hex: "#6A696A"))
27 | .clipShape(Capsule())
28 | )
29 | }
30 |
31 | func createBackground() -> AnyView {
32 | AnyView(
33 | Image("apeach")
34 | .resizable()
35 | .aspectRatio(contentMode: .fill)
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/ThemeFactory/ConThemeFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConThemeFactory.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ConThemeFactory: ThemeFactory {
11 | func createOpponentChatBubble(text: String) -> AnyView {
12 | AnyView(
13 | Text(text)
14 | .padding(8)
15 | .background(Color.white)
16 | .foregroundStyle(Color(hex: "#4D4D4D"))
17 | .clipShape(Capsule())
18 | )
19 | }
20 |
21 | func createMyChatBubble(text: String) -> AnyView {
22 | AnyView(
23 | Text(text)
24 | .padding(8)
25 | .background(Color(hex: "#2DA15A"))
26 | .foregroundStyle(Color.white)
27 | .clipShape(Capsule())
28 | )
29 | }
30 |
31 | func createBackground() -> AnyView {
32 | AnyView(
33 | Image("con")
34 | .resizable()
35 | .aspectRatio(contentMode: .fill)
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/ThemeFactory/MuziThemeFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MuziThemeFactory.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MuziThemeFactory: ThemeFactory {
11 | func createOpponentChatBubble(text: String) -> AnyView {
12 | AnyView(
13 | Text(text)
14 | .padding(8)
15 | .background(Color.white)
16 | .foregroundStyle(Color.black)
17 | .clipShape(Capsule())
18 | .overlay(
19 | Capsule()
20 | .stroke(Color.black, lineWidth: 1.0)
21 | )
22 | )
23 | }
24 |
25 | func createMyChatBubble(text: String) -> AnyView {
26 | AnyView(
27 | Text(text)
28 | .padding(8)
29 | .background(Color(hex: "#FEF2B5"))
30 | .foregroundStyle(Color.black)
31 | .clipShape(Capsule())
32 | .overlay(
33 | Capsule()
34 | .stroke(Color.black, lineWidth: 1.0)
35 | )
36 | )
37 | }
38 |
39 | func createBackground() -> AnyView {
40 | AnyView(
41 | Color(hex: "#FFED71")
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/ThemeFactory/ThemeFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeFactory.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol ThemeFactory {
11 | func createOpponentChatBubble(text: String) -> AnyView
12 | func createMyChatBubble(text: String) -> AnyView
13 | func createBackground() -> AnyView
14 | }
15 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Example/AbstractFactory/Utils/Color+Hex.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Hex.swift
3 | // AbstractFactory
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Color {
11 | init(hex: String) {
12 | let scanner = Scanner(string: hex)
13 | _ = scanner.scanString("#")
14 |
15 | var rgb: UInt64 = 0
16 | scanner.scanHexInt64(&rgb)
17 |
18 | let r = Double((rgb >> 16) & 0xFF) / 255.0
19 | let g = Double((rgb >> 8) & 0xFF) / 255.0
20 | let b = Double((rgb >> 0) & 0xFF) / 255.0
21 | self.init(red: r, green: g, blue: b)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/README.md:
--------------------------------------------------------------------------------
1 | # Abstract Factory
2 |
3 | 추상 팩토리는 서로 관련 있거나 의존성이 있는 객체들의 그룹을을 생성하는 인터페이스를 말합니다.
4 |
5 | 역시 뭔소린지 모르겠죠?
6 |
7 | 그냥 객체를 생성하는 함수들을 여러개 모아둔 프로토콜을 얘기하는 겁니다.
8 | 하여간 이 디자인 패턴들은 이름이 거창해서 문제인 듯 합니다.
9 | 이름만 더 직관적이었어도 이해하기 훨씬 쉬웠을텐데 말이죠..
10 |
11 |
12 | 근데 이러면 "야 그럼 그냥 프로토콜 만들고 그거 구현하면 그거 추상 팩토리 사용했다고 할 수 있는거 아님?"
13 | 이라고 할 수 있는데요. 네 제가 그랬습니다.
14 |
15 |
16 | 일단 아닙니다. POP는 맞는데.. 이거 패턴 이름이 뭔가요?
17 | 추상 팩토리 입니다.
18 | 팩토리는 뭔가요? 공장이죠?
19 | 뭘 만들어줘야한다는 뜻 입니다.
20 | 그러니까 아래와 같은 추상 함수들로만 구상 되어있어야 하는거죠.
21 |
22 | ```Swift
23 | func createObject() -> Object
24 | ```
25 |
26 |
27 | 그럼 여기서 또 의문이 들겁니다.
28 | 이거.. 팩토리 메서드 아님?!
29 |
30 | 틀린말은 아닙니다.
31 | 다만 가장 중요한 차이점으로는 추상 팩토리의 경우 **서로 연관되거나 의존적인** 그러니까 일련된 객체들을 생성해주는 추상 함수들을 모아준 것을 의미합니다.
32 |
33 |
34 | 즉
35 |
36 | **팩토리 메서드**
37 |
38 | - 객체를 생성하는 구현을 서브 클래스에게 넘김
39 | - 여러개여도 상관 X
40 | - 일관되지 않아도 상관 X
41 |
42 |
43 |
44 | **추상 팩토리**
45 |
46 | - 객체를 생성하는 구현을 서브 클래스에게 넘김
47 | - 여러개이어야 함.
48 | - 왜냐하면 일관되어야 하니까 (일관이라는 단어는 하나의 것을 얘기할 때에는 적합하지 않음, 적어도 두 개 이상이 되어야 일관된다고 얘기할 수 있음.)
49 |
50 | 뭐 이런 차이가 있는 것이죠.
51 | 간혹 어떤 글에서는 팩토리 메서드가 하나의 객체 생성 함수만 있는거고 추상 팩토리가 여러개의 객체 생성 함수가 있는거다 이러는데 위 개념에 포함된 말은 맞는데 맞는 말은 아닙니다.
52 |
53 |
54 |
55 | ## IRL (In Real Life)
56 |
57 | 자 그럼 사용해 보아야겠죠?
58 | 예를 들어서 내 앱에 카카오톡 처럼 다양한 테마를 지원한다고 가정해볼게요.
59 |
60 | 그러면 다양한 테마에 따라서 챗 버블이나 다양한 UI 요소들을 달리 생성하는 팩토리를 만들어 본다고 할 때 아래와같이 프로토콜을 정의할 수 있겠죠?
61 |
62 | ```Swift
63 | protocol ThemeFactory {
64 | func createOpponentChatBubble(text: String) -> AnyView
65 | func createMyChatBubble(text: String) -> AnyView
66 | func createBackground() -> AnyView
67 | }
68 | ```
69 |
70 | 자 넘어가기전에 이게 추상 팩토리 패턴이라고 불릴 수 있는 이유를 봅시다.
71 | createOpponentChatBubble, createMyChatBubble, createBackground 모두 protocol을 통해서 구현을 서브클래스(또는 구조체)에게 넘기고있죠?
72 |
73 | 그리고 또 일련 된 AnyView 객체들을 생성해주고있죠? (만약 UIKit 이라면 UISwitch, UIView, UIButton 등 다양하게 있어도 일련 되었다고 말할 수 있음)
74 | 만약 쌩뚱맞게 createPizza 이런거 포함되어있으면 그 순간 추상 팩토리가 아니라 팩토리 메서드가 되겠죠?
75 |
76 | 그럼 이어서 구현을 해보겠습니다.
77 |
78 | ```Swift
79 | struct MuziThemeFactory: ThemeFactory {
80 | func createOpponentChatBubble(text: String) -> AnyView {
81 | AnyView(
82 | Text(text)
83 | .padding(8)
84 | .background(Color.white)
85 | .foregroundStyle(Color.black)
86 | .clipShape(Capsule())
87 | .overlay(
88 | Capsule()
89 | .stroke(Color.black, lineWidth: 1.0)
90 | )
91 | )
92 | }
93 |
94 | func createMyChatBubble(text: String) -> AnyView {
95 | AnyView(
96 | Text(text)
97 | .padding(8)
98 | .background(Color(hex: "#FEF2B5"))
99 | .foregroundStyle(Color.black)
100 | .clipShape(Capsule())
101 | .overlay(
102 | Capsule()
103 | .stroke(Color.black, lineWidth: 1.0)
104 | )
105 | )
106 | }
107 |
108 | func createBackground() -> AnyView {
109 | AnyView(
110 | Color(hex: "#FFED71")
111 | )
112 | }
113 | }
114 |
115 |
116 | // 위와 비슷한 방식으로 구현
117 | struct MuziThemeFactory: ThemeFactory { ... }
118 | struct ConThemeFactory: ThemeFactory { ... }
119 | ```
120 |
121 | 자 이렇게 만들면 사용자 입장에서는 아래와같이 사용할 수 있겠죠
122 |
123 | ```Swift
124 | struct ContentView: View {
125 |
126 | // MARK: - Properties
127 |
128 | enum Theme: String, CaseIterable {
129 | case muji
130 | case apeach
131 | case con
132 |
133 | var factory: ThemeFactory {
134 | switch self {
135 | case .muji:
136 | return MuziThemeFactory()
137 | case .apeach:
138 | return AppeachThemeFactory()
139 | case .con:
140 | return ConThemeFactory()
141 | }
142 | }
143 | }
144 |
145 | @State private var currentTheme: Theme = .muji
146 |
147 |
148 | // MARK: - Views
149 |
150 | var body: some View {
151 | VStack {
152 | HStack {
153 | currentTheme.factory.createOpponentChatBubble(text: "안녕하세요")
154 | Spacer()
155 | }
156 |
157 | HStack {
158 | Spacer()
159 | currentTheme.factory.createMyChatBubble(text: "안녕하세요☺️")
160 | }
161 |
162 | HStack {
163 | Spacer()
164 | currentTheme.factory.createMyChatBubble(text: "아래에서 segment에서 테마를 선택할 수 있어요")
165 | }
166 |
167 | Spacer()
168 |
169 | Picker("테마를 선택해 주세요", selection: $currentTheme) {
170 | ForEach(Theme.allCases, id: \.self) { theme in
171 | Text(theme.rawValue)
172 | }
173 | }
174 | .pickerStyle(.segmented)
175 | }
176 | .padding(12)
177 | .frame(maxWidth: .infinity, maxHeight: .infinity)
178 | .background(
179 | currentTheme.factory.createBackground()
180 | .ignoresSafeArea()
181 | )
182 | }
183 | }
184 | ```
185 |
186 | 이를 다이어그램으로 표현하자면 아래와같습니다.
187 |
188 | 
189 |
190 |
191 |
192 |
193 | 어떤가요? 이렇게 그룹화 된 추상화를 사용하게 되면 클라이언트 코드 간의 단단한 결합도 느슨하게 해주고 만약 새로운 테마를 추가한다고 하면 단순히 ThemeFactory 를 채택한 구현체를 만들어서 사용하면 되겠죠? 그러면서 자연스럽게 SRP, OCP도 챙길 수 있겠죠.
194 |
195 | 이처럼 어떤 제품군을 만들때 특히 추상 팩토리가 유용하게 사용됩니다.
196 |
197 |
198 |
199 | ## 실행 화면
200 |
201 | 
202 |
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/AbstractFactory/Resources/diagram.png
--------------------------------------------------------------------------------
/Creational patterns/AbstractFactory/Resources/simulator_preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/AbstractFactory/Resources/simulator_preview.gif
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/AlertBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertBuildㄷㄱ.swift
3 | // Builder
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol AlertBuilder {
11 | func setTitle(_ title: String) -> AlertBuilder
12 | func setMessage(_ message: String) -> AlertBuilder
13 | func addAction(title: String, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)?) -> AlertBuilder
14 | func build() -> UIAlertController
15 | }
16 |
17 | class CustomAlertBuilder: AlertBuilder {
18 | private var title: String?
19 | private var message: String?
20 | private var actions: [UIAlertAction] = []
21 |
22 | @discardableResult
23 | func setTitle(_ title: String) -> AlertBuilder {
24 | self.title = title
25 | return self
26 | }
27 |
28 | @discardableResult
29 | func setMessage(_ message: String) -> AlertBuilder {
30 | self.message = message
31 | return self
32 | }
33 |
34 | @discardableResult
35 | func addAction(title: String, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)?) -> AlertBuilder {
36 | let action = UIAlertAction(title: title, style: style, handler: handler)
37 | actions.append(action)
38 | return self
39 | }
40 |
41 | func build() -> UIAlertController {
42 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
43 | actions.forEach { alert.addAction($0) }
44 | return alert
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Builder
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Builder
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Example/Builder/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Builder
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class ViewController: UIViewController {
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | // Do any additional setup after loading the view.
15 | }
16 |
17 | @IBAction func didTapShowAlertButton(_ sender: Any) {
18 | let builder = CustomAlertBuilder()
19 | let alert = builder
20 | .setTitle("스위프트 디자인 패턴")
21 | .setMessage("스위프트 디자인패턴을 배워봅시다")
22 | .addAction(title: "취소", style: .cancel, handler: nil)
23 | .addAction(title: "설정으로 이동", style: .default) { _ in
24 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
25 | }
26 | .build()
27 |
28 | present(alert, animated: true)
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/README.md:
--------------------------------------------------------------------------------
1 | # Builder
2 |
3 | 빌더 패턴은 객체를 생성하는 단계를 여러개로 나누어서 관리할때 도움이 되는 디자인 패턴 입니다.
4 |
5 | 역시 정의 자체로는 이게 뭐하는 녀석인지 이해하기란 쉽지 않습니다.
6 | Before, After 예시로 이해를 도와보죠.
7 |
8 | 예를 들어 다음과 같이 Document 객체가 있다고 해볼게요.
9 | 이 객체에는 뭘 넣느냐에 따라서 다양한 결과가 나오겠죠?
10 |
11 | ```SWift
12 | // 생성 시 모든 매개변수를 명시해야 하며, 매개변수의 순서와 의미를 정확히 알아야 함
13 | let document = Document(title: "Swift Design Patterns",
14 | author: "John Doe",
15 | text: "Full text of the document",
16 | pageCount: 120,
17 | summary: "A brief summary of design patterns in Swift",
18 | keywords: ["design", "patterns", "swift"])
19 |
20 | ```
21 |
22 | 이렇게 유연성은 생기지만 객체 구조에 대해서 클라이언트가 모두 알아야하고 특히 optional 로 받아야하는 요소들에 대해서도 처리를 해줘야겠죠.
23 |
24 | 그럼 이거를 Builder 패턴을 적용해서 만들어 사용하는 모습을 봐볼까요?
25 |
26 | ```Swift
27 | let builder = DocumentBuilder()
28 | builder.setTitle("Swift Design Patterns")
29 | builder.setAuthor("John Doe")
30 | builder.setText("Full text of the document")
31 | builder.setPageCount(120)
32 | builder.setSummary("A brief summary of design patterns in Swift")
33 | builder.setKeywords(["design", "patterns", "swift"])
34 |
35 | let document = builder.build()
36 | ```
37 |
38 | 어떤가요? 훨씬 체계적이고 유연해 보이죠?
39 | 그리고 저 setSummary, setKeywords 같이 optional 한 요소들에 대해서는 생성자 처럼 기본값을 넣어준다거나 할 필요없이 애초에 호출하지 않으면 되는 것이죠.
40 |
41 | 게다가 setTitle, setAuthor 등 함수들을 protocol로 담아서 관리하면 클라이언트 입장에서는 추상 인터페이스만 보고있기 때문에 구현 코드는 어떻게 바뀌던 상관 없겠죠.
42 |
43 |
44 |
45 | ### Method Chaining
46 |
47 | 그리고 제가 생각하기에 builder의 꽃은 method chaining에 있다고 생각하는데 이를 구현하는 건 뒤에서 보고 일단 적용한 모습부터 보면 아래와 같습니다.
48 |
49 | ```Swift
50 | let builder = DocumentBuilder()
51 | let document = builder.setTitle("Swift Design Patterns")
52 | .setAuthor("John Doe")
53 | .setText("Full text of the document")
54 | .setPageCount(120)
55 | .setSummary("A brief summary of design patterns in Swift")
56 | .setKeywords(["design", "patterns", "swift"])
57 | .build()
58 | ```
59 |
60 | 주먹구구식으로 생성자에 다 때려박는거 보다 훨 섹시하지 않나요?
61 |
62 | 이렇게 생성하려는 객체가 매우 복잡할 경우 (특히 많은 매개변수를 필요로 하거나, 객체 생성 과정이 여러 단계를 포함하는 경우)에 유용하게 사용됩니다.
63 |
64 |
65 |
66 | ## IRL (In Real Life)
67 |
68 | 자 그럼 써먹어봐야겠죠?
69 |
70 | 빌더 패턴을 이용해서 기존 Aelrt보다 훨 사용하기 쉽고 가독성 좋은 Alert를 만들어보겠습니다.
71 |
72 | 일단 빌더 프로토콜을 하나 만들어 주겠습니다.
73 |
74 | ```Swift
75 | protocol AlertBuilder {
76 | func setTitle(_ title: String) -> AlertBuilder
77 | func setMessage(_ message: String) -> AlertBuilder
78 | func addAction(title: String, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)?) -> AlertBuilder
79 | func build() -> UIAlertController
80 | }
81 |
82 | ```
83 |
84 | 프로토콜을 보면 각 함수들이 빌더 자기자신을 리턴하는 것을 볼 수 있습니다.
85 |
86 | **바로 이게 연쇄적으로 다른 빌더의 함수를 호출할 수 있게 도와주는 메소드 체이닝의 핵심이 되겠습니다.**
87 |
88 | 아마 사용하는 모습까지 보시면 왜 자기자신을 리턴하는 이유에 대해서 더 이해하기 쉬우실 겁니다.
89 |
90 | 그럼 이어서 이 프로토콜을 구현해줘보죠.
91 |
92 | ```Swift
93 | class CustomAlertBuilder: AlertBuilder {
94 | private var title: String?
95 | private var message: String?
96 | private var actions: [UIAlertAction] = []
97 |
98 | @discardableResult
99 | func setTitle(_ title: String) -> AlertBuilder {
100 | self.title = title
101 | return self
102 | }
103 |
104 | @discardableResult
105 | func setMessage(_ message: String) -> AlertBuilder {
106 | self.message = message
107 | return self
108 | }
109 |
110 | @discardableResult
111 | func addAction(title: String, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)?) -> AlertBuilder {
112 | let action = UIAlertAction(title: title, style: style, handler: handler)
113 | actions.append(action)
114 | return self
115 | }
116 |
117 | func build() -> UIAlertController {
118 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
119 | actions.forEach { alert.addAction($0) }
120 | return alert
121 | }
122 | }
123 |
124 |
125 | ```
126 |
127 | 이렇게 빌더의 각 함수는 우리가 필요한 객체의 수정을 도와줍니다.
128 |
129 | 최종적으로 이렇게 만든 빌더를 사용하는 모습을 보겠습니다.
130 |
131 | ```Swift
132 | let builder = CustomAlertBuilder()
133 | let alert = builder
134 | .setTitle("스위프트 디자인 패턴")
135 | .setMessage("스위프트 디자인패턴을 배워봅시다")
136 | .addAction(title: "취소", style: .cancel, handler: nil)
137 | .addAction(title: "설정으로 이동", style: .default) { _ in
138 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
139 | }
140 | .build()
141 |
142 | present(alert, animated: true)
143 | ```
144 |
145 | 자 어떤가요? 깔끔하죠?
146 |
147 | 이를 다이어그램으로 표현하면 아래와같습니다.
148 |
149 | 
150 |
151 |
152 |
153 |
154 | 사실 아래처럼 전통적으로 UIAlertController 생성해주는 거랑 별 안 다르지 않음? 이라고 생각하실 수 있습니다.
155 |
156 | ```Swift
157 | let alert = UIAlertController(title: "스위프트 디자인 패턴", message: "스위프트 디자인패턴을 배워봅시다", preferredStyle: .alert)
158 | alert.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil))
159 | alert.addAction(UIAlertAction(title: "설정으로 이동", style: .default, handler: { _ in
160 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
161 | }))
162 | ```
163 |
164 | 네, 예시가 UIAlertController라 그렇지 만약에 생성자에 10개 이상의 값을 넘겨줘야한다면요?
165 | 사용하지 않는 객체에 옵셔널 처리 또는 여러개의 생성자를 만드는 데 하루를 꼬박 보낼지도 모릅니다
166 |
167 | 이처럼 생성자를 이용해서 클라이언트로부터 다양한 객체를 생성하게 하는데 생성자가 좀 비대하다? 그럼 Builder 패턴의 도입을 적극 고려해보세요!
168 |
169 | 그리고 빌더패턴은 기존에 객체를 생성하는 방식보다 훨씬 가독성이 좋아진다는 이점도 있습니다.
170 | 특히 클로져를 가진 생성자 프로퍼티가 많다면요!
171 |
172 |
173 |
174 | ## 실행 화면
175 |
176 | 
177 |
--------------------------------------------------------------------------------
/Creational patterns/Builder/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/Builder/Resources/diagram.png
--------------------------------------------------------------------------------
/Creational patterns/Builder/Resources/simulator_preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Creational patterns/Builder/Resources/simulator_preview.gif
--------------------------------------------------------------------------------
/Creational patterns/FactoryMethod/README.md:
--------------------------------------------------------------------------------
1 | # Factory Method
2 |
3 | Factory Method는 Factory pattern과 더불어 많이 거론됩니다. 구글에서 Factory Mehtod에 대해서 검색해보면 Factory pattern과 함께 혼용되어 사용하는 경우도 종종 보이더라고요.
4 |
5 | **하지만 엄연히 이 둘은 다릅니다.**
6 |
7 | 팩토리 패턴은 객체를 생성하는 자주 변경되는 어떤 로직을 클래스나 구조체로(또는 함수로) 떼어내어 사용하는 것을 말합니다.
8 |
9 | 하지만 팩토리 메서드는 객체를 생성하는 것을 따로 분리된 클래스나 구조체로 캡슐화를 하는 것이 아니라 **객체를 생성하는 함수를 서브클래스에게 위임**하는 것을 의미합니다.
10 |
11 | 그냥 서로 다른것이라 생각하시는 게 마음 편하실지도 모릅니다😅
12 |
13 | 자 그럼 팩토리 메서드가 **"객체 생성을 서브클래스에게 위임한다~"** 이랬죠? 바로 코드로 보시죠.
14 |
15 | 아래와 같이 VideoModifier가 있다고 가정해 보겠습니다.
16 |
17 | ```swift
18 | protocol VideoModifier {
19 | func addWaterMark(_ video: Video) -> Video
20 | func generateWaterMarkLayer() -> VideoLayer
21 | }
22 | ```
23 |
24 | 그리고 addWaterMark 함수만 구현해 줍시다.
25 |
26 | ```swift
27 | extension VideoModifier {
28 | func addWaterMark(_ video: Video) -> Video {
29 | let waterMarkLayer = generateWaterMarkLayer()
30 | let render = video.renderMode()
31 | render.addLayer(waterMarkLayer)
32 | let video = render.finishRender()
33 | return video
34 | }
35 | }
36 | ```
37 |
38 | 구현체를 잘 보면 generateWaterMarkLayer()를 사용한 것을 볼 수 있습니다.
39 |
40 | 그리고 우리는 아직 이걸 구현해주진 않았죠.
41 |
42 | 그럼 언제 구현하냐?
43 |
44 | ### 네!! 팩토리 메서드의 본질인..! 서브클래스에서 구현해주면 됩니다
45 |
46 | 만약 VideoModifier 프로토콜을 채택한 ShortFormVideoModifier와 ClassicFormVideoModifier를 구현해주고싶다고 해보겠습니다.
47 |
48 | 두 비디오의 스타일이 다르니 워터마크 레이어도 각자 다르게 생성해줘야겠죠?
49 |
50 | 일단 만들어봅시다.
51 |
52 | ```swift
53 | class ShortFormVideoModifier: VideoModifier {
54 | func generateWaterMarkLayer() -> VideoLayer {
55 | // 숏폼 비디오에 맞는 워터마크 생성...
56 | return videoLayer
57 | }
58 | }
59 |
60 | class ClassicFormVideoModifier: VideoModifier {
61 | func generateWaterMarkLayer() -> VideoLayer {
62 | // 전통적인 비디오에 맞는 워터마크 생성...
63 | return videoLayer
64 | }
65 | }
66 | ```
67 |
68 | 자 이렇게 VideoModifier라는 프로토콜을 채택하는데 우린 이미 extension을 통해서 addWaterMark는 구현하였고 그 안에서 사용하는 generateWaterMarkLayer 함수만 서브클래스에게 위임해서 각 ShortFormVideoModifier, ClassicFormVideoModifier에서 generateWaterMarkLayer를 구현해서 각기 다른 스타일로 VideoLayer를 생성해주는 모습을 볼 수 있습니다.
69 |
70 | 이렇게 객체를 생성하는 메서드를 추상화시켜 만들어주면 확장에 굉장히 유연한 구조를 가져갈 수 있습니다.
71 |
72 | 팩토리 메서드와 팩토리 패턴.. 이름 때문에 이 둘을 엮어서 보려는 경향이 생기기 마련인데
73 |
74 | 위키에서도 그러는데 애초에 팩토리 메서드는 이름을 좀 잘못지은 것 같다고 얘기하더군요..
75 |
76 | 그래서 서로 그냥 다른 디자인 패턴이라고 보는게 편할 것 같습니다.
77 |
78 | 이상!
79 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "snapkit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/SnapKit/SnapKit",
7 | "state" : {
8 | "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
9 | "version" : "5.7.1"
10 | }
11 | },
12 | {
13 | "identity" : "then",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/devxoul/Then",
16 | "state" : {
17 | "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a",
18 | "version" : "3.0.0"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Cells/CellFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CellFactory.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 |
10 | final class CellFactory {
11 | func createCell(for item: SettingItem, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
12 | if type(of: item) == ToggleItem.self {
13 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ToggleCell.id, for: indexPath) as! ToggleCell
14 | cell.configure(title: item.title)
15 | return cell
16 |
17 | } else {
18 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NavigationCell.id, for: indexPath) as! NavigationCell
19 | cell.configure(title: item.title)
20 | return cell
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Cells/NavigationCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationCell.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 | import Then
11 |
12 | final class NavigationCell: UICollectionViewCell {
13 |
14 | static let id = String(describing: NavigationCell.self)
15 |
16 | // MARK: - UI Components
17 |
18 | private let titleLabel = UILabel().then {
19 | $0.textColor = .black
20 | $0.font = UIFont.systemFont(ofSize: 16)
21 | }
22 |
23 | private let chevronImageView = UIImageView().then {
24 | $0.image = UIImage(systemName: "chevron.right") // SF Symbols를 사용합니다. 커스텀 이미지로 변경 가능
25 | $0.contentMode = .scaleAspectFit
26 | $0.tintColor = .gray // 이미지 색상 조정
27 | }
28 |
29 |
30 | // MARK: - LifeCycle
31 |
32 | override init(frame: CGRect) {
33 | super.init(frame: frame)
34 | setup()
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 |
42 | // MARK: - Public
43 |
44 | public func configure(title: String) {
45 | titleLabel.text = title
46 | }
47 |
48 |
49 | // MARK: - Private
50 |
51 | private func setup() {
52 | setupView()
53 | layoutView()
54 | }
55 |
56 | private func setupView() {
57 | contentView.addSubview(titleLabel)
58 | contentView.addSubview(chevronImageView)
59 | }
60 |
61 | private func layoutView() {
62 | titleLabel.snp.makeConstraints { make in
63 | make.leading.equalToSuperview().offset(10)
64 | make.centerY.equalToSuperview()
65 | }
66 |
67 | chevronImageView.snp.makeConstraints { make in
68 | make.trailing.equalToSuperview().offset(-10)
69 | make.centerY.equalToSuperview()
70 | make.width.equalTo(20) // Chevron 이미지의 너비 설정
71 | make.height.equalTo(20) // Chevron 이미지의 높이 설정
72 | }
73 | }
74 | }
75 |
76 | #Preview {
77 | let cell = NavigationCell(frame: .init(x: 0, y: 0, width: 220, height: 80))
78 | cell.configure(title: "Title")
79 | return cell
80 | }
81 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Cells/ToggleCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleCell.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 | import Then
11 |
12 | final class ToggleCell: UICollectionViewCell {
13 |
14 | static let id = String(describing: ToggleCell.self)
15 |
16 | // MARK: - UI Components
17 | private let titleLabel = UILabel().then {
18 | $0.textColor = .black
19 | $0.font = UIFont.systemFont(ofSize: 16)
20 | }
21 |
22 | private let toggleButton = UISwitch().then {
23 | $0.isOn = false
24 | }
25 |
26 |
27 | // MARK: - LifeCycle
28 |
29 | override init(frame: CGRect) {
30 | super.init(frame: frame)
31 | setup()
32 | }
33 |
34 | required init?(coder: NSCoder) {
35 | fatalError("init(coder:) has not been implemented")
36 | }
37 |
38 |
39 | // MARK: - Public
40 |
41 | public func configure(title: String) {
42 | titleLabel.text = title
43 | }
44 |
45 |
46 | // MARK: - Private
47 |
48 | private func setup() {
49 | setupView()
50 | layoutView()
51 | }
52 |
53 | private func setupView() {
54 | contentView.addSubview(titleLabel)
55 | contentView.addSubview(toggleButton)
56 | }
57 |
58 | private func layoutView() {
59 | titleLabel.snp.makeConstraints { make in
60 | make.leading.equalToSuperview().offset(10)
61 | make.centerY.equalToSuperview()
62 | }
63 |
64 | toggleButton.snp.makeConstraints { make in
65 | make.trailing.equalToSuperview().offset(-10)
66 | make.centerY.equalToSuperview()
67 | }
68 | }
69 | }
70 |
71 | #Preview {
72 | let cell = ToggleCell(frame: .init(x: 0, y: 0, width: 240, height: 80))
73 | cell.configure(title: "Title")
74 | return cell
75 | }
76 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Items/NavigationItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationItem.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct NavigationItem: SettingItem {
11 | var title: String
12 | }
13 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Items/SettingItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingItem.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol SettingItem {
11 | var title: String { get set }
12 | }
13 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/Items/ToggleItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleItem.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ToggleItem: SettingItem {
11 | var title: String
12 | }
13 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let scene = (scene as? UIWindowScene) else { return }
20 | let window = UIWindow(windowScene: scene)
21 | window.rootViewController = ViewController()
22 | window.makeKeyAndVisible()
23 | self.window = window
24 | }
25 |
26 | func sceneDidDisconnect(_ scene: UIScene) {
27 | // Called as the scene is being released by the system.
28 | // This occurs shortly after the scene enters the background, or when its session is discarded.
29 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
30 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
31 | }
32 |
33 | func sceneDidBecomeActive(_ scene: UIScene) {
34 | // Called when the scene has moved from an inactive state to an active state.
35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
36 | }
37 |
38 | func sceneWillResignActive(_ scene: UIScene) {
39 | // Called when the scene will move from an active state to an inactive state.
40 | // This may occur due to temporary interruptions (ex. an incoming phone call).
41 | }
42 |
43 | func sceneWillEnterForeground(_ scene: UIScene) {
44 | // Called as the scene transitions from the background to the foreground.
45 | // Use this method to undo the changes made on entering the background.
46 | }
47 |
48 | func sceneDidEnterBackground(_ scene: UIScene) {
49 | // Called as the scene transitions from the foreground to the background.
50 | // Use this method to save data, release shared resources, and store enough scene-specific state information
51 | // to restore the scene back to its current state.
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/Example/FactoryPattern/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // FactoryPattern
4 | //
5 | // Created by 이승기 on 2/26/24.
6 | //
7 |
8 | import UIKit
9 | import Then
10 | import SnapKit
11 |
12 | class ViewController: UIViewController {
13 |
14 | let settingItems: [SettingItem] = [
15 | ToggleItem(title: "Night Mode"),
16 | ToggleItem(title: "Sound & Haptics"),
17 | ToggleItem(title: "Privacy"),
18 | ToggleItem(title: "Bluetooth"),
19 | NavigationItem(title: "Notifications"),
20 | NavigationItem(title: "Display & Brightness"),
21 | NavigationItem(title: "General"),
22 | NavigationItem(title: "Battery"),
23 | NavigationItem(title: "WIFI"),
24 | ToggleItem(title: "Accessibility")
25 | ]
26 |
27 |
28 | let cellFactory = CellFactory()
29 | private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
30 |
31 |
32 | // MARK: - LifeCycle
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 | setup()
37 | }
38 |
39 |
40 | // MARK: - Methods
41 |
42 | private func setup() {
43 | setupView()
44 | setupLayout()
45 | }
46 |
47 | private func setupView() {
48 | view.addSubview(collectionView)
49 | collectionView.delegate = self
50 | collectionView.dataSource = self
51 | collectionView.register(ToggleCell.self, forCellWithReuseIdentifier: ToggleCell.id)
52 | collectionView.register(NavigationCell.self, forCellWithReuseIdentifier: NavigationCell.id)
53 | }
54 |
55 | private func setupLayout() {
56 | collectionView.snp.makeConstraints {
57 | $0.edges.equalToSuperview()
58 | }
59 | }
60 |
61 | private func createLayout() -> UICollectionViewCompositionalLayout {
62 | let itemSize = NSCollectionLayoutSize(
63 | widthDimension: .fractionalWidth(1.0),
64 | heightDimension: .absolute(60)
65 | )
66 | let item = NSCollectionLayoutItem(layoutSize: itemSize)
67 |
68 | let groupSize = NSCollectionLayoutSize(
69 | widthDimension: .fractionalWidth(1.0),
70 | heightDimension: .absolute(60)
71 | )
72 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
73 |
74 | let section = NSCollectionLayoutSection(group: group)
75 | return UICollectionViewCompositionalLayout(section: section)
76 | }
77 | }
78 |
79 |
80 | // MARK: - CollectionView Delegates
81 |
82 | extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
83 |
84 | func numberOfSections(in collectionView: UICollectionView) -> Int {
85 | return 1
86 | }
87 |
88 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
89 | return settingItems.count
90 | }
91 |
92 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
93 | let item = settingItems[indexPath.row]
94 | return cellFactory.createCell(for: item, collectionView: collectionView, indexPath: indexPath)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Creational patterns/FactoryPattern/README.md:
--------------------------------------------------------------------------------
1 | # Factory Pattern
2 |
3 | Factory 패턴은 자주 변경되는 **객체 생성 부분을** 별도의 클래스(팩토리)에게 위임하는 것을 말합니다. 이는 곧 자주 변경되는 곳의 캡슐화를 의미하기도 합니다.
4 | 정말 쉽게 설명하자면 그냥 if else 나 switch 처럼 분기처리 되는 부분을 통해서 어떤 객체를 생성하는 로직을 그냥 클래스나 구조체로 빼는 것이 Factory 패턴입니다.
5 | 정말 간단하죠? 괜히 팩토리.. 이래서 지레 겁먹지 말자구요!!
6 |
7 |
8 |
9 | ## IRL (In Real Life)
10 |
11 | 유용하게 사용할 수 있는 예시로 UICollectionView의 cell을 생성하는 부분을 얘기해볼 수 있을것 같습니다.
12 | UIcollectionView에서 다양한 종류의 셀을 사용하는 경우는 흔하게 접하는 경우이죠.
13 |
14 | 먼저 Factory 패턴을 사용하지 않은 상태로 Cell을 dequeue할 때의 예시를 보겠습니다.
15 |
16 | **셀 클래스 정의**
17 |
18 | ```Swift
19 | class ToggleCell: UICollectionViewCell {
20 | // 토글 셀 구성 로직
21 | }
22 |
23 | class NavigataionCell: UIColectionViewCell {
24 | // 내비게이션 셀 구성 로직
25 | }
26 |
27 | ```
28 |
29 | **UICollectionViewDataSource 구현**
30 |
31 | ```Swift
32 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
33 | let item = settingItems[indexPath.row]
34 | if type(of: item) == ToggleItem.self {
35 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ToggleCell.id, for: indexPath) as! ToggleCell
36 | cell.configure(title: item.title)
37 | return cell
38 |
39 | } else {
40 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NavigationCell.id, for: indexPath) as! NavigationCell
41 | cell.configure(title: item.title)
42 | return cell
43 | }
44 | }
45 | ```
46 |
47 | 만약 이런 상황에서 새로운 셀을 추가한다고 해봅시다. 그렇게 되면 collectionView delegate에서 분기를 하나 더 따는 식의 변경에 닫혀있지 않은 코드가 되어버립니다.
48 | 즉 기존 코드를 변경 해야만 새로운 기능을 추가할 수 있다는 의미입니다.
49 |
50 | 바로 이런 경우에 팩토리 패턴을 적용할 수 있겠습니다.
51 | 바로 팩토리 패턴을 적용해 보면서 얘기 나눠보죠!
52 |
53 | **셀 팩토리 클래스**
54 |
55 | ```Swift
56 | final class CellFactory {
57 | func createCell(for item: SettingItem, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
58 | if type(of: item) == ToggleItem.self {
59 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ToggleCell.id, for: indexPath) as! ToggleCell
60 | cell.configure(title: item.title)
61 | return cell
62 |
63 | } else {
64 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NavigationCell.id, for: indexPath) as! NavigationCell
65 | cell.configure(title: item.title)
66 | return cell
67 | }
68 | }
69 | }
70 | ```
71 |
72 |
73 |
74 | **UICollectionView에서 팩토리 사용**
75 |
76 | ```Swift
77 | extension ViewController: UIViewController, UICollectionViewDataSource {
78 | ...
79 |
80 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
81 | let item = settingItems[indexPath.row]
82 | return cellFactory.createCell(for: item, collectionView: collectionView, indexPath: indexPath)
83 | }
84 |
85 | ...
86 | }
87 | ```
88 |
89 |
90 |
91 | 자 보면 그냥 CellFactory에서 CellType에 따라서 다른 cell을 dequeue 해주는게 전부 입니다!
92 | 진짜 별거 없죠? 진짜 이게 끝이에요..
93 | 그리고 createCell 같은 경우에 static 함수로 사용해도 전혀 무방하고 그렇게 사용하는 경우도 종종 있답니다.
94 |
95 | 그냥 다른 클래스로 로직을 빼준건데 뭔 이점이 있냐?
96 | 약간 정석적으로 얘기 해보자면
97 |
98 | 객체 생성을 중앙화 시켜 하나의 팩토리 클래스에서 집중적으로 관리할 수 있다는 이점이 있겠습니다.
99 | 또 비슷한 말이지만 객체 생성에 필요한 로직과 정보를 팩토리 클래스 내부에 숨겨 클라이언트 코드는 팩토리 클래스의 메서드를 호출해서 객체를 요청하기만 하면 됩니다.
100 | 가장 중요한 것으로는 새로운 타입의 객체를 추가하거나 생성 방식을 변경하고자 할 때 픽토리 클래스만 수정하면 되기 때문에 유지보수 측면에서 이점을 얻게되겠죠.
101 |
102 |
103 |
104 | ## 마치며
105 |
106 | 이와 비슷한 사례로 CompositionalLayout을 생성할 때에도 유용하게 사용할 수 있습니다.
107 | 여러분의 코드에서도 이런 로직이 있다면 팩토리로 캡슐화 시켜보세요~~!
108 |
--------------------------------------------------------------------------------
/Creational patterns/Singleton/README.md:
--------------------------------------------------------------------------------
1 | # Singleton
2 |
3 | 싱글톤은 객체의 인스턴스를 하나만 생성하도록 도와주는 디자인 패턴 입니다.
4 | 또한 전역적으로의 접근도 제공하게 됩니다.
5 |
6 | 왜 객체가 하나만 필요한데?
7 |
8 | 예를들어 객체가 두 개 이상일 때 예상하지 않은 결과를 리턴한다거나 자원을 불필요하게 잡아먹는등 객체를 아토믹하게 관리했을 때 더 좋은 결과를 낳는 경우가
9 | 프로그래밍을 하다보면 있을 수 있습니다.
10 | 이런 경우에 싱글톤을 유용하게 사용할 수 있죠.
11 |
12 | ## 구현 방법
13 |
14 | 싱글톤을 구현하는 방법은 간단합니다.
15 |
16 | ```Swift
17 | class Singleton {
18 | static let shared = SingleTon()
19 | private init() { }
20 | }
21 | ```
22 |
23 | 위와같이 싱글톤 클래스를 하나 만들어주고 전역적으로 접근할 수 있는 유일무이한 싱글톤 인스턴스 객체를 할당합니다.
24 | (전통적으로 shared 라는 이름을 많이 사용하곤 합니다.)
25 | 그리고 생성자를 private으로 감싸는 것으로 싱글톤 완성 입니다!
26 |
27 | 여기에서 class 를 사용해야하는 이유가 있습니다.
28 | struct로 싱글톤 객체를 관리하게되면 싱글톤 객체를 다른 변수에 할당하고 값을 수정하게 되면 또는 그냥 할당하는 것 만으로도 새로운 싱글톤 객체가 생성되어버리기 때문에
29 | 싱글톤의 의미가 퇴색되어버립니다.
30 |
31 | 따라서 싱글톤을 만든다면 무조건 class로 만들어야 합니다.
32 |
33 | ## 지연 생성
34 |
35 | 싱글톤은 또한 객체를 지연하여 생성할 수 있다는 장점이 있습니다.
36 | 만약 객체를 초기화하는데 시간이 오래 걸린다거나 객체가 많은 자원을 잡아먹는 경우에 지연생성하는 것이 의미가 있겠습니다.
37 |
38 | 이는 전역적으로 변수를 아토믹하게 사용하는것의 한계점을 해결해줄 수 있습니다.
39 |
40 | Swift에서는 다른 언어들처럼 별다른 뻘짓을 안 해줘도 위에서 보여드린대로
41 |
42 | ```Swift
43 | static let shared = SingleTon()
44 | ```
45 |
46 | 이렇게만 해주면 알아서 지연로딩 됩니다.
47 | 참 편하죠?
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift DesignPattern IRL
2 |
3 | 안녕하세요, 왈라비 입니다.
4 |
5 | 다들 Swift와 좀 친해지셨다면 디자인 패턴에 관해서 한 번씩은 들어보셨을 것이라 생각합니다.
6 |
7 | 저 또한 그랬는데요. 그러면서 자연스럽게 디자인 패턴이란 것에 관심이 생기게 되었고, 무작정 책을 구매하여 읽게 되었습니다.
8 | 또한 다양한 블로그들도 많이 보았죠.
9 |
10 | 하지만 보통 책에서는 Java를 기반으로 설명하기도 하고 많은 블로그에서는 개념적인 예시를 가지고 설명하다보니 쉽게 와닿지 않았습니다.
11 |
12 | 그렇게 항상 다양한 패턴들에 대해서 학습하면서 뭔가 이해는 되는데.. 이 멋진 패턴들을 내가 발 담구고 있는 Swift(특히 iOS 개발)에서 어떻게 응용하면 좋을지에 대해 가려운 부분들이 생겨났고,
13 | 그런 가려운 부분들을 시원하게 긁어주는 자료가 많이 없다고 생각이 들어서 이렇게 직접 작성하게 되었습니다.
14 |
15 |
16 |
17 | ## 내가 생각하는 디자인 패턴
18 |
19 | 디자인 패턴은 말 그대로 패턴을 의미합니다.
20 |
21 | 즉 많은 개발자들이 코드를 작성하는 과정에서 발견한 자연스러운 해결 방식의 반복적 양상을 얘기하는데요, 이런 양상들에 거창한 이름을 붙인것이 디자인 패턴이라 생각합니다.
22 |
23 | 따라서 우리가 디자인 패턴을 학습하는 것은 맹목적인 따라하기가 아니라, 이미 발견된 지식을 통해서 유사한 문제에 더 빠르게 접근하고 해결하는 데에 초점을 맞추는 것에 의미가 있다고 생각합니다.
24 |
25 |
26 |
27 | ## 구성
28 |
29 | 구성은 Swift를 이용한 디자인 패턴 설명(ReadMe)과 예시 프로젝트를 담고있습니다.
30 |
31 | 다만 예시 프로젝트 같은 경우에 너무 설명을 위해 어거지스럽게 작성하지 않았나 싶은 부분에 대해서는 이해의 혼동을 피하기 위해 예시 프로젝드 없이 설명만 첨부 하였음을 참고 부탁드립니다!
32 |
33 | 해당 디자인 패턴을 사용하는 데에 있어 납득할만한 예시 프로젝트가 있다면 자유롭게 PR 날려주세요 🫰
34 |
35 |
36 |
37 | ### 행동 패턴
38 |
39 | | 디자인 패턴 | 설명 | 예시 프로젝트 |
40 | | ---------------------- | ---- | ------------ |
41 | | Observer | | |
42 | | Strategy | ✅ | ✅ |
43 | | Command | ✅ | ✅ |
44 | | Iterator | ✅ | ✅ |
45 | | State | ✅ | ✅ |
46 | | Visitor | ✅ | ✅ |
47 | | Memento | ✅ | ✅ |
48 |
49 | ### 구조 패턴
50 |
51 | | 디자인 패턴 | 설명 | 예시 프로젝트 |
52 | | ----------- | ---- | ------------ |
53 | | Adapter | ✅ | ✅ |
54 | | Composite | ✅ | ✅ |
55 | | ProtectionProxy | ✅ | ✅ |
56 | | VirtualProxy | ✅ | |
57 | | CachingProxy | ✅ | |
58 | | Facade | ✅ | |
59 | | Bridge | ✅ | ✅ |
60 | | Decorator | | |
61 |
62 | ### 생성 패턴
63 |
64 | | 디자인 패턴 | 설명 | 예시 프로젝트 |
65 | | ----------------- | ---- | ------------ |
66 | | Singleton | ✅ | |
67 | | Factory | ✅ | ✅ |
68 | | Factory Method | ✅ | |
69 | | Abstract Factory | ✅ | ✅ |
70 | | Builder | ✅ | ✅ |
71 | | Prototype | | |
72 |
--------------------------------------------------------------------------------
/Structural patterns/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/.gitkeep
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Adapters/AppleMapAdapter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleMapAdapter.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 |
11 | import MapKit
12 |
13 | final class AppleMapAdapter: MapService {
14 |
15 | private let mapView = MKMapView()
16 |
17 | func draw(in viewController: UIViewController) {
18 | viewController.view.addSubview(mapView)
19 | mapView.snp.makeConstraints {
20 | $0.edges.equalToSuperview()
21 | }
22 | }
23 |
24 | func moveCamera(lat: Double, lng: Double) {
25 | let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lng),
26 | latitudinalMeters: 1000, longitudinalMeters: 1000)
27 | mapView.setRegion(region, animated: true)
28 | }
29 |
30 | func addMarker(lat: Double, lng: Double) {
31 | let annotation = MKPointAnnotation()
32 | annotation.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
33 | annotation.title = "부산"
34 | annotation.subtitle = "떠나자 부산으로!"
35 |
36 | mapView.addAnnotation(annotation)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Adapters/MapService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapService.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol MapService {
11 | func draw(in viewController: UIViewController)
12 | func moveCamera(lat: Double, lng: Double)
13 | func addMarker(lat: Double, lng: Double)
14 | }
15 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Adapters/NaverMapAdapter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NaverMapAdapter.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 |
11 | import NMapsMap
12 |
13 | final class NaverMapAdapter: MapService {
14 |
15 | private let mapView = NMFMapView()
16 |
17 | func draw(in viewController: UIViewController) {
18 | viewController.view.addSubview(mapView)
19 | mapView.snp.makeConstraints {
20 | $0.edges.equalToSuperview()
21 | }
22 | }
23 |
24 | func moveCamera(lat: Double, lng: Double) {
25 | let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng), zoomTo: 9)
26 | cameraUpdate.animation = .easeIn
27 | mapView.moveCamera(cameraUpdate)
28 | }
29 |
30 | func addMarker(lat: Double, lng: Double) {
31 | let marker = NMFMarker()
32 | marker.position = NMGLatLng(lat: lat, lng: lng)
33 | marker.mapView = mapView
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13 | return true
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NMFClientId
6 | p44azytlwp
7 | UIApplicationSceneManifest
8 |
9 | UIApplicationSupportsMultipleScenes
10 |
11 | UISceneConfigurations
12 |
13 | UIWindowSceneSessionRoleApplication
14 |
15 |
16 | UISceneConfigurationName
17 | Default Configuration
18 | UISceneDelegateClassName
19 | $(PRODUCT_MODULE_NAME).SceneDelegate
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/MapViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapViewController.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 | import SnapKit
10 | import Then
11 |
12 | class MapViewController: UIViewController {
13 |
14 | private let moveButton = UIButton().then {
15 | $0.setTitle("🚢 부산으로 이동", for: .normal)
16 | $0.backgroundColor = .white
17 | $0.setTitleColor(.black, for: .normal)
18 | $0.layer.cornerRadius = 10
19 | }
20 |
21 | private let addMarkerButton = UIButton().then {
22 | $0.setTitle("📍 마커 추가", for: .normal)
23 | $0.backgroundColor = .black
24 | $0.layer.cornerRadius = 10
25 | }
26 |
27 | private let mapService: MapService // 다른 MapService를 주입 받으면 별다른 작업 없이 그걸 그대로 사용 가능
28 |
29 | init(mapService: MapService) {
30 | self.mapService = mapService
31 | super.init(nibName: nil, bundle: nil)
32 | }
33 |
34 | required init?(coder: NSCoder) {
35 | fatalError("init(coder:) has not been implemented")
36 | }
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 | mapService.draw(in: self)
41 | setupView()
42 | setupAction()
43 | }
44 |
45 | private func setupView() {
46 | view.addSubview(moveButton)
47 | moveButton.snp.makeConstraints {
48 | $0.bottom.equalToSuperview().inset(40)
49 | $0.left.right.equalToSuperview().inset(20)
50 | $0.height.equalTo(48)
51 | }
52 | view.addSubview(addMarkerButton)
53 | addMarkerButton.snp.makeConstraints {
54 | $0.bottom.equalTo(moveButton.snp.top).offset(-12)
55 | $0.left.right.equalToSuperview().inset(20)
56 | $0.height.equalTo(48)
57 | }
58 | }
59 |
60 | private func setupAction() {
61 | let lat: Double = 35.166668
62 | let lng: Double = 129.066666
63 |
64 | // Move button Action
65 | moveButton.addAction(.init(handler: { [weak self] _ in
66 | self?.mapService.moveCamera(lat: lat, lng: lng)
67 | }), for: .touchUpInside)
68 |
69 | // Add marker button action
70 | addMarkerButton.addAction(.init(handler: { [weak self] _ in
71 | self?.mapService.addMarker(lat: lat, lng: lng)
72 | }), for: .touchUpInside)
73 | }
74 | }
75 |
76 | #Preview {
77 | MapViewController(mapService: AppleMapAdapter()) // 이걸 AppleMapAdapter로 변경해서도 프리뷰 빌드해 보세요!
78 | }
79 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Adapter/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Adapter
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | guard let scene = (scene as? UIWindowScene) else { return }
17 | let window = UIWindow(windowScene: scene)
18 | window.rootViewController = MapViewController(mapService: NaverMapAdapter())
19 | window.makeKeyAndVisible()
20 | self.window = window
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'Adapter' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | pod 'NMapsMap'
9 |
10 | pod 'SnapKit'
11 | pod 'Then'
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - NMapsGeometry (1.0.1)
3 | - NMapsMap (3.17.0):
4 | - NMapsGeometry
5 | - SnapKit (5.7.1)
6 | - Then (3.0.0)
7 |
8 | DEPENDENCIES:
9 | - NMapsMap
10 | - SnapKit
11 | - Then
12 |
13 | SPEC REPOS:
14 | trunk:
15 | - NMapsGeometry
16 | - NMapsMap
17 | - SnapKit
18 | - Then
19 |
20 | SPEC CHECKSUMS:
21 | NMapsGeometry: 53c573ead66466681cf123f99f698dc8071a4b83
22 | NMapsMap: a5b909a31b6f3d27a670f6eb2ddc913c38975474
23 | SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
24 | Then: 844265ae87834bbe1147d91d5d41a404da2ec27d
25 |
26 | PODFILE CHECKSUM: 27646ac6cfb8fdafd45f867d1fa8111127e97cc6
27 |
28 | COCOAPODS: 1.15.2
29 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/README.md:
--------------------------------------------------------------------------------
1 | # Adapter
2 |
3 | Adapter 패턴은 서로다른 인터페이스를 가진 클래스들을 연결해서 함께 작동하도록 만드는 구조적 패턴 입니다.
4 | 음.. 정의만으로는 이해가 잘 되지 않으니 코드랑 함께 보시죠.
5 |
6 |
7 |
8 | ## IRL (In Real Life)
9 |
10 | 예를들어 다음과 같이 기존에 사용하던 NaverMap의 카메라이동, 마커추가 로직이 있다고 해보겠습니다.
11 |
12 | ```Swift
13 | // 네이버맵 카메라 이동 로직
14 | let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng), zoomTo: 9)
15 | cameraUpdate.animation = .easeIn
16 | mapView.moveCamera(cameraUpdate)
17 |
18 | // 네이버맵 마커 추가 로직
19 | let marker = NMFMarker()
20 | marker.position = NMGLatLng(lat: lat, lng: lng)
21 | marker.mapView = mapView
22 |
23 | ```
24 |
25 | 아니근데 서비스 확장을 위해서 일부 지역에서는 네이버 지도가 아닌 애플지도를 사용해야한다는 기획이 떨어졌습니다!!
26 | 뭐 어쩌겠습니까! 까라면 까아죠!!
27 |
28 | 그렇게 애플맵에서도 같은 기능인 moveCamera 그리고 마커추가 로직에 대응을 해주려는데 네이버맵과는 사용 방식이 약간 다른겁니다..
29 |
30 | ```Swift
31 | // 애플맵 카메라 이동 로직
32 | let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lng),
33 | latitudinalMeters: 1000, longitudinalMeters: 1000)
34 | mapView.setRegion(region, animated: true)
35 |
36 | // 애플맵 마커 추가 로직
37 | let annotation = MKPointAnnotation()
38 | annotation.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
39 | annotation.title = "부산"
40 | annotation.subtitle = "떠나자 부산으로!"
41 |
42 | mapView.addAnnotation(annotation)
43 |
44 | ```
45 |
46 | 아.. 곤란합니다.
47 | 그럼 카메라 이동로직, 마커추가 로직에 대해서 맵 종류에 따라서 분기 따줄까요?
48 |
49 | ```Swift
50 | switch mapType {
51 | case .naver:
52 | let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng), zoomTo: 9)
53 | cameraUpdate.animation = .easeIn
54 | mapView.moveCamera(cameraUpdate)
55 | case .apple:
56 | let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lng),
57 | latitudinalMeters: 1000, longitudinalMeters: 1000)
58 | mapView.setRegion(region, animated: true)
59 | }
60 | ```
61 |
62 | 흠.. 이렇게 하면 새로운 지도 서비스를 추가하거나 기존 서비스의 API가 변경되면 이 함수를 직접 수정해야하면서 OCP에 위배가 되겠군요.. 그리고 그럼 이렇게 일일이 각 서비스 API에 맞게 분기쳐줘야 한단 말이야??
63 | 라는 퇴사 마려운 생각이 들게 됩니다.
64 |
65 | 하지만 어림도 없습니다.
66 | 우리에겐 Adapter 패턴이 있으니까요!
67 |
68 | 자 차근차근 Adapter 패턴을 이용해서 우리가 수정할 수 없는 외부 라이브러리들의 코드들을 기존에 우리가 사용하던 방식대로 호환 되게 만들어 보자구요!
69 |
70 | 그러기 위해선 일단 기존에 사용하던 방식을 추상화 시켜줘야 합니다.! **(중요!! 추상화를 안 하면 어댑터를 사용할 수 없음)**
71 |
72 | ```Swift
73 | protocol MapService {
74 | func moveCamera(lat: Double, lng: Double)
75 | func addMarker(lat: Double, lng: Double)
76 | }
77 |
78 | ```
79 |
80 | 그럼 기존의 NaverMap은 이 프로토콜을 채택해서 아래와 같이 구현할 수 있겠죠?
81 |
82 | ```Swift
83 | import NMapsMap
84 |
85 | final class NaverMapAdapter: MapService {
86 |
87 | private let mapView = NMFMapView()
88 |
89 | func moveCamera(lat: Double, lng: Double) {
90 | let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng), zoomTo: 9)
91 | cameraUpdate.animation = .easeIn
92 | mapView.moveCamera(cameraUpdate)
93 | }
94 |
95 | func addMarker(lat: Double, lng: Double) {
96 | let marker = NMFMarker()
97 | marker.position = NMGLatLng(lat: lat, lng: lng)
98 | marker.mapView = mapView
99 | }
100 | }
101 |
102 |
103 | ```
104 |
105 | 추상화 된 걸로 한 번 감싸주는 거라고 생각해주면 됩니다.
106 |
107 | 그럼 기획 요청에 따라 애플맵을 추가해보겠습니다.
108 | 애플맵에 구현되어있는 함수들은 조금 다르긴 하지만 MapService의 함수들을 구현할 때 최대한 기존 동작 방식과 비슷하게만 로직을 작성해주면 되는 문제 아니겠습니까!
109 |
110 | ```Swift
111 | import MapKit
112 |
113 | final class AppleMapAdapter: MapService {
114 |
115 | private let mapView = MKMapView()
116 |
117 | func draw(in viewController: UIViewController) {
118 | viewController.view.addSubview(mapView)
119 | mapView.snp.makeConstraints {
120 | $0.edges.equalToSuperview()
121 | }
122 | }
123 |
124 | func moveCamera(lat: Double, lng: Double) {
125 | let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lng),
126 | latitudinalMeters: 1000, longitudinalMeters: 1000)
127 | mapView.setRegion(region, animated: true)
128 | }
129 |
130 | func addMarker(lat: Double, lng: Double) {
131 | let annotation = MKPointAnnotation()
132 | annotation.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
133 | annotation.title = "부산"
134 | annotation.subtitle = "떠나자 부산으로!"
135 |
136 | mapView.addAnnotation(annotation)
137 | }
138 | }
139 |
140 |
141 | ```
142 |
143 | 자 보시면 네이버 지도에서의 사용하는 moveCamera와 가장 비슷한 동작을 하는 애플맵에서의 setRegion을 호출해서 adapting 해주는 모습을 볼 수 있습니다. 마커 추가해주는 쪽도 마찬가지로요!
144 |
145 | 이렇게 기존에 사용하던 객체를 추상화 해서 어댑터로 기존에 동작하던 로직과 비슷하게 구현해주면 유연하게 객체를 추가할 수 있겠죠??
146 | (만약 새로운 카카오맵을 추가한다고 하면 MapService 프로토콜을 채택한 객체만 새로 만들어주면 되니까 말이죠!)
147 | 어떻게 보면 각 기능들을 추상화 해서 거기에 wrapping 해준다고도 생각할 수 있을것 같습니다.
148 |
149 | 다이어그램으로 표현하면 이렇게 표현할 수 있겠네요!
150 |
151 | 
152 |
153 | 기존에 사용하던 객체를 추상화 해서 다른 객체도 추상화의 구현을 통해 비슷한 동작을 하도록 wrapping 해주는 방식, 이게 어댑터 패턴입니다.
154 | 간단하죠?
155 |
156 |
157 |
158 | ## 실행 화면
159 |
160 | 잘 보이진 않지만 NaverMapAdapter에서 AppleMapAdapter로 바꿔주는 것만으로 같은 동작을 동일하게 잘 수행하는 모습을 보실 수 있습니다.
161 |
162 | 
163 |
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Resources/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Adapter/Resources/diagram.png
--------------------------------------------------------------------------------
/Structural patterns/Adapter/Resources/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Adapter/Resources/preview.gif
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "alerttoast",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/elai950/AlertToast",
7 | "state" : {
8 | "revision" : "b39252eacd159904afd7d718bba0afabb87f2f2f",
9 | "version" : "1.3.9"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/BridgeApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BridgeApp.swift
3 | // Bridge
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 | import AlertToast
10 |
11 | @main
12 | struct BridgeApp: App {
13 |
14 | @StateObject var globalToast = GlobalToast.shared
15 |
16 | var body: some Scene {
17 | WindowGroup {
18 | PaymentView()
19 | .toast(isPresenting: $globalToast.isShown) {
20 | .init(displayMode: .alert, type: .regular, title: globalToast.title)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Bridge
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 | import AlertToast
10 |
11 | // 결제 방식과 결제 처리 클래스는 이전 예시와 동일하게 유지
12 |
13 | struct PaymentView: View {
14 |
15 | @State private var selectedMethod: PaymentMethod = CreditCard()
16 |
17 | var body: some View {
18 | VStack {
19 | Text("결제 수단을 선택해 주세요")
20 | .font(.headline)
21 |
22 | HStack {
23 | Button("신용카드") {
24 | selectedMethod = CreditCard()
25 | }
26 | .padding()
27 | .background(Color.orange)
28 | .foregroundColor(.white)
29 | .cornerRadius(10)
30 | .opacity(type(of: selectedMethod) == CreditCard.self ? 1 : 0.6)
31 |
32 | Button("토스페이") {
33 | selectedMethod = Toss()
34 | }
35 | .padding()
36 | .background(Color.blue)
37 | .foregroundColor(.white)
38 | .cornerRadius(10)
39 | .opacity(type(of: selectedMethod) == Toss.self ? 1 : 0.6)
40 |
41 | Button("네이버페이") {
42 | selectedMethod = NaverPay()
43 | }
44 | .padding()
45 | .background(Color.green)
46 | .foregroundColor(.white)
47 | .cornerRadius(10)
48 | .opacity(type(of: selectedMethod) == NaverPay.self ? 1 : 0.6)
49 | }
50 |
51 | Spacer()
52 |
53 | Button("결제") {
54 | let processor = PaymentProcessor(method: selectedMethod, amount: 100_000)
55 | processor.executePayment()
56 | }
57 | .padding()
58 | .background(Color.black)
59 | .foregroundColor(.white)
60 | .cornerRadius(10)
61 | }
62 | .padding()
63 | }
64 | }
65 |
66 | // SwiftUI 미리보기 환경
67 | struct PaymentView_Previews: PreviewProvider {
68 | static var previews: some View {
69 | PaymentView()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/GlobalToast.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GlobalToast.swift
3 | // Bridge
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | final class GlobalToast: ObservableObject {
11 |
12 | static let shared = GlobalToast()
13 |
14 | @Published var isShown = false
15 | @Published var title: String = ""
16 |
17 | private init() { }
18 |
19 | public func showToast(title: String) {
20 | self.title = title
21 | isShown = true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/PaymentMethod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PaymentMethod.swift
3 | // Bridge
4 | //
5 | // Created by 이승기 on 2/27/24.
6 | //
7 |
8 | import Foundation
9 |
10 | // 결제 방식에 대한 추상화
11 | protocol PaymentMethod {
12 | func processPayment(amount: Double)
13 | }
14 |
15 | class CreditCard: PaymentMethod {
16 | func processPayment(amount: Double) {
17 | GlobalToast.shared.showToast(title: "신용카드로 \(amount)원 결제 중입니다..")
18 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
19 | GlobalToast.shared.showToast(title: "\(amount)원 결제 완료 (결제수단: 신용카드)")
20 | }
21 | }
22 | }
23 |
24 | class Toss: PaymentMethod {
25 | func processPayment(amount: Double) {
26 | GlobalToast.shared.showToast(title: "토스로 \(amount)원 결제 중입니다..")
27 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
28 | GlobalToast.shared.showToast(title: "\(amount)원 결제 완료 (결제수단: 토스페이)")
29 | }
30 | }
31 | }
32 |
33 | class NaverPay: PaymentMethod {
34 | func processPayment(amount: Double) {
35 | GlobalToast.shared.showToast(title: "네이버페이로 \(amount)원 결제 중입니다..")
36 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
37 | GlobalToast.shared.showToast(title: "\(amount)원 결제 완료 (결제수단: 네이버페이)")
38 | }
39 | }
40 | }
41 |
42 | // 결제 처리에 대한 추상화
43 | class PaymentProcessor {
44 | private let method: PaymentMethod
45 | private let amount: Double
46 |
47 | init(method: PaymentMethod, amount: Double) {
48 | self.method = method
49 | self.amount = amount
50 | }
51 |
52 | func executePayment() {
53 | method.processPayment(amount: amount)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/Example/Bridge/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Bridge/README.md:
--------------------------------------------------------------------------------
1 | # Bridge
2 |
3 | Bridge 패턴은 구현을 추상화로 분리하고 그 구현을 가져다 사용하는것을 얘기합니다.
4 | 역시 정의만으로는 와닿지 않네요..
5 |
6 | Bridge 패턴을 사용하지 않았을 때와 사용했을 때를 비교해가면 설명해보겠습니다.
7 |
8 |
9 |
10 | ## IRL (In Real Life)
11 |
12 | ### Bridge 패턴을 사용하지 않았을 때
13 |
14 | 아래와같이 결제를 처리하는 클래스를 만들어보았다고 해보겠습니다.
15 |
16 | ```Swift
17 | class PaymentProcessor {
18 | private let amount: Double
19 |
20 | init(amount: Double) {
21 | self.amount = amount
22 | }
23 |
24 | func payWithCreditCard() {
25 | // 신용카드 결제 로직
26 | }
27 |
28 | func payWithToss() {
29 | // 토스페이 결제 로직
30 | }
31 |
32 | func payWithNaverPay() {
33 | // 네이버페이 결제 로직
34 | }
35 | }
36 | ```
37 |
38 | 뭐 사용하는 데에 크게 문제는 없어보입니다...만!!
39 | 브릿지패턴 입장에서 보면 다음과 같은 문제들이 있을 수 있겠습니다.
40 |
41 | - payWithCreditCard, payWithToss, payWithNaverPay 로직이 PaymentProcess 라는 클래스에 강하게 결합되어있습니다.
42 | - 또 새로운 메세지 전송방식을 추가하려면 PaymentProcessor 클래스 자체를 수정해야하며 이는 OCP를 위배합니다.
43 |
44 | 코드 자체가 좀 간단해서 그렇게 큰 문제로 느껴지지는 않습니다만 만약 메세지를 전송하는 방식이 다양하거나 여러 기능을 묶어서 관리해야한다면 유지보수에 어려움을 겪을지도 모릅니다.
45 |
46 | 예를들어서 결제 방식에 prepareTossPay, payWithToss, cancelPayment 이렇게 기능들을 묶음으로 관리해야한다면 NaverPay 에도 비슷한 기능들을 모두 추가해줘야할텐데 다른 개발자가 코드를 넘겨 받았을 때 이를 단번에 알기란 쉽지 않겠죠.
47 |
48 | 바로 이런 불편함 점들을 Bridge 패턴이 해소 시켜줍니다!
49 |
50 | ### Bridge 패턴을 사용했을 때
51 |
52 | 그럼 Bridge 패턴을 바로 적용해보도록 하겠습니다.
53 |
54 | Bridge 패턴의 핵심은 구현체를 추상화 하고 그 추상화된 것을 따로 class나 struct에 구현한 후 사용하는 것입니다.
55 | 그러니까 기존에 사용하던 payWithCreditCard, payWithToss, payWithNaverPay를 processPayment 로 추상화 시켜버리는거죠.
56 |
57 | 그리고 이걸 protocol 에다 박아넣겠습니다.
58 |
59 | ```Swift
60 | protocol PaymentMethod {
61 | func processPayment(amount: Double)
62 | }
63 |
64 | ```
65 |
66 | 자 그리고서 이걸 각각 필요에 맞게 구현해주면 됩니다.
67 | 우린 CreditCard, Toss, NaverPay 방식을 사용하고 있으니까 각각 구현해 주면 되겠죠?
68 |
69 | ```Swift
70 | class CreditCard: PaymentMethod {
71 | func processPayment(amount: Double) {
72 | // 신용카드 결제로직
73 | }
74 | }
75 |
76 | class Toss: PaymentMethod {
77 | func processPayment(amount: Double) {
78 | // 토스페이 결제로직
79 | }
80 | }
81 |
82 | class NaverPay: PaymentMethod {
83 | func processPayment(amount: Double) {
84 | // 네이버페이 결제로직
85 | }
86 | }
87 | ```
88 |
89 | 그럼 이걸 이제 기존 PaymentProcessor 객체에서..
90 |
91 | ```Swift
92 | class PaymentProcessor {
93 | private let method: PaymentMethod // 이걸 Bridge 된거라고 표현함.
94 | private let amount: Amount
95 |
96 | init(method: PaymentMethod, amount: Double) {
97 | self.method = method
98 | self.amount = amount
99 | }
100 |
101 | func executePayment() {
102 | method.processPayment(amount: amount)
103 | }
104 | }
105 |
106 |
107 | ```
108 |
109 | 이렇게 주입 받아서 사용하면 됩니다.
110 |
111 | 느낌 왔나요...!!!???
112 | 네 이게 바로 Bridge 패턴 입니다.
113 |
114 | 실제로 결제방식이 이것보다 많을텐데
115 | 만약 페이팔을 통한 결제를 추가하고싶다! 그러면 아래와같이 PaymentMethod 채택한 객체 하나 추가해주면 끝이죠.
116 |
117 | ```Swift
118 | class PayPal: PaymentMethod {
119 | func processPayment(amount: Double) {
120 | // 페이팔 결제로직
121 | }
122 | }
123 |
124 | ```
125 |
126 | 자 그럼 Bridge 패턴의 정의를 다시 한 번 볼까요?
127 | '구현(payWithCreditCard, payWithToss, payWithNaverPay)을 추상화(PaymentMethod)로 분리하고 그 구현(CreditCard, Toss, NaverPay)을 가져다 사용하는것을 얘기합니다.'
128 |
129 | 이젠 Bridge 패턴을 이해하셨을거라 믿습니다.
130 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/CompositeApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompositeApp.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct CompositeApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | SettingsView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingComponent.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol SettingComponent {
11 | var title: String { get }
12 | func render() -> AnyView
13 | }
14 |
15 | // 얘는 하위 leaf 들을 포함하고 있는 Composite임.
16 | // 편의를 위해 protocol로 뺐음.
17 | protocol Compositable {
18 | var leaves: [SettingComponent] { get set }
19 | mutating func addComponent(_ component: SettingComponent)
20 | }
21 |
22 | extension Compositable {
23 | mutating func addComponent(_ component: SettingComponent) {
24 | self.leaves.append(component)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Audio.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Composite+Audio.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AudioSetting: SettingComponent, Compositable {
11 | var title: String
12 | var leaves = [SettingComponent]()
13 |
14 | func render() -> AnyView {
15 | AnyView(
16 | Section {
17 | ForEach(leaves.indices, id: \.self) { index in
18 | self.leaves[index].render()
19 | }
20 | } header: {
21 | Text(title)
22 | }
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Notification/Composite+Notification.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Composite+Notification.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NotificationSetting: SettingComponent, Compositable {
11 | var title: String
12 | var leaves = [SettingComponent]()
13 |
14 | func render() -> AnyView {
15 | AnyView(
16 | NavigationLink {
17 | List {
18 | ForEach(leaves.indices, id: \.self) { index in
19 | self.leaves[index].render()
20 | }
21 | }
22 | } label: {
23 | Text(title)
24 | }
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Notification/Leaf+ScheduledSummary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Leave+ScheduledSummary.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ScheduledSummary: SettingComponent {
11 | var title: String
12 | @Binding var isOn: Bool
13 |
14 | init(title: String, isOn: Binding) {
15 | self.title = title
16 | _isOn = isOn
17 | }
18 |
19 | func render() -> AnyView {
20 | AnyView(
21 | Toggle(title, isOn: $isOn)
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Notification/Leaf+ShowPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Leave+ShowPreview.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ShowPreview: SettingComponent {
11 | var title: String
12 |
13 | func render() -> AnyView {
14 | AnyView(
15 | NavigationLink(destination: {
16 | Text("\(title) 설정 화면")
17 | }, label: {
18 | Text(title)
19 | })
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Sounds/Composite+Sounds.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Composite+Sounds.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SoundSetting: SettingComponent, Compositable {
11 | var title: String
12 | var leaves = [SettingComponent]()
13 |
14 | func render() -> AnyView {
15 | AnyView(
16 | NavigationLink {
17 | List {
18 | ForEach(leaves.indices, id: \.self) { index in
19 | self.leaves[index].render()
20 | }
21 | }
22 | } label: {
23 | Text(title)
24 | }
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Audio/Composite+Sounds/Leaf+SilentMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Leaf+SilentMode.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SilentMode: SettingComponent {
11 | var title: String
12 | @Binding var isOn: Bool
13 |
14 | init(title: String, isOn: Binding) {
15 | self.title = title
16 | _isOn = isOn
17 | }
18 |
19 | func render() -> AnyView {
20 | AnyView(
21 | Toggle(title, isOn: $isOn)
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Network/Composite+Network.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkSetting.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NetworkSetting: SettingComponent, Compositable {
11 | var title: String
12 | var leaves = [SettingComponent]()
13 |
14 | func render() -> AnyView {
15 | AnyView(
16 | Section {
17 | ForEach(leaves.indices, id: \.self) { index in
18 | self.leaves[index].render()
19 | }
20 | } header: {
21 | Text(title)
22 | }
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Network/Compsite+Wifi/Composite+Wifi.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Root.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Wifi Setting에는 하위 leaf들을 가지고 있기 때문에 하위 leaf를 추가할 수 있는 Compositable을 같이 채택
11 | struct WifiSetting: SettingComponent, Compositable {
12 | var title: String
13 | var leaves = [SettingComponent]()
14 |
15 | func render() -> AnyView {
16 | AnyView(
17 | NavigationLink {
18 | List {
19 | ForEach(leaves.indices, id: \.self) { index in
20 | self.leaves[index].render()
21 | }
22 | }
23 | } label: {
24 | Text(title)
25 | }
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Network/Compsite+Wifi/Leaf+AskToJoinNetwork.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AskToJoinNetwork.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AskToJoinNetworks: SettingComponent {
11 | var title: String
12 |
13 | func render() -> AnyView {
14 | AnyView(
15 | NavigationLink(destination: {
16 | Text("\(title) 설정 화면")
17 | }, label: {
18 | Text(title)
19 | })
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Network/Compsite+Wifi/Leaf+Hotspot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Hotspot.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct Hotspot: SettingComponent {
11 | var title: String
12 |
13 | func render() -> AnyView {
14 | AnyView(
15 | NavigationLink(destination: {
16 | Text("\(title) 설정 화면")
17 | }, label: {
18 | Text(title)
19 | })
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Composite+Network/Leaf+AirplaneMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AirplaneMode.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // AirplaneMode 에는 더이상 하위 leaf가 없기 때문에 children이 존재하지 않습니다.
11 | struct AirplaneModeSetting: SettingComponent {
12 | var title: String
13 | @Binding var isOn: Bool
14 |
15 | init(title: String, isOn: Binding) {
16 | self.title = title
17 | _isOn = isOn
18 | }
19 |
20 | func render() -> AnyView {
21 | AnyView(
22 | Toggle(title, isOn: $isOn)
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingItems/Compsite+Setting.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootSetting.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RootSetting: SettingComponent, Compositable {
11 | var title: String
12 | var leaves = [SettingComponent]()
13 |
14 | func render() -> AnyView {
15 | AnyView(
16 | List {
17 | ForEach(leaves.indices, id: \.self) { index in
18 | self.leaves[index].render()
19 | }
20 | }.navigationTitle(title)
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Example/Composite/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // Composite
4 | //
5 | // Created by 이승기 on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SettingsView: View {
11 |
12 | var body: some View {
13 | NavigationView(content: {
14 | compositeSettingView()
15 | })
16 | }
17 |
18 | private func compositeSettingView() -> some View {
19 | var root = RootSetting(title: "Setting")
20 |
21 | // 🗼. 네트워크 설정 그룹
22 | var networkSetting = NetworkSetting(title: "Network Setting")
23 |
24 | // 🗼 > ✈️. 비행기보드 설정
25 | let airplaneMode = AirplaneModeSetting(title: "AirplainMode", isOn: .constant(true))
26 |
27 | // 🗼 > 🛜. Wifi 설정
28 | var wifiSetting = WifiSetting(title: "Wi-Fi")
29 | // 🗼 > 🛜 > 💭. 네트워크 접속시 물어보기
30 | wifiSetting.addComponent(AskToJoinNetworks(title: "Ask to Join Networks"))
31 | // 🗼 > 🛜 > 🔥. 핫스팟
32 | wifiSetting.addComponent(Hotspot(title: "Hotspot"))
33 |
34 |
35 | // 🗼 네트워크 설정 조합
36 | networkSetting.addComponent(airplaneMode)
37 | networkSetting.addComponent(wifiSetting)
38 |
39 |
40 |
41 | // 👂. 오디오 설정 그룹
42 | var audioSetting = AudioSetting(title: "Audio")
43 |
44 | // 👂 > 🔔. 알림 설정
45 | var notifications = NotificationSetting(title: "Notifications")
46 |
47 | // 👂 > 🔔 > 📝. Scheduled summary
48 | let scheduledSummary = ScheduledSummary(title: "Scheduled Summary", isOn: .constant(false))
49 | notifications.addComponent(scheduledSummary)
50 |
51 | // 👂 > 🔔 > 🔍. Show Preview
52 | let showPreview = ShowPreview(title: "Show Preview")
53 | notifications.addComponent(showPreview)
54 |
55 |
56 | // 👂 > 🔊. 사운드 설정
57 | var sounds = SoundSetting(title: "Sounds")
58 |
59 | // 👂 > 🔊 > 🤫 Silent Mode
60 | let silentMode = SilentMode(title: "Silent Mode", isOn: .constant(true))
61 | sounds.addComponent(silentMode)
62 |
63 | // 👂. 오디오 설정 조합
64 | audioSetting.addComponent(notifications)
65 | audioSetting.addComponent(sounds)
66 |
67 |
68 |
69 | // 루트 설정 조합
70 | root.addComponent(networkSetting)
71 | root.addComponent(audioSetting)
72 | return root.render()
73 | }
74 | }
75 |
76 | #Preview {
77 | SettingsView()
78 | }
79 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/README.md:
--------------------------------------------------------------------------------
1 | # Composite
2 |
3 | Composite 패턴은 구조적 디자인 패턴의 일종으로, 객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 클라이언트가 동일하게 다룰 수 있도록 해줍니다.
4 | 이 패턴의 핵심은 "개체(Leaf)"와 "복합체(Composite)"가 같은 인터페이스를 공유한다는 것입니다.
5 |
6 | 개체와 복합체? 뭔가 와닿지 않죠?
7 | 하지만 이런 구조는 이미 우리가 모르는 사이에 수많이 접했을 겁니다.
8 |
9 | 예를들어 아이폰의 설정을 생각해 볼게요.
10 | 설정에는 여러 설정 아이템들이 있고 그 아이템 자체로 어떤 설정 페이지로 넘어갈 수도 있고 해당 아이템을 클릭했을때 다시 다른 설정 아이템들의 묶음으로 넘어갈 수도 있죠? 마치 파일과 디렉토리로 구성 되어있는 폴더 구조처럼요!
11 |
12 |
13 |
14 | 이렇게 하위의 하위의 하위의.. 객체. 이렇게 트리 형태로 구조화할 수 있는 것을 composite 패턴을 이용하면 쉽게 관리할 수 있게 됩니다!
15 |
16 | ### 구성 요소
17 |
18 | Composite 패턴은 다음과 같은 구성요소를 가집니다.
19 |
20 | - **컴포넌트**: 개체와 복합체가 공통으로 구현하는 인터페이스입니다. 이 인터페이스는 개체와 복합체가 수행해야 하는 작업을 정의합니다.
21 | - **리프**(Leaf): 컴포지트 구조의 기본 요소로, 컴포넌트 인터페이스를 구현합니다. 리프는 다른 객체를 포함할 수 없는 단순 객체입니다.
22 | - **컴포지트**(Composite): 여러 개의 컴포넌트(리프 또는 다른 컴포지트)를 포함할 수 있는 복합 객체입니다. 컴포지트는 컴포넌트 인터페이스를 구현하며, 내부적으로 자식 컴포넌트 목록을 관리합니다.
23 |
24 |
25 |
26 | ## IRL (In Real Life)
27 |
28 | 자 그럼 위에서 예시로 들었던 설정 구조를 Composite 패턴을 이용해서 가볍게 구현해보면서 좀 더 명확히 이해해보자구요!
29 |
30 | 일단 구성요소를 하나하나 구현해 나가볼게요.
31 |
32 | ### Component 구현
33 |
34 | Component는 개체와 복합체가 각각 수행해야하는 작업을 의미하는데요.
35 | 다음과 같이 구현해볼 수 있겠네요.
36 |
37 | ```Swift
38 | protocol SettingComponent {
39 | var title: String { get }
40 | func render() -> AnyView
41 | }
42 |
43 | ```
44 |
45 | 이것만으로는 Component가 뭐하는 녀석인지는 아직 감이 안 옵니다..
46 |
47 | ### Leaf 구현
48 |
49 | 이어서 Leaf를 구현해 볼게요!
50 |
51 | ```Swift
52 | struct AskToJoinNetworks: SettingComponent {
53 | var title: String
54 |
55 | func render() -> AnyView {
56 | AnyView(
57 | NavigationLink(destination: {
58 | Text("\(title) 설정 화면")
59 | }, label: {
60 | Text(title)
61 | })
62 | )
63 | }
64 | }
65 |
66 | ```
67 |
68 | 이렇게 SettingComponent(공통) 프로토콜을 채택한 메뉴 아이템을 만들었습니다.
69 | 얘는 이제 Leaf이기 때문에 뭐 하위 메뉴를 또 만든다거나 할 수 없이 얘 자체로 끝 입니다.
70 |
71 | 예를들면 소프트웨어 업데이트 페이지에서 더이상 네비게이션 되는 메뉴아이템이 없는것 처럼요! (실제로 있긴한데 일단 이해를 돕기 위해서 그렇다고 합시다.)
72 |
73 | **Composite 구현**
74 | 만약 Leaf의 하위 Leaf들을 구성(예를들면 설정 -> 일반 으로 들어가면 또 하위 아이템들이 있는것 처럼)하고 싶다면 해당 Leaf는 Composite으로 승격시켜서 사용하면 됩니다.
75 | 여기서 승격 시킨다는 것을 별 대단한 건 아니구요 그냥 하위 leaf들을 가지고 있을 수 있는 array 그리고 추가할 수 있는 함수를 가지고 있음을 의미합니다.
76 |
77 | 하지만 본질적으로 얘도 결국 SettingComponent를 채택하고 있기 때문에 또 다른 Compite의 array에 포함되거나 할 수 있습니다.
78 | 이게 Composite이 복잡한 구조를 단순하게 관리할 수 있는 핵심이죠!
79 |
80 | 자 그럼 아까 위에서 만든 AskToJoinNetwork는 실제로 Wifi 설정 하위에 있기 때문에 우리도 Wifi 라는 상위 Composite을 만들어보면 되겠죠?
81 |
82 | ```Swift
83 | protocol Compositable {
84 | var leaves: [SettingComponent] { get set }
85 | mutating func addComponent(_ component: SettingComponent)
86 | }
87 |
88 | extension Compositable {
89 | mutating func addComponent(_ component: SettingComponent) {
90 | self.leaves.append(component)
91 | }
92 | }
93 |
94 | struct WifiSetting: SettingComponent, Compositable {
95 | var title: String
96 | var leaves = [SettingComponent]()
97 |
98 | func render() -> AnyView {
99 | AnyView(
100 | NavigationLink {
101 | List {
102 | ForEach(leaves.indices, id: \.self) { index in
103 | self.leaves[index].render()
104 | }
105 | }
106 | } label: {
107 | Text(title)
108 | }
109 | )
110 | }
111 | }
112 | ```
113 |
114 | 위처럼 처럼 WifiSetting(Composite)객체가 하위 SettingComponent들을 가지고 있을 수 있게 된걸 볼 수 있습니다.
115 | 이렇게 하위 leaf들을 포함할 수 있으면서 동시에 leaf와 같은 SettingComponent를 채택하고 있는 녀석이 Composite 입니다.
116 |
117 | (저는 반복해서 작성하는게 귀찮아서 Composite이 되는 SettingCompoent는 공통 로직을 따로 구현된 프로토콜로 빼서 사용하겠습니다.)
118 |
119 | 아무튼 이렇게 구현했으니 사용해 봐야겠죠?
120 |
121 | ```Swift
122 | // 얘가 Composite
123 | var wifiSetting = WifiSetting(title: "Wi-Fi")
124 |
125 | // 그리고 Leaf가 Composite 하위로 들어가는 모습
126 | wifiSetting.addComponent(AskToJoinNetworks(title: "Ask to Join Networks"))
127 | ```
128 |
129 | 자! 어떤가요? 상당히 직관적이죠??
130 |
131 | 여기에 또 하위 Leaf들을 추가한다면 아래와같이 표현할 수 있겠네요.
132 | 그리고 Composite은 또 다른 Composite의 leaf로도 들어갈 수 있으니 아래와 같이 작성하면 network setting 이라는 더 큰 카테고리 안에 담길 수 있겠네요!
133 |
134 | ```Swift
135 | ...
136 |
137 | // 🗼. 네트워크 설정 그룹
138 | var networkSetting = NetworkSetting(title: "Network Setting")
139 |
140 | // 🗼 > 🛜. Wifi 설정
141 | var wifiSetting = WifiSetting(title: "Wi-Fi")
142 | // 🗼 > 🛜 > 💭. 네트워크 접속시 물어보기
143 | wifiSetting.addComponent(AskToJoinNetworks(title: "Ask to Join Networks"))
144 | // 🗼 > 🛜 > 🔥. 핫스팟
145 | wifiSetting.addComponent(Hotspot(title: "Hotspot"))
146 |
147 | ...
148 | ```
149 |
150 | 이런식의 복잡한 트리 구조를 관리할때 Composite 패턴이 상당히 유용히 사용될 수 있습니다.
151 |
152 | 예시 프로젝트에는 이보다 조금 더 복잡하게 구성해놨으니 예시도 보면서 이해를 도와보세요!
153 |
154 | (실제로 사용할 때에는 최적화 같은것도 같이 고려해 보세요 🙂)
155 |
156 |
157 |
158 | ## 실행 화면
159 |
160 | 
161 |
--------------------------------------------------------------------------------
/Structural patterns/Composite/Resources/composite pattern structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Composite/Resources/composite pattern structure.png
--------------------------------------------------------------------------------
/Structural patterns/Composite/Resources/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Composite/Resources/preview.gif
--------------------------------------------------------------------------------
/Structural patterns/Facade/README.md:
--------------------------------------------------------------------------------
1 | # Facade
2 |
3 | Facade 패턴은 복잡한 시스템을 간단한 인터페이스로 제공해주는 패턴 입니다.
4 |
5 |
6 |
7 | [Refactoring Guru](https://refactoring.guru/ko/design-patterns/facade)에서 소개된 훌륭한 예시를 참조해 보겠습니다.
8 |
9 | 여러분이 고객 서비스 센터에 전화를 걸어 "요금제 변경해주세요!"와 같은 요청을 한다고 합니다.
10 | 이 때, 상담원은 여러분의 요청을 처리하기 위해 다양한 사내 시스템을 사용하여 사용자 정보를 조회, 요금제 정보를 확인하고, 새로운 요금제로 변경하는 등 여러 단계의 작업을 수행합니다.
11 |
12 | 
13 |
14 | 그러나 이러한 복잡한 과정은 클라이언트 측에서 알거나 이해할 필요가 없죠?
15 |
16 | Facade 패턴은 이와 같은 복잡한 작업을 "요금제 변경"이라는 단일 작업으로 추상화하여, 클라이언트가 간단한 명령으로 복잡한 시스템의 결과를 얻을 수 있도록 해줍니다. 마치 고객이 상담원에게 단순한 요청을 통해 여러 복잡한 내부 과정을 거치지 않고도 원하는 결과를 얻는 것과 같이 말이죠!
17 |
18 | 말이 거창해져서 그런데 그냥 복잡한 구조를 사용하기 쉬운 API로 제공하는 것을 퍼사드라도 이해하셔도 무방합니다.
19 |
20 |
21 |
22 | ## IRL (In Real Life)
23 |
24 | > 뭔가 예제 프로젝트를 만들어서 하기에는 Facade패턴이 너무 단순해서 코드 스니펫으로 대체하겠습니다.
25 |
26 | 예를들어 AuthenticationService, CacheService, NetworkService 등 다양하게 존재한다고 해보겠습니다.
27 |
28 | 그래서 네트워크 요청을 할 때
29 |
30 | 1. 사용자 인증 정보를 AuthenticationService를 통해서 확인하고
31 | 2. CacheService를 이용해서 캐싱된 정보가 있는지 확인하고 없다면
32 | 3. 최종적으로 NetworkService를 통해서 데이터를 요청한다고 가정해 볼게요.
33 |
34 | 여기서 문제가 뭐냐? 만약 ViewModel 같은 곳에서 매번 네트워크 요청할 때마다 이런 작업을 항상 반복하기에는 조금 무리가 있어보이죠? 로직이 조금 변경되면 구현한 곳 일일이 찾아서 변경도 해줘야하구요..
35 |
36 | 건강한 개발자라면 이런 반복되는 로직을 어딘가에 빼서 사용할 생각을 할겁니다.
37 |
38 | 이렇게 다양한 API 호출과 그에 따른 복잡한 처리 로직이 존재할 때, Facade 패턴은 이를 캡슐화하여 클라이언트 코드가 단일 API 호출을 통해 필요한 작업을 수행할 수 있도록 해줍니다.
39 |
40 | 아마 여러분들도 알게모르게 자주 사용하시던 기법일 겁니다.
41 | 그냥 많은 개발자들이 비슷하게 사용하다보니 하나의 패턴이 발견된 것이고 거기에 거창한 이름이 붙었을 뿐.
42 |
43 | ```Swift
44 |
45 | import Foundation
46 |
47 | class AuthenticationService {
48 | func isAuthenticated() -> Bool {
49 | // 사용자 인증 상태 확인 로직
50 | return true
51 | }
52 | }
53 |
54 | class CacheService {
55 | func fetchData(forKey key: String) -> Data? {
56 | // 캐시에서 데이터 조회 로직
57 | return nil
58 | }
59 |
60 | func cacheData(_ data: Data, forKey key: String) {
61 | // 데이터 캐싱 로직
62 | }
63 | }
64 |
65 | class NetworkService {
66 | func performRequest(to url: URL, completion: @escaping (Result) -> Void) {
67 | // 실제 네트워크 요청 실행 로직
68 | }
69 | }
70 |
71 | // MARK: - Error
72 |
73 | enum NetworkFacadeError: Error {
74 | case authenticationFailed
75 | case invalidURL
76 | case noData
77 | case cachedDataUnavailable
78 | case underlyingError(Error)
79 | }
80 |
81 |
82 | // MARK: - Facade
83 |
84 | class NetworkServiceFacade {
85 | static let authenticationService = AuthenticationService()
86 | static let cacheService = CacheService()
87 | static let networkService = NetworkService()
88 |
89 | static func fetchData(from urlString: String, useCache: Bool = true, completion: @escaping (Result) -> Void) {
90 | guard let url = URL(string: urlString) else {
91 | completion(.failure(NetworkFacadeError.invalidURL))
92 | return
93 | }
94 |
95 | guard authenticationService.isAuthenticated() else {
96 | completion(.failure(NetworkFacadeError.authenticationFailed))
97 | return
98 | }
99 |
100 | if useCache, let cachedData = cacheService.fetchData(forKey: urlString) {
101 | completion(.success(cachedData))
102 | return
103 | }
104 |
105 | networkService.performRequest(to: url) { result in
106 | DispatchQueue.main.async {
107 | switch result {
108 | case .success(let data):
109 | cacheService.cacheData(data, forKey: urlString)
110 | completion(.success(data))
111 | case .failure(let error):
112 | completion(.failure(NetworkFacadeError.underlyingError(error)))
113 | }
114 | }
115 | }
116 | }
117 | }
118 |
119 |
120 | // MARK: - Facade 사용하는 곳
121 |
122 | NetworkServiceFacade.fetchData(from: "https://api.example.com/data") { result in
123 | switch result {
124 | case .success(let data):
125 | // 데이터 처리
126 | print("Data fetched successfully.")
127 | case .failure(let error):
128 | // 에러 처리
129 | print("Failed to fetch data:", error)
130 | }
131 | }
132 |
133 | ```
134 |
135 | 보셨다시피 그리 대단한 건 아니고 그냥 저렇게 복잡한 로직을 다른 개발자(클라이언트)가 직접 작성하게 할 필요없이
136 | "이 함수 호출하면 알아서 너가 만드려는거 다 되니까 이거 가져가 써~~ "
137 | 뭐 이런 느낌이지 않나 싶습니다.
138 |
139 |
140 |
141 | 이렇게 시스템이 고도화됨에 따라 로직의 복잡성이 증가하는 것은 피할 수 없는 현상인데요. 이러한 복잡성을 관리하기 위해, 특정 부분을 적절히 추상화하거나 다른 객체로 은닉함으로써 클라이언트(개발자)가 보다 고차원적인 함수들을 이용할 수 있게 됩니다.
142 |
143 | 이 과정에서 Facade 패턴이 복잡한 시스템의 내부 작업을 숨기고, 간단한 인터페이스를 제공함으로써 큰 도움을 주지 않나 생각합니다 =)
144 |
--------------------------------------------------------------------------------
/Structural patterns/Facade/Resources/facade_example_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Facade/Resources/facade_example_image.png
--------------------------------------------------------------------------------
/Structural patterns/Proxy/CachingProxy/README.md:
--------------------------------------------------------------------------------
1 | # 캐싱 프록시
2 |
3 | 캐싱 프록시는 비용이 많이 드는 작업 결과를 미리 캐시 하여 같은 요청이 있을때 캐시 된 데이터를 전달해 주는 그런 익숙하디 익숙한 로직을 프록시로 빼준 것을 얘기합니다.
4 |
5 | 이걸 프록시로 해줄 이유 있음..?
6 | 이란 의문이 드실 수 있습니다.
7 |
8 | 뭐 내가 캐싱 로직을 추가하고싶은 객체를 직접 수정해서 바꿔줄 수 있겠죠.
9 | 근데 만약 그 객체가 외무 모듈의 객체라면?? 건들이기 조차 두려운 방대한 레거시 코드라면????
10 |
11 | 답이 없습니다. 저런거 수정해서 사용할 바에는 그냥 프록시 같은 캐싱 전용 레이어를 하나 추가해서 관리하는게 유지보수 측면에서는 훨 편할지 모릅니다.
12 |
13 | 언제 사용해면 좋을지를 알았으니 바로 코드로도 함 봐보죠.
14 |
15 | ```Swift
16 | protocol DataFetcher {
17 | func fetchData() -> String
18 | }
19 |
20 | class ExpensiveDataFetcher: DataFetcher {
21 | func fetchData() -> String {
22 | // 비용이 많이 드는 데이터 획득 과정 (예: 복잡한 계산, 외부 API 호출 등)
23 | return "Expensive Data"
24 | }
25 | }
26 |
27 | ```
28 |
29 | 자 프록시 특 뭐다? 프로토콜 똑같이 채택하고 같은 동작에 대해서 약간 변형을 주는 것이죠.
30 | (약간 클래스 상속받아서 오버라이딩 해서 쓴다는 느낌도 들고..?)
31 |
32 | 암튼 캐싱 프록시 구현하면 아래와 같이 구현할 수 있습니다.
33 |
34 | ```Swift
35 | class CachingDataFetcherProxy: DataFetcher { // 👈 이거 프록시 종특
36 | private let wrappedFetcher: DataFetcher // 👈 이거 프록시 종특
37 | private var cache: String?
38 |
39 | init(fetcher: DataFetcher) {
40 | self.wrappedFetcher = fetcher
41 | }
42 |
43 | func fetchData() -> String {
44 | if let cachedData = cache {
45 | return cachedData
46 | } else {
47 | let data = wrappedFetcher.fetchData()
48 | cache = data
49 | return data
50 | }
51 | }
52 | }
53 | ```
54 |
55 | 자 이렇게 DataFetcher와 같은 함수들을 가지고 있지만 캐싱을 도와주는 더 쩌는 객체(프록시)가 탄생했습니다.
56 | 일케 되면 ExpensiveDataFetcher를 직접 사용하지 않고 CachingDataFetcherProxy를 대신 사용하게 되면 캐싱 작업이 추가된 ExpensiveDataFetcher를 ExpensiveDataFetcher객체의 직접적인 수정 없이 가능하게 되는 것이죠.
57 |
58 | 음청나죠?
59 |
60 | 이렇게 프록시는 단순 캐싱 뿐만 아니라 기존에 구현되어있는 코드 앞 뒤로 어떤 동작들을 추가해서 꾸며주고싶다 할 때에도 프록시는 유용하게 사용될 수 있습니다.
61 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // ProtectionProxy
4 | //
5 | // Created by 이승기 on 3/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | @State private var userRole: UserRole = .guest
13 |
14 | var body: some View {
15 | NavigationView {
16 | HStack {
17 | Picker("유저 모드", selection: $userRole) {
18 | ForEach(UserRole.allCases, id: \.self) { role in
19 | Text(role.displayName)
20 | }
21 | }
22 | .pickerStyle(.segmented)
23 | .frame(maxWidth: 200)
24 |
25 | NavigationLink {
26 | // 이렇게 직접 myDocument를 보여주는게 아니라 프록시를 통해서 보여주는 거임
27 | let myDocument = MyDocument()
28 | let documentProxy = DocumentProxy(userRole: userRole, document: myDocument)
29 | documentProxy.display()
30 | } label: {
31 | HStack {
32 | Text("도큐먼트 접속")
33 | Image(systemName: "arrow.right")
34 | }
35 | .foregroundStyle(Color.black)
36 | .padding()
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | #Preview {
44 | ContentView()
45 | }
46 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/Document.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Document.swift
3 | // ProtectionProxy
4 | //
5 | // Created by 이승기 on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol Document {
11 | func display() -> AnyView
12 | }
13 |
14 | struct MyDocument: Document {
15 | func display() -> AnyView {
16 | let url = Bundle.main.url(forResource: "sample", withExtension: "pdf")!
17 | return AnyView(PDFKitView(url: url))
18 | }
19 | }
20 |
21 | struct DocumentProxy: Document { // 👈 이거 프록시 종특
22 |
23 | private let userRole: UserRole
24 | private let document: any Document
25 |
26 | init(userRole: UserRole, document: any Document) {
27 | self.userRole = userRole
28 | self.document = document
29 | }
30 |
31 | func display() -> AnyView {
32 | switch userRole {
33 | case .guest:
34 | return AnyView(
35 | VStack {
36 | Text("🤚")
37 | .font(.system(size: 60))
38 |
39 | Text("접근 권한이 없습니다")
40 | .foregroundStyle(Color.black)
41 | .opacity(0.5)
42 | }
43 | )
44 |
45 | case .admin:
46 | return document.display() // 👈 이거 프록시 종특 DocumentProxy가 document와 같은 protocol을 채택함으로 display를 대리해서 호출해주는 모습
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/PDFKitView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PDFKitView.swift
3 | // ProtectionProxy
4 | //
5 | // Created by 이승기 on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 | import PDFKit
10 |
11 | struct PDFKitView: UIViewRepresentable {
12 |
13 | let url: URL
14 |
15 | func makeUIView(context: Context) -> some UIView {
16 | let pdfView = PDFView()
17 | pdfView.document = PDFDocument(url: url)
18 | return pdfView
19 | }
20 |
21 | func updateUIView(_ uiView: UIViewType, context: Context) { }
22 | }
23 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/ProtectionProxyApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProtectionProxyApp.swift
3 | // ProtectionProxy
4 | //
5 | // Created by 이승기 on 3/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ProtectionProxyApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/UserMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserMode.swift
3 | // ProtectionProxy
4 | //
5 | // Created by 이승기 on 3/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum UserRole: CaseIterable {
11 | case guest
12 | case admin
13 |
14 | var displayName: String {
15 | switch self {
16 | case .guest:
17 | "🫥 게스트"
18 | case .admin:
19 | "🕵️ 어드민"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/sample.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WallabyStuff/Swift-DesignPattern-IRL/6b3d33fe455e1410cb2de64b639b70792d531826/Structural patterns/Proxy/ProtectionProxy/Example/ProtectionProxy/sample.pdf
--------------------------------------------------------------------------------
/Structural patterns/Proxy/ProtectionProxy/README.md:
--------------------------------------------------------------------------------
1 | # 보호 프록시 (접근 제어)
2 |
3 | 보호 프록시는 기존 객체의 엑세스를 제한적으로 사용하도록 만들어주는 프록시 입니다.
4 | 그러니까 직접 객체의 함수들을 불러와서 이상한 분기까지 말고 보호 프록시 통해서 함수 호출하고 엑세스 수준에 맞는 적절한 결과 받아서 써라~ 이런거죠.
5 |
6 | 코드로 보면 이해가 더 잘 되실겁니다.
7 |
8 | 예를들어서 도큐멘트가 있다고 해보겠습니다.
9 | 이때 접근 권한은 어드민, 게스트 이렇게 두 가지로 나뉜다고 해볼게요.
10 |
11 | ```Swift
12 | enum UserRole {
13 | case admin
14 | case guest
15 | }
16 | ```
17 |
18 | document에는 아래와같이 display() 즉 도큐멘트를 열어서 볼 수 있는 기능이 있습니다.
19 |
20 | ```Swift
21 | protocol Document {
22 | func display() -> AnyView
23 | }
24 | ```
25 |
26 | 그리고 아래코드와 같이 Document를 채택하는 나만의 document 구조체를 만들어주었습니다.
27 |
28 | ```Swift
29 | struct MyDocument: Document {
30 | func display() -> AnyView {
31 | let url = Bundle.main.url(forResource: "sample", withExtension: "pdf")!
32 | return AnyView(PDFKitView(url: url))
33 | }
34 | }
35 | ```
36 |
37 | 자 여기에서 만약 UserRole이 게스트인 경우에 도큐멘트를 못 보게 막고싶다고 해볼게요.
38 |
39 | ```Swift
40 | ...
41 |
42 | let documnet = MyDocument()
43 |
44 | if userRole == .admin {
45 | documnet.display()
46 | } else {
47 | print("너 권한 없음;")
48 | }
49 |
50 | ...
51 | ```
52 |
53 | 뭐 이런식으로 할 수 있겠죠?
54 | 별 문제 없어보입니다.
55 |
56 | 하지만 개발자라면 최악의 상황들도 생각해볼 줄 알아야합니다!
57 | 자... 최악의 상황을 생각해 볼게요.
58 |
59 | 만약에 저 document.display() 를 호출하는 곳이 저쪽 뿐만이 아니라 다른 곳에도 있다고 해봅시다.
60 | 그것도 한... 100 곳에서요.
61 |
62 | 그럼 거기서 UserRole에 따라서 하나하나 다 분기 까줘야겠죠?
63 |
64 | 거기에서 또 만약 UserRole이 추가가 되었어요. 그럼 100군데 하나하나 돌아다니면서 분기 다시 수정해 줘야겠죠?
65 |
66 | 끔찍하네요🫠
67 |
68 | 이때 저 접근제어만 따로 관리해주는 녀석을 만들어보면 좋을것 같은데... 관리해주는.. 대리자..... 아!!! 프록시!!!!(ㅋㅋ;;;)
69 | 이렇게 프록시가 떠올랐다니 바로 만들어볼게요.
70 |
71 | 프록시의 종특으로는 기존 객체의 동작들을 정의하고있는 protocol 을 프록시에서도 동일하게 채택하고 원본 객체는 프록시 내부 인스턴스로 가지고있다고 하였습니다.
72 |
73 | 아래처럼요!
74 |
75 | ```Swift
76 | struct DocumentProxy: Document { // 👈 이거 프록시 종특
77 |
78 | private let userRole: UserRole
79 | private let document: any Document
80 |
81 | init(userRole: UserRole, document: any Document) {
82 | self.userRole = userRole
83 | self.document = document
84 | }
85 |
86 | func display() -> AnyView {
87 | switch userRole {
88 | case .guest:
89 | return AnyView(
90 | VStack {
91 | Text("🤚")
92 | .font(.system(size: 60))
93 |
94 | Text("접근 권한이 없습니다")
95 | .foregroundStyle(Color.black)
96 | .opacity(0.5)
97 | }
98 | )
99 |
100 | case .admin:
101 | return document.display() // 👈 이거 프록시 종특 DocumentProxy가 document와 같은 protocol을 채택함으로 display를 대리해서 호출해주는 모습
102 | }
103 | }
104 | }
105 |
106 |
107 | ```
108 |
109 | 예 이렇게 구현하면 이제 클라이언트는 아래처럼 프록시를 통해서만 함수를 호출해주면 되겠습니다.
110 |
111 | ```Swift
112 | let myDocument = MyDocument()
113 | let documentProxy = DocumentProxy(userRole: userRole, document: myDocument)
114 | documentProxy.display()
115 | ```
116 |
117 | 자 이러면 아까 생각해본 최악의 상황들,, 100군데 분기 깐 곳 찾아서 일일이 유지보수 해주지 않고 그냥 프록시의 display 함수에서만 유지보수해주면 되겠죠? 고오급 용어로는 OCP 를 잘 준수했다~ 라고 할 수 있습니다.
118 |
119 | 이렇게 보호프록시를 이해하셨다면 실제 Swift로 앱을 제작하면서 어디에 사용하면 적절할지 막 아이디어가 솟구칠 것이라 생각이 듭니다. (절대 실사례 작성하기 귀찮아서 그런거 아님)
120 |
121 | 예를들면 UserRole이 아닌 위도 경도 값에 따라서 일부 지역에서 컨텐츠 접근을 Proxy에서 제한 한다던지..? 암튼 저보단 여러분들의 상상력이 더 좋을것이라 믿습니다!!
122 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/README.md:
--------------------------------------------------------------------------------
1 | # Proxy
2 |
3 | 프록시 패턴은 어떤 객체를 가지고 내가 직접 컨트롤하지 않고 다른 객체를 통해서 간접적으로 컨트롤하는 패턴을 의미합니다.
4 | 예를들어 A라는 객체의 함수를 직접 호출하지 않고 B라는 객체가 instance로 A 객체를 가지고 있고 사용자는 B 객체를 통해서 A 객체의 함수를 호출하게 되는 겁니다.
5 |
6 | 그렇기에 Proxy 패턴의 특징으로는 A의 동작들을 정의하고있는 프로토콜을 B 객체도 동일하게 채택하고 있다는 겁니다.
7 |
8 | ```Swift
9 | protocol A_Protocol {
10 | func doSomething()
11 | }
12 |
13 | struct A: A_Protocol {
14 | func doSomething() { ... }
15 | }
16 |
17 | struct Proxy: A_Protocol { // 👈 이렇게 말이죠!
18 |
19 | let a: A // 그리고 프록시에서는 대리 호출할 객체를 인스턴스로 가지고 있어야 합니다. (그래야 대리 호출을 하던 뭐든 하겠죠?)
20 |
21 | func doSomething() {
22 | a.doSomething()
23 | }
24 | }
25 | ```
26 |
27 | 이게 Proxy 패턴의 큰 골자 입니다.
28 |
29 | 이런 Proxy의 구조적인 특징 덕분에 다양한 용도로 사용될 수 있는데요?
30 | 크게 다음과 같은 용도로 많이 사용됩니다.
31 |
32 | - **접근 제어**
33 | - **지연 초기화**
34 | - **캐싱**
35 |
36 | 이 외에도 원격, 스마트 참조 등 다양하게 더 있는데 약간 예시로 다루기에 애매한 감이 있어서 뺐습니다.
37 |
38 | 방금 언급한 접근제어, 지연 초기화, 캐싱, 추가기능 목적들에는 또 화려한 이름들이 붙여져 있습니다.
39 |
40 | - 접근제어 -> 보호 프록시 (**Protection Proxy**)
41 | - 지연 초기화 -> 가상 프록시 (**Virtual Proxy**)
42 | - 캐싱 -> 캐싱 프록시 (**Caching Proxy**)
43 |
44 | 디자인 패턴에서 화려한 네이밍들은 정말 없으면 안되나봅니다.
45 | 아무튼 이 다양한 프록시 양상들을 하나씩 예시와 함께 설명 드리도록 하겠습니다.
46 |
--------------------------------------------------------------------------------
/Structural patterns/Proxy/VirtualProxy/README.md:
--------------------------------------------------------------------------------
1 | # 가상 프록시 (지연 초기화)
2 |
3 | 지연 초기화 프록시 라고 이름 지어도 괜찮을 것 같은데 가상 프록시라고 해서 상당히 있어보이는 가상 프록시에 대해서도 알아보겠습니다.
4 |
5 | 가상 프록시는 객체를 초기화하는 작업 자체가 많이 무거울 때 유용하게 사용될 수 있습니다.
6 |
7 | 예를들어서 다음과 같은 프로토콜이 있고 이를 채택하는 객체가 있다고 해볼게요.
8 |
9 | ```Swift
10 | protocol ExpensiveObject {
11 | func performAction()
12 | }
13 |
14 | class RealExpensiveObject: ExpensiveObject {
15 | init() {
16 | print("와.. 초기화만 하는데도 빡세!!!")
17 | // 비용이 많이 드는 초기화 작업
18 | }
19 |
20 | func performAction() {
21 | print("작업을 수행하겠습니다.")
22 | }
23 | }
24 |
25 | ```
26 |
27 | 자 그럼 초기화에서 힘든 작업을 한다고 하면 그냥 객체를 초기화해서 가지고 있는 것 만으로도 상당한 손해가 일어나겠죠?
28 | 더군다다 performAction()을 사용하지도 않을 것 이라면요!?
29 |
30 | 굉장한 손해가 아닐 수 없습니다...
31 |
32 | 여기에서 가상 프록시라는 녀석이 객체 자체를 지연 초기화 시켜주도록 해줍니다.
33 | 이게 가능한것은 구조적으로 프록시라는 녀석이 실제 객체를 인스턴스로 가지고있기 때문에 가능한 것이죠.
34 |
35 | 코드로 보면 좀 더 이해가 쉬울 겁니다.
36 |
37 | ```Swift
38 | class LazyExpensiveObjectProxy: ExpensiveObject { // 👈 이거 프록시 종특
39 | private lazy var realObject: RealExpensiveObject = RealExpensiveObject() // 👈 이거 프록시 종특 다만 가상 프록시는 실제 객체를 lazy 하게 가지고 있음
40 |
41 | func performAction() { // 👈 프록시가 대리 호출해주는 거임.
42 | realObject.performAction()
43 | }
44 | }
45 | ```
46 |
47 | 일케 해주고 아래처럼 사용한다고 하면 performAction이 호출될 때까지는 초기화가 안 되겠죠?
48 |
49 | ```Swift
50 | let proxyObject = LazyExpensiveObjectProxy()
51 | print("프록시 객체 만들어짐")
52 |
53 | // 실제 객체는 아래 줄이 실행될 때 초기화됩니다.
54 | proxyObject.performAction()
55 |
56 | ```
57 |
58 | 이런 느낌으로 최적화를 해줄 수 있겠습니다.
59 |
60 | 근데 이거 그냥 첨부터 RealExpensiveObject 객체를 lazy 하게 전역 변수로 갖고있다가 사용하면 되는거 아님!?!?!?
61 | 예 맞습니다, 그래서 만약 로컬변수로 밖에 사용할 수 없을때 사용하면 되겠죠? 또는 다양한 프록시를 짬뽕 시킬 수도 있고 실제 객체의 생성 로직을 캡슐화하고, 클라이언트로부터 숨긴다는 점에서도 이점이 있을 수 있겠습니다.
62 |
63 | 암튼 이런 식으로도 사용된다~ 라고 알아주시면 될 것 같습니다.
64 |
--------------------------------------------------------------------------------