By Andrew Jaffee, translator: BigLuo; Proofreading: PMST, NUMbbbbb; Finalized: Forelax

“Gang of Four” (” GoF “) Erich Gamma, Richard Helm, Ralph Johonson, and John Vlissides in their “Design Patterns: Some 23 classic design patterns are organized in “Fundamentals of Object-oriented Software Design Reuse”. This article introduces two creational patterns summarized by GoF: the factory approach and the singleton approach.

Software developers have been trying to simulate real-world scenarios, creating tools to enhance the human experience. Wealth management tools, such as banking apps and shopping AIDS such as Amazon or eBay, have certainly made life easier for consumers than it was a decade ago. Review how far we’ve come. As applications have become more powerful and easy to use, application development has become more complex.

So developers have created a set of best practices. Popular names include object-oriented programming, protocol oriented programming, value Semantics, local inference, which breaks large chunks of code into smaller pieces with well-defined interfaces (such as using Swift extensions), and syntax-sugar. And one of the most important practices I didn’t mention, but one that deserves attention, is the use of design patterns.

Design patterns

Design patterns are an important tool for developers to manage software complexity. As a common template technique, it conceptualizes similar, recurring, easily identifiable problems in software. Use it as a best practice for everyday programming scenarios such as creating a class cluster-related object without knowing the implementation details of the class cluster. Design patterns are mainly used in problem scenarios that occur frequently. They are used frequently because these problems are common, so let me give you a specific example to help you understand.

Design patterns do not discuss specific problems such as “how to iterate over a Swift array of 11 ints”. For this type of problem, GoF defines the Iterator Pattern, which is a general Pattern that describes how to traverse a list of data without determining the data type. Design patterns are not language coding. It is a set of practical guidelines for solving problems in the same software scenario.

Remember, I talked about the Model-View-ViewModel or MVVM in AppCoda and the famous model-View-Controller or MVC design pattern, Both modes are popular with Apple and iOS developers.

These two modes are generally used throughout the application. MVVM and MVC are architectural design patterns used to separate the UI from application data code and presentation logic (e.g. MVC) and application data from the core data flow or business logic (e.g. MVVM). The GoF design pattern is more specific in nature, aiming to solve specific problems based on program code. You may use 3, 7, or 12 GoF design patterns in an application. In addition to the iterator example, the proxy pattern is another good example of a design pattern, although it is not covered in detail among the 23 design patterns listed in GoF.

While GoF’s book stands out as a bible for many developers, it also has its detractors, a topic we discuss at the end of this article.

Categories of design patterns

GoF categorizes 23 design patterns into three categories: “creative,” “structural,” and “behavioral.” This tutorial discusses two of the creation pattern categories (factory pattern and singleton). Like the implementation of instance objects and classes, patterns serve to make the creation of complex objects simple, easy to understand, and easy to maintain, hiding details.

** Hiding complexity (encapsulation) ** is one of the primary goals of a smart programmer. For example, object-oriented (OOP) classes can provide very complex, powerful, and mature functions without needing to know anything about how things work within classes. In the creative mode, the developer doesn’t even need to know the properties and methods of the class, but if needed, the programmer can see the interface – in the protocol in Swift – or extend those classes of interest. You’ll see what I mean in my first “factory approach” example.

Factory method design patterns

If you’ve explored GoF design patterns or spent a lot of time in the OOP world, you’ve probably heard of at least “abstract factory,” “Factory,” or “factory method” patterns. The “exact” naming can be much debated, but the closest naming I’ll show you is the factory pattern.

In this example, you create objects through the factory method without needing to know the constructor of the class or any information about the class and its hierarchy. This brings great convenience. The UI and its associated functionality can be created with a small amount of code. My factory Method project example, available for download on GitHub, shows how easy it is to work with objects in a complex class hierarchy.

Most successful apps have a consistent theme. To keep your app theme consistent, assume that all shapes in your app are the same color and size, so that they are consistent with the theme — that is, branding. These graphics are good for custom buttons or as background images for the login process.

Suppose the design team agrees to use my code as the background image for the app’s theme. Let’s take a look at my code, including protocols, class structures, and factory methods (which UI developers don’t need to care about).

