Share this
UIKit and Decorator Design Pattern
by Seven Peaks on Feb 11, 2021 5:20:00 PM
How to use UIkit in SwiftUI
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:
- Move subview(type:) to the UIView extension
- Add ViewDecorator protocol
- Add another generic function viewDecorator(type:) to the UIView extension that would search and create a new decorator if it’s not found.
- Conforms HyperlinkDecorator to ViewDecorator protocol
- UILabel extension
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 }
}
}
Share this
- FinTech (13)
- Career (12)
- Expert Spotlight (11)
- Thought Leadership (11)
- Product Growth (9)
- Software Development (9)
- Product Design (7)
- Data and Analytics (5)
- Design Thinking (5)
- InsurTech (5)
- QA (5)
- Agile (4)
- Cloud (4)
- Company (4)
- Digital Transformation (4)
- Financial Inclusion (4)
- JavaScript (4)
- Seven Peaks Insights (4)
- Trend (4)
- UX Design (4)
- UX Research (4)
- .NET (3)
- Android Developer (3)
- Android Development (3)
- Azure (3)
- Banking (3)
- CSR (3)
- Data (3)
- DevOps (3)
- IoT (3)
- Product-Centric Mindset (3)
- AI (2)
- CDP (2)
- Cloud Development (2)
- Customer Data Platform (2)
- Digital Product (2)
- E-wallet (2)
- Expat (2)
- Hybrid App (2)
- Kotlin (2)
- Product Owner (2)
- Software Tester (2)
- SwiftUI (2)
- UI (2)
- UX (2)
- UX Writing (2)
- Visual Design (2)
- iOS Development (2)
- .NET 8 (1)
- 2023 (1)
- 4IR (1)
- 5G (1)
- API (1)
- Agritech (1)
- AndroidX Biometric (1)
- App Development (1)
- Azure OpenAI Service (1)
- Backend (1)
- Brand Loyalty (1)
- CI/CD (1)
- Conversions (1)
- Cross-Platform Application (1)
- Dashboard (1)
- Digital (1)
- Digital Healthcare (1)
- Digital ID (1)
- Digital Landscape (1)
- Engineer (1)
- Expert Interview (1)
- Fiddler (1)
- Figma (1)
- Financial Times (1)
- GraphQL (1)
- Hilt (1)
- IT outsourcing (1)
- KYC (1)
- MVP (1)
- MVVM (1)
- Metaverse (1)
- Morphosis (1)
- Native App (1)
- New C# (1)
- Newsletter (1)
- Node.js (1)
- Payment (1)
- Platform Engineer (1)
- Platform Engineering Jobs (1)
- Platform Engineering Services (1)
- Project Manager (1)
- Rabbit MQ (1)
- React (1)
- ReactJS (1)
- Stripe (1)
- Super App (1)
- Turnkey (1)
- UIkit (1)
- UX Strategy (1)
- Web 3.0 (1)
- Web-Debugging Tool (1)
- December 2024 (2)
- November 2024 (2)
- September 2024 (4)
- August 2024 (3)
- July 2024 (6)
- April 2024 (1)
- March 2024 (7)
- February 2024 (14)
- January 2024 (14)
- December 2023 (9)
- November 2023 (9)
- October 2023 (2)
- September 2023 (6)
- August 2023 (6)
- June 2023 (4)
- May 2023 (4)
- April 2023 (1)
- March 2023 (1)
- November 2022 (1)
- August 2022 (4)
- July 2022 (1)
- June 2022 (6)
- April 2022 (6)
- March 2022 (4)
- February 2022 (8)
- January 2022 (4)
- December 2021 (1)
- November 2021 (2)
- October 2021 (2)
- September 2021 (1)
- August 2021 (3)
- July 2021 (1)
- June 2021 (2)
- May 2021 (1)
- March 2021 (4)
- February 2021 (5)
- December 2020 (4)
- November 2020 (1)
- June 2020 (1)
- April 2020 (1)