├── .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 | ![Diagram](./Resources/diagram1.png) 12 | 13 | 근데 만약 나의 아이폰을 흔들어재꼈을 때에도 같은 동작을 수행하고 싶다고 해볼게요. 14 | 15 | 그럼 버튼을 클릭했을 때의 실행되는 로직과 똑같은 로직을 또 구현해줘야할 겁니다. 16 | 17 | 이런게 몇 개 안된다면 그냥 복붙해서 사용해도 괜찮겠지만 많은 버튼과 중복되는 액션들이 존재한다면 유지보수가 쉽지 않을겁니다. 18 | 19 | ![Diagram](./Resources/diagram2.png) 20 | 21 |
22 | 23 | 그래서 이러한 문제를 해결할 수 있는 패턴으로 커맨드 패턴이 존재합니다! 24 | 25 | 위에서 말했듯 어떠한 로직을 캡슐화 시킨다 그랬었죠? 26 | 27 | 따라서 어떠한 요청에 대해서 직접 로직을 실행하는게 아니라! 커맨드를 통해서 실행하는 것 입니다! 28 | 29 | ![Diagram](./Resources/diagram3.png) 30 | 31 | 커맨드 패턴의 기저는 이렇고 구현은 보통 단일 커맨드 인터페이스를 생성하고 콘크리트 커맨드들(커맨드 인터페이스를 채택하는 객체들)을 만들어서 로직을 실행시킵니다. 32 | 33 | ![Diagram](./Resources/diagram4.png) 34 | 35 |
36 | 37 | ## 독립된 객체에서 로직을 어떻게 실행 시킨다는거임? 38 | 39 | 여기에서 의문점이 생기신 분들도 계실겁니다. 40 | 41 | 잠만.. 로직을 캡슐화 한다고 했지.. 그럼 Action 저게 어떠한 로직을 의미하는거고.. 그렇다면 만약 로직이 ```UIImageView.image = UIImage(...)``` 와 같이UI 요소를 업데이트 하는 로직이라면 캡슐화 되어있어서 어떻게 UIImageView 같은거에 접근 못하는거 아님?? 42 | 43 | ![Diagram](./Resources/diagram5.png) 44 | 45 | 네! 맞습니다. 46 | 47 | 캡슐화 된 로직이 완전히 독립적일 수는 없습니다. 48 | 49 | 그렇기에 아래 그림처럼 커맨드 패턴에서는 외부로부터 의존성을 주입받는 경우가 흔합니다. 50 | 51 | 여기에서 UIImageView처럼 주입받는 녀석을 보통 "리시버" 라고 부릅니다. 52 | 53 | ![Diagram](./Resources/diagram6.png) 54 | 55 |
56 | 57 | ## 구조 58 | 59 | 커맨드 패턴의 전통적인 구조를 보면서 앞서 설명한 것들을 조금 정리하는 시간을 가져보도록 하죠. 60 | 61 | ![Diagram](./Resources/diagram7.png) 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 | ![Simulator](./Resources/simulator.gif) 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 | ![diagram](./Resources/diagram.png) 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 | ![structure](./Resources/structure.png) 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 | ![emoji_picker](./Resources/emoji_picker.png) 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 | ![simulator](./Resources/simulator.gif) 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 | ![Diagram](./Resources/diagram.png) 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 | ![Simulator](./Resources/simulator.gif) 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 | ![첨부이미지](./Resources/TextEditorElement.png) 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 | ![Diagram](./Resources/diagram.png) 107 | 108 | 이 시점에서 이 복잡한 구조를 굳이 사용해야하나..? 라는 의심이 점점 드셨을 겁니다. 109 | 자 그러면 만약 이 상태에서 XML exporter로 구현한다고 해보겠습니다. 110 | 111 | 그럼 아래와같은 구조로 만들어볼 수 있겠죠? 112 | ![Diagram](./Resources/diagram2.png) 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 | ![Record](./Resources/record.gif) 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 | ![diagram](./Resources/diagram.png) 189 | 190 |
191 |
192 | 193 | 어떤가요? 이렇게 그룹화 된 추상화를 사용하게 되면 클라이언트 코드 간의 단단한 결합도 느슨하게 해주고 만약 새로운 테마를 추가한다고 하면 단순히 ThemeFactory 를 채택한 구현체를 만들어서 사용하면 되겠죠? 그러면서 자연스럽게 SRP, OCP도 챙길 수 있겠죠. 194 | 195 | 이처럼 어떤 제품군을 만들때 특히 추상 팩토리가 유용하게 사용됩니다. 196 | 197 |
198 | 199 | ## 실행 화면 200 | 201 | ![preview](./Resources/simulator_preview.gif) 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 | ![diagram](./Resources/diagram.png) 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 | ![preview](./Resources/simulator_preview.gif) 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 | ![Diagram](./Resources/diagram.png) 152 | 153 | 기존에 사용하던 객체를 추상화 해서 다른 객체도 추상화의 구현을 통해 비슷한 동작을 하도록 wrapping 해주는 방식, 이게 어댑터 패턴입니다. 154 | 간단하죠? 155 | 156 |
157 | 158 | ## 실행 화면 159 | 160 | 잘 보이진 않지만 NaverMapAdapter에서 AppleMapAdapter로 바꿔주는 것만으로 같은 동작을 동일하게 잘 수행하는 모습을 보실 수 있습니다. 161 | 162 | ![Preview](./Resources/preview.gif) 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 | ![preview](./Resources/preview.gif) 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 | ![Example](./Resources/facade_example_image.png) 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 | --------------------------------------------------------------------------------