The shapeFactory. swift file is a protocol for drawing shapes within a view controller. Since it can be used for various purposes, its access level is public:

// These values are pre-selected by the graphic design team
let defaultHeight = 200
let defaultColor = UIColor.blue
 
protocol HelperViewFactoryProtocol {
    
    func configure(a)
    func position(a)
    func display(a)
    var height: Int { get }
    var view: UIView { get }
    var parentView: UIView { get}}Copy the code

Remember? The UIView class has a default rectangle property frame, so I can easily create the shape base class Square:

fileprivate class Square: HelperViewFactoryProtocol {
    
    let height: Int
    let parentView: UIView
    var view: UIView
    
    init(height: Int = defaultHeight, parentView: UIView) {
        
        self.height = height
        self.parentView = parentView
        view = UIView()}func configure(a) {
        
        let frame = CGRect(x: 0, y: 0, width: height, height: height)
        view.frame = frame
        view.backgroundColor = defaultColor
        
    }
    
    func position(a) {
        
        view.center = parentView.center
        
    }
 
    func display(a) {
        
        configure()
        position()
        parentView.addSubview(view)
        
    }
    
} 
Copy the code

Notice that I built my reusable code around OOP design ideas, which made the Shape hierarchy simpler and maintainable. The Circle and Rectangle classes are specializations of the Square class (plus you can see how easy it is to draw a Circle from a Square).

fileprivate class Circle : Square {
    
    override func configure(a) {
        
        super.configure()
        
        view.layer.cornerRadius = view.frame.width / 2
        view.layer.masksToBounds = true
        
    }
    
} 
 
fileprivate class Rectangle : Square {
    
    override func configure(a) {
        
        let frame = CGRect(x: 0, y: 0, width: height + height/2, height: height)
        view.frame = frame
        view.backgroundColor = UIColor.blue
        
    }
    
} 
Copy the code

I use Fileprivate to emphasize the purpose behind the factory method pattern: encapsulation. You can see how easy it is to modify and extend the Shape class hierarchy without changing the factory methods below. This is the code for the factory methods that make object creation so simple and abstract.

enum Shapes {
    
    case square
    case circle
    case rectangle
    
}

class ShapeFactory {
    
    let parentView: UIView
    
    init(parentView: UIView) {
        
        self.parentView = parentView
        
    }
    
    func create(as shape: Shapes) -> HelperViewFactoryProtocol {
        
        switch shape {
            
        case .square:
            
            let square = Square(parentView: parentView)
            return square
            
        case .circle:
            
            let circle = Circle(parentView: parentView)
            return circle
            
        case .rectangle:
            
            let rectangle = Rectangle(parentView: parentView)
            return rectangle
            
        }
        
    } 
    
} 

// Public factory method to display shapes
func createShape(_ shape: Shapes, on view: UIView) {
    
    let shapeFactory = ShapeFactory(parentView: view)
    shapeFactory.create(as: shape).display()
    
}

// Select the public factory method to display shapes
Strictly speaking, the factory method should return one of the related classes.
func getShape(_ shape: Shapes, on view: UIView) -> HelperViewFactoryProtocol {
    
    let shapeFactory = ShapeFactory(parentView: view)
    return shapeFactory.create(as: shape)
    
}
Copy the code

