Proxy pattern in iOS production
According to GOF
Proxy Provide a surrogate or placeholder for another object to control access to it. It makes consumers believe they’re talking to the real implementation.
The Proxy pattern provides a placeholder for another object to control its access. It's useful when you want to control or manage access to the original object, typically to add some kind of behavior, like lazy initialization, access control, or weak reference management.
Proxies vary in the degree to which they are implemented like a decorator. A protection proxy might be implemented exactly like a decorator. On the other hand, a remote proxy will not contain a direct reference to its real subject but only an indirect reference, such as “host ID and local address on host.” A virtual proxy will start off with an indirect reference such as a file name but will eventually obtain and use a direct reference._
let’s see theoretically how it works?
the proxy implements the same interface as the the object it wants to control access so the client thinks it’s talking to the real object. so behind the scenes it can do things like lazy initialization, access control, or weak reference management and forward the call to the real object.
lazy initialization
// Protocol defining the methods for the real subject
protocol ObjectLifecycleProtocol: AnyObject {
func performAction()
}
// Real subject class that we want to manage the lifecycle of
class RealObject: ObjectLifecycleProtocol {
init() {
print("RealObject initialized")
}
func performAction() {
print("RealObject performing action")
}
deinit {
print("RealObject deinitialized")
}
}
// Proxy class that will manage the lazy initializing of the real object
class ObjectProxy: ObjectLifecycleProtocol {
private var realObject: RealObject?
init() {
print("ObjectProxy initialized")
}
func performAction() {
// Lazy initialization of the real object
if realObject == nil {
realObject = RealObject()
}
realObject?.performAction()
}
deinit {
print("ObjectProxy deinitialized")
}
}
// Usage example
var proxy: ObjectProxy? = ObjectProxy() // Proxy initialized
proxy?.performAction() // Real object initialized and action performed
proxy = nil // Proxy deinitialized
- We have a real subject class
RealObject
that conforms toObjectLifecycleProtocol
. It represents the object whose lifecycle we want to manage. - We have a proxy class
ObjectProxy
that also conforms toObjectLifecycleProtocol
. - When
performAction()
is called on the proxy, it lazily initializes the real object if it hasn't been initialized yet and then forwards the method call to the real object. - We create an instance of the proxy, call
performAction()
, and then set the proxy tonil
to observe the initialization and deallocation of the proxy and real object.
access control with proxy
Access control refers to the ability to restrict the visibility and usage of certain parts of your code from other parts. In Swift, access control is managed using access control keywords like private
, fileprivate
, internal
, public
, and open
.
When using the Proxy pattern, access control plays an important role in determining which parts of your proxy and real subject can be accessed from outside the module.
Let’s consider a scenario where we have a real subject class RealObject
and a proxy class ObjectProxy
. We want to restrict direct access to the RealObject
from outside the module and only allow access through the proxy.
Here’s how you can apply access control with a proxy in Swift:
so the real question is how does the above translate to iOS development
we will use proxy patterns in iOS applications to prevent retain cycles effectively and cleanly.
// ModuleA.swift
// Real subject class with restricted access only within module
class RealObject {
private var value: Int
init(value: Int) {
self.value = value
}
func performAction() {
print("RealObject performing action with value \(value)")
}
}
// public Proxy class that is accessible from outside the module
public class ObjectProxy {
private var realObject: RealObject
init(value: Int) {
self.realObject = RealObject(value: value)
}
func performAction() {
realObject.performAction()
}
}
// Usage in another module
let proxy = ObjectProxy(value: 42)
proxy.performAction() // Can access RealObject through the proxy
- In
ModuleA.swift
, we define theRealObject
class with access control set toprivate
. This means thatRealObject
can only be accessed from within the same source file and not from outside the module. - We define the
ObjectProxy
class with access control set topublic
. This allowsObjectProxy
to be accessed from outside the module. - The
ObjectProxy
class creates and manages an instance ofRealObject
internally, but theRealObject
itself is not directly accessible from outside the module. - When using the proxy from another module, we can only interact with the
RealObject
through the proxy, ensuring that access to the real subject is controlled.
By applying access control in this way, we ensure that the real subject’s implementation details are encapsulated and only accessible through the proxy, promoting better encapsulation and modularization of code.
weak reference counting
Weak reference counting is a mechanism used in programming languages, including Swift, to manage memory and avoid strong reference cycles, also known as retain cycles.
In Swift, objects are held in memory as long as there is at least one strong reference to them. A strong reference means that an object cannot be deallocated as long as there is at least one strong reference pointing to it. This is the default behavior in Swift when you create an instance of a class.
However, strong references can lead to retain cycles, where two or more objects hold strong references to each other, preventing them from being deallocated even when they are no longer needed. This can cause memory leaks and degrade app performance.
// Protocol defining the methods for the real subject
protocol ObjectProtocol: AnyObject {
func performAction()
}
// Real subject class that we want to manage the lifecycle of
class RealObject: ObjectProtocol {
func performAction() {
print("RealObject performing action")
}
}
// Proxy class that will manage the reference counting of the real object
class ObjectProxy: ObjectProtocol {
// Weak reference to the real object
weak var realObject: RealObject?
init(realObject: RealObject) {
self.realObject = realObject
}
func performAction() {
realObject?.performAction()
}
}
// Usage example
var realObject: RealObject? = RealObject()
var proxy: ObjectProxy? = ObjectProxy(realObject: realObject!)
// Perform action through the proxy
proxy?.performAction() // Output: RealObject performing action
// Set the real object and proxy to nil to observe deallocation
realObject = nil
proxy = nil
we have a proxy class ObjectProxy
that acts as a surrogate for the real object RealObject
. We want to avoid creating a retain cycle where the real object holds a strong reference to the proxy, and the proxy holds a weak reference to the real object.
iOS Example: Using a proxy in the MVP UI design pattern
Problem
let us take a simple example from the MVP UI pattern where we simulate a simple request and show it on the View Controller.
class loader{
func load() -> [String] {
return["a","b","c","d"]
}
}
// for presentation
struct ViewModel{
let message:[String]
}
protocol AbstractView{
func display(viewModel:ViewModel)
}
class Presenter{
let view:AbstractView
let loader:loader
init(view: AbstractView, loader: loader) {
self.view = view
self.loader = loader
}
func fetch() {
print("fetch")
let a = loader.load()
self.view.display(viewModel: ViewModel(message: a))
}
}
class ViewController: UIViewController, AbstractView {
var presenter: Presenter?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
presenter?.fetch()
}
func display(viewModel: ViewModel) {
print(viewModel.message)
}
}
//composer for instantiating
public class Composer{
public static func instantiate() -> UIViewController {
let vc = ViewController()
let loader = loader()
let presenter = Presenter(view:vc, loader: loader)
vc.presenter = presenter
return vc
}
}
#Preview{
Composer.instantiate()
}
as you can see in the diagram below both the VC and the presenter reference each other which creates a memory leak.
let’s see it using a test. As you can see we get a memory leak as the ViewController doesn’t get deallocated.
Solution: Proxy
Proxy conforms to the same interface as the viewController and acts as its surrogate by using a weak reference whereby managing the reference counting of the real object.
import Foundation
class WeakRefProxy<T: AnyObject> {
weak var object: T?
init(_ object: T) {
self.object = object
}
}
extension WeakRefProxy: AbstractView where T: AbstractView {
func display(viewModel: ViewModel) {
object?.display(viewModel: viewModel)
}
}
now let us see how we use the proxy
public class Composer{
public static func instantiate() -> UIViewController {
let vc = ViewController()
let loader = loader()
//the proxy acting as surrogate here for the viewcontroller
let presenter = Presenter(view: WeakRefProxy(vc), loader: loader)
vc.presenter = presenter
return vc
}
}
let’s verify using our tests if we get a memory leak and viola we don’t
Conclusion
The proxy Pattern promotes cleaner, more modular, and maintainable software architectures, ultimately enhancing the quality and robustness of the system.
Reference:
Github: https://github.com/abdahad1996/DesignPatterns_Bootcamp