The Adapter pattern allows incompatible interfaces to work together by converting the interface of one class into an interface expected by the client. Itโs like a power plug adapter that lets a U.S. charger work in a European socket.
protocol MediaPlayer {
func play(filename: String)
}
class MP3Player: MediaPlayer {
func play(filename: String) {
print("๐ต Playing MP3 file: \(filename)")
}
}
// Existing incompatible class
class VLCPlayer {
func playVLC(file: String) {
print("๐ฌ Playing VLC file: \(file)")
}
}
// Adapter that makes VLCPlayer compatible
class VLCAdapter: MediaPlayer {
private let vlcPlayer = VLCPlayer()
func play(filename: String) {
vlcPlayer.playVLC(file: filename)
}
}
let mp3 = MP3Player()
mp3.play(filename: "song.mp3")
let vlcAdapter = VLCAdapter()
vlcAdapter.play(filename: "movie.vlc")
iOS uses the Adapter pattern in many places:
URLSession vs. Third-party Networking (like Alamofire)
You may create an adapter that converts URLSession
calls to match a common protocol.
Bridging Swift with Objective-C classes Swift automatically creates adapters (known as bridging headers) to allow compatibility.
UICollectionViewDiffableDataSource adapts legacy data source logic into modern diffable models.
Component | Role |
---|---|
Target |
The interface the client expects |
Adaptee |
The existing, incompatible interface |
Adapter |
Translates Adaptee methods to Target interface |
๐ The Adapter pattern promotes code reusability by allowing legacy or third-party code to be reused in modern systems without modification.
The Bridge Pattern decouples an abstraction from its implementation so that the two can vary independently. Itโs especially useful when you have multiple dimensions of variation in your system.
Suppose youโre building an e-commerce app that needs to support:
Hard-coding all combinations leads to a mess. With the Bridge pattern, we separate the payment logic (abstraction) from gateway implementations, keeping your system extensible and maintainable.
// MARK: - Implementation Interface
protocol PaymentGateway {
func processPayment(amount: Double)
}
// MARK: - Concrete Implementations
class StripeGateway: PaymentGateway {
func processPayment(amount: Double) {
print("Processing โน\(amount) via Stripe.")
}
}
class PayPalGateway: PaymentGateway {
func processPayment(amount: Double) {
print("Processing โน\(amount) via PayPal.")
}
}
// MARK: - Abstraction
class Payment {
let gateway: PaymentGateway
init(gateway: PaymentGateway) {
self.gateway = gateway
}
func makePayment(amount: Double) {
gateway.processPayment(amount: amount)
}
}
// MARK: - Refined Abstractions
class SubscriptionPayment: Payment {
override func makePayment(amount: Double) {
print("Handling subscription logic...")
super.makePayment(amount: amount)
}
}
class OneTimePayment: Payment {
override func makePayment(amount: Double) {
print("Handling one-time payment logic...")
super.makePayment(amount: amount)
}
}
let stripe = StripeGateway()
let paypal = PayPalGateway()
let monthlySub = SubscriptionPayment(gateway: stripe)
monthlySub.makePayment(amount: 999.0)
// Output:
// Handling subscription logic...
// Processing โน999.0 via Stripe.
let oneTime = OneTimePayment(gateway: paypal)
oneTime.makePayment(amount: 499.0)
// Output:
// Handling one-time payment logic...
// Processing โน499.0 via PayPal.
UIView
uses a CALayer
underneath โ a form of bridge between visual abstraction and rendering engine.NSURLSession
is an abstraction that can delegate network behavior to different URLProtocol
implementations.View
is protocol-based but rendering is handled separately by the system โ another subtle bridge.The Composite Pattern lets you treat individual objects and compositions of objects uniformly. Itโs perfect for representing hierarchical, tree-like structures where you want to work with both single items and groups in the same way.
File System
File
and Folder
can be treated using the same operations like getSize()
, displayName()
, or delete()
.UIView
can be:
UILabel
, UIImageView
)UIStackView
containing other views).addSubview()
and .removeFromSuperview()
.// Component Protocol
protocol FileSystemItem {
var name: String { get }
func display(indentation: String)
}
// Leaf
struct File: FileSystemItem {
let name: String
func display(indentation: String) {
print("\(indentation)๐ \(name)")
}
}
// Composite
class Folder: FileSystemItem {
let name: String
private var items: [FileSystemItem] = []
init(name: String) {
self.name = name
}
func add(_ item: FileSystemItem) {
items.append(item)
}
func display(indentation: String = "") {
print("\(indentation)๐ \(name)")
items.forEach { $0.display(indentation: indentation + " ") }
}
}
// Usage
let file1 = File(name: "Resume.pdf")
let file2 = File(name: "CoverLetter.pdf")
let docsFolder = Folder(name: "Documents")
docsFolder.add(file1)
docsFolder.add(file2)
let rootFolder = Folder(name: "Home")
rootFolder.add(docsFolder)
rootFolder.display()
Output:
๐ Home
๐ Documents
๐ Resume.pdf
๐ CoverLetter.pdf
๐ง Definition The Decorator Pattern allows you to add new behavior to an object dynamically without modifying its existing code. It wraps the original object inside another object that adds the extra functionality โ just like adding toppings to ice cream without changing the ice cream itself. This provides a flexible alternative to using inheritance to modify behaviour ๐ฑ iOS Real Example
.foregroundColor, .font, .underline
..padding(), .background(), .shadow()
Here are Top 5 real-world & iOS relatable examples for the ๐ง Decorator Pattern you can choose from:
.padding()
, .background()
, .shadow()
as chained decorators.The Decorator Pattern is perfect for a streaming player: you keep a simple base player implementation and attach features (subtitles, watermark overlays, analytics, adaptive bitrate, etc.) at runtime โ without changing the player itself. This yields a highly modular and scalable architecture: add/remove features independently, compose them in any order, and test each feature in isolation.
Player
with decorators that add behavior before/after (or around) the core play()
/ stop()
/ seek()
operations.PlayerWithSubtitlesAndAnalytics
combinatorial problem).Player
protocol โ the target interface.BasicPlayer
โ the concrete core player (plays video).PlayerDecorator
โ base decorator that forwards calls.SubtitlesDecorator
, WatermarkDecorator
, AnalyticsDecorator
, AdaptiveBitrateDecorator
.import Foundation
import CoreGraphics
// MARK: - Player Protocol (Component)
protocol Player {
func play(url: URL)
func stop()
func seek(to seconds: Double)
}
// MARK: - Basic Player (Concrete Component)
class BasicPlayer: Player {
func play(url: URL) {
print("โถ๏ธ Playing video at \(url.absoluteString)")
// actual playback logic (AVPlayer, etc.) goes here
}
func stop() {
print("โน Stopped playback")
}
func seek(to seconds: Double) {
print("โฑ Seek to \(seconds)s")
}
}
// MARK: - Decorator Base
class PlayerDecorator: Player {
private let wrapped: Player
init(wrapping player: Player) {
self.wrapped = player
}
func play(url: URL) {
wrapped.play(url: url)
}
func stop() {
wrapped.stop()
}
func seek(to seconds: Double) {
wrapped.seek(to: seconds)
}
}
// MARK: - Subtitles Decorator
class SubtitlesDecorator: PlayerDecorator {
private let subtitleURL: URL
init(wrapping player: Player, subtitleURL: URL) {
self.subtitleURL = subtitleURL
super.init(wrapping: player)
}
override func play(url: URL) {
loadSubtitles()
super.play(url: url)
}
private func loadSubtitles() {
print("๐ฌ Loading subtitles from \(subtitleURL.lastPathComponent)")
// parse and attach subtitle rendering to player UI
}
}
// MARK: - Watermark Decorator
class WatermarkDecorator: PlayerDecorator {
private let watermarkText: String
private let position: CGPoint
init(wrapping player: Player, text: String, position: CGPoint = .init(x: 10, y: 10)) {
self.watermarkText = text
self.position = position
super.init(wrapping: player)
}
override func play(url: URL) {
addWatermark()
super.play(url: url)
}
private func addWatermark() {
print("๐ Adding watermark '\(watermarkText)' at \(position)")
// overlay watermark view on player layer
}
}
// MARK: - Analytics Decorator
class AnalyticsDecorator: PlayerDecorator {
override func play(url: URL) {
trackEvent("play", url: url)
super.play(url: url)
}
override func stop() {
trackEvent("stop", url: nil)
super.stop()
}
override func seek(to seconds: Double) {
trackEvent("seek", url: nil, extra: ["position": seconds])
super.seek(to: seconds)
}
private func trackEvent(_ name: String, url: URL?, extra: [String: Any]? = nil) {
print("๐ Analytics - event: \(name), url: \(url?.lastPathComponent ?? "-"), extra: \(extra ?? [:])")
// forward to analytics backend (batching, retries)
}
}
// MARK: - Adaptive Bitrate Decorator
class AdaptiveBitrateDecorator: PlayerDecorator {
override func play(url: URL) {
selectInitialQuality()
super.play(url: url)
monitorNetworkAndAdapt()
}
private func selectInitialQuality() {
print("โ๏ธ Selecting initial bitrate based on current conditions")
}
private func monitorNetworkAndAdapt() {
print("โ๏ธ Monitoring network and adapting bitrate (simulated)")
// In real app: observe bandwidth, switch streams or set AVPlayer item variants
}
}
let url = URL(string: "https://cdn.example.com/movie.m3u8")!
let subtitleURL = URL(string: "https://cdn.example.com/movie.en.vtt")!
// Start with basic player
var player: Player = BasicPlayer()
// Dynamically compose decorators (order can matter: analytics wrapped outside will see all events)
player = SubtitlesDecorator(wrapping: player, subtitleURL: subtitleURL)
player = WatermarkDecorator(wrapping: player, text: "MyBrand")
player = AdaptiveBitrateDecorator(wrapping: player)
player = AnalyticsDecorator(wrapping: player)
// Use player
player.play(url: url)
player.seek(to: 120)
player.stop()
Sample Console Output:
๐ฌ Loading subtitles from movie.en.vtt
๐ Adding watermark 'MyBrand' at (10.0, 10.0)
โ๏ธ Selecting initial bitrate based on current conditions
๐ Analytics - event: play, url: movie.m3u8, extra: [:]
โถ๏ธ Playing video at https://cdn.example.com/movie.m3u8
๐ Analytics - event: seek, url: -, extra: ["position": 120]
โน Stopped playback
๐ Analytics - event: stop, url: -
๐งฐ When to Use
Definition: The Faรงade pattern provides a unified, simplified interface to a set of complex subsystems, making them easier to use without exposing the underlying complexity. It acts as a โfront deskโ that delegates requests to the appropriate backend services.
Imagine a social media app where users can upload videos with multiple processing steps:
Without Faรงade, the ViewModel or Controller would need to orchestrate all these steps individually, increasing complexity and making the code harder to maintain.
With Faรงade:
import Foundation
// MARK: - Subsystem Placeholders
struct VideoCompressor {
func compress(_ url: URL) -> URL {
print("Compressing video...")
return url // Placeholder
}
}
struct VideoEncoder {
func encode(_ url: URL) -> URL {
print("Encoding video...")
return url // Placeholder
}
}
struct ThumbnailGenerator {
func generate(from url: URL) {
print("Generating thumbnails...")
}
}
struct MetadataExtractor {
func extract(from url: URL) {
print("Extracting metadata...")
}
}
struct CDNUploader {
func upload(_ url: URL) {
print("Uploading to CDN...")
}
}
// MARK: - Facade
class MediaUploadFacade {
private let compressor = VideoCompressor()
private let encoder = VideoEncoder()
private let thumbnailGen = ThumbnailGenerator()
private let metadataExtractor = MetadataExtractor()
private let uploader = CDNUploader()
func uploadVideo(_ fileURL: URL) {
let compressed = compressor.compress(fileURL)
let encoded = encoder.encode(compressed)
thumbnailGen.generate(from: encoded)
metadataExtractor.extract(from: encoded)
uploader.upload(encoded)
}
}
// MARK: - Usage
let videoURL = URL(fileURLWithPath: "/path/to/video.mp4")
let mediaUploader = MediaUploadFacade()
mediaUploader.uploadVideo(videoURL)
let mediaUploader = MediaUploadFacade()
mediaUploader.uploadVideo(videoURL)
โ Benefits:
In a production app like TikTok or Instagram:
VideoCompressor
with a more advanced one)๐ Definition - The Flyweight pattern minimizes memory usage by sharing common, immutable data between multiple objects, instead of duplicating it.โจItโs especially useful when working with a large number of similar objects.
Hereโs a focused Flyweight Pattern implementation for E-commerce Product Options with memory impact analysis:
## Example: Text Formatting in a Document Editor
import Foundation
// Flyweight protocol
protocol TextFormatting {
func apply(to character: Character)
}
// Concrete Flyweight
class CharacterFormatting: TextFormatting, Hashable {
let fontName: String
let fontSize: CGFloat
let color: UIColor
let isBold: Bool
let isItalic: Bool
init(fontName: String, fontSize: CGFloat, color: UIColor, isBold: Bool, isItalic: Bool) {
self.fontName = fontName
self.fontSize = fontSize
self.color = color
self.isBold = isBold
self.isItalic = isItalic
}
func apply(to character: Character) {
print("Displaying '\(character)' with: \(fontName), \(fontSize)pt, \(color), bold: \(isBold), italic: \(isItalic)")
}
// Hashable conformance for use in a Set/Dictionary
func hash(into hasher: inout Hasher) {
hasher.combine(fontName)
hasher.combine(fontSize)
hasher.combine(color)
hasher.combine(isBold)
hasher.combine(isItalic)
}
static func == (lhs: CharacterFormatting, rhs: CharacterFormatting) -> Bool {
return lhs.fontName == rhs.fontName &&
lhs.fontSize == rhs.fontSize &&
lhs.color == rhs.color &&
lhs.isBold == rhs.isBold &&
lhs.isItalic == rhs.isItalic
}
}
// Flyweight Factory
class FormattingFactory {
private var formats: [CharacterFormatting] = []
func getFormatting(fontName: String, fontSize: CGFloat, color: UIColor, isBold: Bool, isItalic: Bool) -> CharacterFormatting {
let newFormat = CharacterFormatting(
fontName: fontName,
fontSize: fontSize,
color: color,
isBold: isBold,
isItalic: isItalic
)
if let existingFormat = formats.first(where: { $0 == newFormat }) {
return existingFormat
}
formats.append(newFormat)
return newFormat
}
var totalFormatsCreated: Int {
return formats.count
}
}
// Client code - Document that uses formatted characters
struct FormattedCharacter {
let character: Character
let formatting: CharacterFormatting
func display() {
formatting.apply(to: character)
}
}
class Document {
private var characters: [FormattedCharacter] = []
private let formattingFactory = FormattingFactory()
func addCharacter(_ char: Character, fontName: String, fontSize: CGFloat, color: UIColor, isBold: Bool, isItalic: Bool) {
let formatting = formattingFactory.getFormatting(
fontName: fontName,
fontSize: fontSize,
color: color,
isBold: isBold,
isItalic: isItalic
)
let formattedChar = FormattedCharacter(character: char, formatting: formatting)
characters.append(formattedChar)
}
func display() {
characters.forEach { $0.display() }
}
func totalUniqueFormats() -> Int {
return formattingFactory.totalFormatsCreated
}
}
// Usage Example
let document = Document()
// Add many characters with different formatting
document.addCharacter("H", fontName: "Helvetica", fontSize: 12, color: .black, isBold: true, isItalic: false)
document.addCharacter("e", fontName: "Helvetica", fontSize: 12, color: .black, isBold: true, isItalic: false)
document.addCharacter("l", fontName: "Helvetica", fontSize: 12, color: .black, isBold: true, isItalic: false)
document.addCharacter("l", fontName: "Helvetica", fontSize: 12, color: .black, isBold: true, isItalic: false)
document.addCharacter("o", fontName: "Helvetica", fontSize: 12, color: .black, isBold: true, isItalic: false)
document.addCharacter(" ", fontName: "Helvetica", fontSize: 12, color: .black, isBold: false, isItalic: false)
document.addCharacter("W", fontName: "Times New Roman", fontSize: 14, color: .blue, isBold: false, isItalic: true)
document.addCharacter("o", fontName: "Times New Roman", fontSize: 14, color: .blue, isBold: false, isItalic: true)
document.addCharacter("r", fontName: "Times New Roman", fontSize: 14, color: .blue, isBold: false, isItalic: true)
document.addCharacter("l", fontName: "Times New Roman", fontSize: 14, color: .blue, isBold: false, isItalic: true)
document.addCharacter("d", fontName: "Times New Roman", fontSize: 14, color: .blue, isBold: false, isItalic: true)
document.display()
print("Total unique formats created: \(document.totalUniqueFormats())")
This pattern is especially useful in iOS/macOS apps where you might have many UI elements with similar styling.
A Protection Proxy controls access to an object by checking permissions, authentication, or other access rules before allowing operations on it. It acts like a gatekeeper โ requests pass through it, and it decides whether to forward them to the real object.
Think of a nightclub bouncer.
Imagine an admin settings screen in an iOS app:
protocol AdminSettings {
func updateSystemConfig()
}
// Real object
class RealAdminSettings: AdminSettings {
func updateSystemConfig() {
print("โ
System configuration updated successfully!")
}
}
// Protection Proxy
class AdminSettingsProxy: AdminSettings {
private let realSettings = RealAdminSettings()
private let currentUserRole: String
init(userRole: String) {
self.currentUserRole = userRole
}
func updateSystemConfig() {
guard currentUserRole == "admin" else {
print("โ Access Denied: You are not authorized to update system configuration.")
return
}
realSettings.updateSystemConfig()
}
}
// Usage
let adminUser = AdminSettingsProxy(userRole: "admin")
adminUser.updateSystemConfig() // โ
Allowed
let guestUser = AdminSettingsProxy(userRole: "guest")
guestUser.updateSystemConfig() // โ Denied
A Virtual Proxy acts as a placeholder for a resource-heavy object, creating it only when itโs actually needed. Itโs like saying: โDonโt make the expensive thing until someone actually asks for it.โ
Think of a restaurant menu picture:
In an iOS photo gallery app:
protocol HighResImage {
func display()
}
// Real heavy object
class RealHighResImage: HighResImage {
private let filename: String
init(filename: String) {
self.filename = filename
loadFromDisk()
}
private func loadFromDisk() {
print("๐ Loading high-res image from disk: \(filename)")
}
func display() {
print("๐ผ Displaying high-res image: \(filename)")
}
}
// Virtual Proxy
class ImageProxy: HighResImage {
private let filename: String
private var realImage: RealHighResImage?
init(filename: String) {
self.filename = filename
}
func display() {
if realImage == nil {
realImage = RealHighResImage(filename: filename)
}
realImage?.display()
}
}
// Usage
let image = ImageProxy(filename: "big_photo.png")
print("๐ฑ App loaded, image not yet displayed.")
image.display() // Loads on demand