Note: I’ve written down a class factory and two factory methods to get you thinking. Strictly speaking, a factory method should return objects of corresponding classes that share a common base class or protocol. My goal is to draw a shape on the view, so I prefer the createShape(_: View 🙂 method. Provide this alternative (the method) for experimentation and exploration of new possibilities as needed.

Finally, I show how the two factory methods can be used to draw shapes. UI developers don’t have to know how shape classes are encoded. In particular, he/she does not have to worry about how the shape class is initialized. The code in the viewController.swift file is easy to read.

import UIKit
 
class ViewController: UIViewController {
    
    override func viewDidLoad(a) {
        super.viewDidLoad()
        // Add Settings after the view is loaded, usually from NIB
        
    }
 
    override func didReceiveMemoryWarning(a) {
        super.didReceiveMemoryWarning()
        // Discard resources that can be recreated
    }
 
    @IBAction func drawCircle(_ sender: Any) {
        
	// Only for drawing shapes
        createShape(.circle, on: view)
        
    }
    
    @IBAction func drawSquare(_ sender: Any) {

	// Draw a graph
        createShape(.square, on: view)
        
    }
    
    @IBAction func drawRectangle(_ sender: Any) {

	// Get an object from the factory and use it to draw a shape
        let rectangle = getShape(.rectangle, on: view)
        rectangle.display()
        
    }
    
} 
Copy the code

Singleton design pattern

Most iOS developers are familiar with the singleton pattern. Recall UNUserNotificationCenter. Current (), UIApplication. Shared or FileManager. The default if you want to send a notification, or open a URL in Safari, Or to work with iOS files, you must use each singleton separately. Singletons are good for securing shared resources, providing access to some systems with only one object instance, and enabling objects to perform some application-level types of collaboration. As we’ll see, singletons can also be used to encapsulate other singletons built into iOS, adding some value manipulation functionality.

As a singleton, we need to ensure that this class:

  • Declare and initialize a static class constant property, and then name that property assharedTo indicate that an instance of this class is a singleton (the default is common);
  • Declare one for some of the resources we want to control and protectprivateProperties. And only throughsharedSharing;
  • Declare a private initialization method that only our singleton class can initializeinitInitializes the shared resources we want to control.

Create a private initialization method for a class by defining a shared static constant. We want to ensure that there is only one instance of this class, that the class can only be initialized once, and that shared instances are available anywhere in the application. That’s how we created a singleton!

The code for this singleton project, available for download on GitHub, shows how a developer can safely and efficiently store user preferences. This is a simple Demo that can record the user’s password text. The preferences can be set to visible or hidden. In hindsight, this wasn’t a good idea, I just needed an example to show you how my code works. This code is purely for educational purposes. I suggest you never give away your password. You can see that users can set their password preferences — and the password preferences are stored in UserDefaults:

When the user closes the application and opens it again, he/she notices that his/her password preferences are recorded:

Let me show you PreferencesSingleton. Swift code snippet of files, in the inline comments, you will see I want to express meaning.

class UserPreferences {

	// Create a static, constant instance using the class initializer.
    static let shared = UserPreferences(a)// This is a private, shared resource that we protect.
    private let userPreferences: UserDefaults
    
	// A private initialization method can only be called by the class itself.
    private init() {
        
	// Get the iOS share singleton. We packaged it here.
        userPreferences = UserDefaults.standard
        
    }
 
} // end class UserPreferences
Copy the code

Static properties need to be initialized at application startup, but global variables are lazily loaded by default. You might be worried about this code executing incorrectly, but as far as I know about Swift, it’s perfectly fine.

You might ask, “Why create a singleton by wrapping another UserDefaults singleton?” First, my main goal is to show you the best practices for creating and using singletons in Swift. A user preference is a resource type that should have a single entry. So in this case, it’s pretty obvious we should use UserDefaults. Second, think about how many times you’ve seen UserDefaults abused in applications.

In some project application code, I’ve seen the use of UserDefaults(or NSUserDefaults before it) be disorganized and unintentional. Each key corresponding to a user preference attribute is a string reference. Just now, I found a bug in the code. I spelled “switch” as “swithc,” and because I copied and pasted the code, I had created quite a few instances of “swithc” before I discovered the problem. What if other developers started or continued to use “switch” as a key to store corresponding values in the app? The current state of the application cannot be saved correctly. We often use Strings in UserDefaults to store the state of our application as key-value mappings. That’s a good way to write it. This makes the meaning of the value clear, easy to understand, and easy to remember. But that’s not to say there’s no risk in describing it through strings.

In my discussion of “swithC” versus “switch”. Most people are probably already aware of code called “string-typed”, where strings as a unique identifier makes a slight difference and ends up being disastrously misspelled. The Swift compiler does not help us avoid “string-typed” errors.

The way to resolve the “string-typed” error is to set the Swift enum to string. Doing so not only allows us to standardize the use of strings, but also allows us to manage them by category. Let us once again return to PreferencesSingleton. Swift:

class UserPreferences {
    
    enum Preferences {
        
        enum UserCredentials: String {
            case passwordVisibile
            case password
            case username
        }
        
        enum AppState: String {
            case appFirstRun
            case dateLastRun
            case currentVersion
        }
 
    } // end enum Preferences
Copy the code

Let’s start by defining the singleton pattern to show you why I use a singleton to encapsulate UserDefaults in my application. You can add new functionality by adding values, but you can make your code more robust by simply wrapping UserDefaults. Error checking should be at the forefront of your mind when retrieving and setting user preferences. In this case, I want to implement a user preference feature that sets the visibility of the password. See the code below. Contents are PreferencesSingleton. Swift file:

import Foundation
 
class UserPreferences {
    
    enum Preferences {
        
        enum UserCredentials: String {
            case passwordVisibile
            case password
            case username
        }
        
        enum AppState: String {
            case appFirstRun
            case dateLastRun
            case currentVersion
        }
 
    } // end enum Preferences
    	
    // Create a static, constant instance and initialize it
    static let shared = UserPreferences(a)// This is a private, protected shared resource
    private let userPreferences: UserDefaults
    
    // a private initialization method that only the class itself can call
    private init() {
        // Get the iOS share singleton. We pack it here
        userPreferences = UserDefaults.standard
 
    }
    
    func setBooleanForKey(_ boolean:Bool, key:String) {
        
        ifkey ! ="" {
            userPreferences.set(boolean, forKey: key)
        }
        
    }
    
    func getBooleanForKey(_ key:String) -> Bool {
        
        if let isBooleanValue = userPreferences.value(forKey: key) as! Bool? {
            print("Key \(key) is \(isBooleanValue)")
            return true
        }
        else {
            print("Key \(key) is false")
            return false}}func isPasswordVisible(a) -> Bool {
        
        let isVisible = userPreferences.bool(forKey: Preferences.UserCredentials.passwordVisibile.rawValue)
        
        if isVisible {
            return true
        }
        else {
            return false}}Copy the code

Go to the viewController.swift file and you’ll see how easy it is to access and use a well-formed singleton:

import UIKit
 
class ViewController: UIViewController {
    
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var passwordVisibleSwitch: UISwitch!
    
    override func viewDidLoad(a) {
        super.viewDidLoad()
	// Perform additional Settings after the view is loaded (usually via NIB).
        
        if UserPreferences.shared.isPasswordVisible() {
            passwordVisibleSwitch.isOn = true
            passwordTextField.isSecureTextEntry = false
        }
        else {
            passwordVisibleSwitch.isOn = false
            passwordTextField.isSecureTextEntry = true}}override func didReceiveMemoryWarning(a) {
        super.didReceiveMemoryWarning()
	// You can destroy resources that can be recreated
    }
    
    @IBAction func passwordVisibleSwitched(_ sender: Any) {
        
        let pwdSwitch:UISwitch = sender as! UISwitch
        
        if pwdSwitch.isOn {
            passwordTextField.isSecureTextEntry = false
            UserPreferences.shared.setPasswordVisibity(true)}else {
            passwordTextField.isSecureTextEntry = true
            UserPreferences.shared.setPasswordVisibity(false)}}Copy the code

conclusion

Some critics claim that the use of design patterns in some programming languages is unproven, and that repeating the same design patterns in code is a bad thing. I don’t agree with that. It would be foolish to expect a programming language to handle everything with its own characteristics. This is likely to result in a bloated language that, like C++, is becoming larger and more complex, making it harder to learn, use, and maintain. Recognizing and solving recurring problems is a positive trait and one worth reinforcing. There are some things that people have tried and failed many times. By learning from previous experience, abstracting and standardizing some of the same problems, design patterns have become a success story that allows good solutions to spread.

A combination of a simple, compact language like Swift and a set of best practices like design patterns is an ideal, happy approach. Uniform code is generally more readable and maintainable. Keep in mind, though, that design patterns are constantly evolving, with millions of developers discussing and sharing the beautiful things connected by the World Wide Web, and this developer discussion continues to lead to a self-regulation of collective intelligence.

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.