Seven Peaks Insights

UIKit and Decorator Design Pattern

How to use UIkit in SwiftUI

 
 
Andrei-Photo
 

In this series of “Design patterns for iOS“, our senior iOS Developer, Andrey Soloviev, will go through all common design patterns and show you how to use UIkit in SwiftUI. He will also adhere to Swift’s best practices and point them out.

Design patterns are general solutions to commonly occurring problems that give you more freedom and confidence in choosing solutions to a specific problem. 

In this part, I’ve chosen to have a simple example as I want to focus more on the decorator design pattern itself and how we can abuse UIView behavior to make the usage even simpler.

The next part would have a bit more difficult example but the main principle of using Decorator pattern will remain the same. Now when we have this definition lets implement a simple decorator that takes URL property and make UILabel clickable:

import UIKit
class HyperlinkDecorator {
var url: URL?
var label: UILabel
required init(label: UILabel) {
self.label = label
label.isUserInteractionEnabled = true label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
}
@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url)
}
}
}

 

As you can see the implementation is straightforward. It takes UILabel, adds UITapGestureRecognizer, and on tap, it opens the URL. We keep the URL optional so it could be changed anytime. Notice that we defined HyperlinkDecorator as a class, not a struct. That’s because it has a property with a reference type.

In this example, we don’t really need to keep a reference to UILabel, but let’s leave it there. Now if we want to use our decorator we need to create and store it somewhere. UIGestureRecognizer does not retain its target, so if we don’t store the reference to HyperlinkDecorator it will get deallocated straight away.

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!
var hyperlinkDecorator: HyperlinkDecorator?
override func viewDidLoad() {
super.viewDidLoad()
hyperlinkDecorator = HyperlinkDecorator(label: hyperLabel)
hyperlinkDecorator?.url = URL(string: “https://medium.com”)
}
}

 

I don’t like to store references to objects that I don’t really use. If we have many decorators then we would have to create an additional property for each of them and the code gets harder to read and maintain.

We can also create an array or set of such objects and store them all there, so they will be deallocated together with HyperlinkViewController.

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!
var decorators = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
let hyperlinkDecorator = HyperlinkDecorator(label: hyperLabel)
hyperlinkDecorator.url = URL(string: “https://medium.com”)
decorators.append(hyperlinkDecorator)
}
}

 

“Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors”  Refactoring.Guru

 

It may look better but I don’t like it either. That’s how I came to the next solution that I still use to get rid of such references by working with UIKit components.

We subclass HyperlinkDecorator from UIView, make it zero size and add it as a subview of UILabel, so it will live and die together with UILabel.

We also set a weak reference to UILabel to avoid retain cycle and memory leaks

import UIKit

class HyperlinkDecorator: UIView {
var url: URL?
weak var label: UILabel?

required init(label: UILabel) {
self.label = label
super.init(frame: .zero)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
label.addSubview(self)
}

// It’s required for UIView subclasses
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}

@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}

   

The usage remains the same but we no longer need to keep the reference.

That looks okay, but we can do it even better. Let’s make an extension for UILabel that hides this implementation and provides us convenient URL property.

extension UILabel {
var url: URL? {
get {
subview(type: HyperlinkDecorator.self)?.url
}
set {
let decorator = subview(type: HyperlinkDecorator.self) ?? HyperlinkDecorator(label: self)
decorator.url = newValue
}
}
}

extension UILabel {
func subview(type: V.Type) -> V? {
return subviews.first(where: { $0 is V }) as? V
}
} 

 

There is generic function subview(type:) that searches for the subview of a specific type. I could use tag but I prefer this version as the code looks cleaner and we don’t need to worry about assigning unique tag value. And that’s how we can use it now.

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
hyperLabel.url = URL(string: “https://medium.com”)
}
}

 

Now it looks much better and I would actually stop here, but still can refactor the UILabel extension and make it easier to add new properties and new decorators.

That’s what I’m going to do:

It seems not many changes, let’s do it.

Moving function subview(type:) to UIView:

import UIKit

extension UIView {
func subview(type: V.Type = V.self ) -> V? {
return subviews.first(where: { $0 is V }) as? V
}
}

 

View decorator should be subclass of UIView and should take any generic object of UIView or its subclass. Exact class of this generic object would be defined inside each view decorator.

import UIKit

protocol ViewDecorator: UIView {
associatedtype View: UIView
init(object: View)
}

 

Now let’s move code of searching and creation of new decorator from UILabel extension to separate generic function inside UIView extension:

import UIKit

extension UIView {
func viewDecorator(type: V.Type = V.self) -> V {
return subview(type: V.self) ?? V(object: self as! V.View)
}
}

 

This function works exactly the same as before only now it’s generic. If you’re unable to understand how it works, please read more about generics.

Conforming HyperlinkDecorator to ViewDecorator is simple, just add protocol confirmation and replace init(label:) with init(object:)

Here’s the complete code for HyperlinkDecorator

 

import UIKit

class HyperlinkDecorator: UIView, ViewDecorator {
var url: URL?
weak var label: UILabel?

required init(object label: UILabel) {
self.label = label
super.init(frame: .zero)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
label.addSubview(self)
}

// It’s required to have for UIView subclasses
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}

@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
} 

 

And finally updated extension of UILabel, the reason for all of these steps.
That’s it, that’s all. You can find the complete example project here.

import UIKit

extension UILabel {
var url: URL? {
get { viewDecorator(type: HyperlinkDecorator.self).url }
set { viewDecorator(type: HyperlinkDecorator.self).url = newValue }
}
}

Do you need senior iOS developers to design & develop your app?
Get in touch below to see how we can help you.
Get in touch with us