In depth understanding of the replacement of the singleton pattern in Swift and the implementation of the Swift 3.0 singleton pattern

  • 2020-05-30 21:10:19
  • OfStack

preface

In addition to MVC and MVVM, the singleton pattern is another common design pattern in iOS development. Whether it's UIKit or some popular 3 - sided library, we can see singletons. We, as developers, subconsciously take the code in these libraries as a best practice and bring it into our daily lives, even though many of us know that singletons have some obvious flaws.

To address the shortcomings of singletons, this article will introduce some ways to replace or modify the singleton pattern to improve the quality of the code.

Advantages of singletons

In addition to the imitation best practices mentioned above, the popularity of singletons certainly has its own reasons and reasons. For example, a singleton ensures that only one instance exists, which helps us coordinate the behavior of the system as a whole. For example, in a server program, the configuration information of the server is stored in a file, which is read by a singleton object, and then obtained by other objects in the service process through this singleton object. This approach simplifies configuration management in complex environments. On the other hand, the global single object also reduces unnecessary object creation and destruction actions and improves efficiency.

Here is a typical singleton pattern code:


class UserManager {
 static let shared = UserManager()
 
 private init() {
 //  The singleton pattern prevents multiple instances from occurring 
 }
 
 ....
}

extension UserManager {
 func logOut( ) {
 ...
 }
 
 func logIn( ) {
 ...
 }
}

class ProfileViewController: UIViewController {
 private lazy var nameLabel = UILabel()

 override func viewDidLoad() {
 super.viewDidLoad()
 nameLabel.text = UserManager.shared.currentUser?.name
 }

 private func handleLogOutButtonTap() {
 UserManager.shared.logOut()
 }
}

Singleton defects

While the advantages of singleton 1 are mentioned above, this does not obscure the obvious disadvantages of singleton pattern 1:

Globally Shared modifiable states: one of the side effects of the singleton pattern is that those Shared state quantities may change during the lifetime of app, and these changes may cause some positional errors. Even worse, due to the nature of the scope and lifecycle, these issues are difficult to locate. Dependencies are not clear: because singletons are easily accessible globally, this will make our code so called spaghetti code. The relationship between the singleton and the user is not clear, and later maintenance is very troublesome. Difficult to trace testing: because the singleton pattern has the same lifecycle as app and any changes are made during the lifecycle, there is no way to ensure that a clean instance is used for testing. Because there is no abstraction layer in the simple interest pattern, it is difficult to extend the singleton class. Singleton classes are overburdened and violate the "single one responsibility principle" to a certain extent.

Dependency injection

Instead of using singletons in between, we can do dependency injection during initialization.


class ProfileViewController: UIViewController {
 private let user: User
 private let logOutService: LogOutService
 private lazy var nameLabel = UILabel()

 init(user: User, logOutService: LogOutService) {
 self.user = user
 self.logOutService = logOutService
 super.init(nibName: nil, bundle: nil)
 }

 override func viewDidLoad() {
 super.viewDidLoad()
 nameLabel.text = user.name
 }

 private func handleLogOutButtonTap() {
 logOutService.logOut()
 }
}

class LogOutService {
 private let user: User
 private let networkService: NetworkService
 private let navigationService: NavigationService

 init(user: User,
 networkService: NetworkService,
 navigationService: NavigationService) {
 self.user = user
 self.networkService = networkService
 self.navigationService = navigationService
 }

 func logOut() {
 networkService.request(.logout(user)) { [weak self] in
 self?.navigationService.showLoginScreen()
 }
 }
}

The dependencies in the code above are significantly clearer than before, and are easier to maintain and write test instances later. In addition, by using the LogOutService object we pulled out certain services, avoiding the bloat that is common in singletons.

Protocol transformation

It is obviously a time-consuming and unreasonable thing to completely rewrite the application of a singleton abuse as a dependency injection and servitization as above. Therefore, the following method will be used to gradually transform the singleton through a protocol. The main method here is to rewrite the service provided by LogOutService above as a protocol:


protocol LogOutService {
 func logOut()
}

protocol NetworkService {
 func request(_ endpoint: Endpoint, completionHandler: @escaping () -> Void)
}

protocol NavigationService {
 func showLoginScreen()
 func showProfile(for user: User)
 ...
}

Once the protocol service is defined, we let the existing singletons follow the protocol. At this point, we can inject the singleton as a service without changing the original code implementation.


extension UserManager: LoginService, LogOutService {}

extension AppDelegate: NavigationService {
 func showLoginScreen() {
 navigationController.viewControllers = [
 LoginViewController(
 loginService: UserManager.shared,
 navigationService: self
 )
 ]
 }

 func showProfile(for user: User) {
 let viewController = ProfileViewController(
 user: user,
 logOutService: UserManager.shared
 )

 navigationController.pushViewController(viewController, animated: true)
 }
}

Several ways to implement the Swift3.0 singleton pattern -Dispatch_Once

It is quite common to use singletons in development. The normal way of thinking is to use GCD's dispatch_once and API. However, in swift3.0, apple has abandoned this method, but don't worry, we can implement it in other ways.

Based on the characteristics of swift language, the following writing methods are summarized:

General creation method Static creation method struct create method By adding extensions to DIspatchQueue

Note: I hope you will call this method in addition to using it

1. General creation method


//MARK - :  The singleton : methods 1
 static let shareSingleOne = Single()

2. Static creation method


let single = Single()
class Single: NSObject {
 //-MARK:  The singleton : methods 2
 class var sharedInstance2 : Single {
  return single
 }
}

3. struct create method


 //-MARK:  The singleton : methods 3
 static var shareInstance3:Single{
 struct MyStatic{
  static var instance :Single = Single()
 }
 return MyStatic.instance;
 }

4. Implement it by adding extensions to DispatchQueue


public extension DispatchQueue { 
 
 private static var _onceTracker = [String]() 
 
 /** 
 Executes a block of code, associated with a unique token, only once. The code is thread safe and will 
 only execute the code once even in the presence of multithreaded calls. 
 
 - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID 
 - parameter block: Block to execute once 
 */ 
 public class func once(token: String, block:()->Void) { 
 objc_sync_enter(self) 
 defer { objc_sync_exit(self) } 
 
 if _onceTracker.contains(token) { 
  return 
 } 
 
 _onceTracker.append(token) 
 block() 
 } 
} 

The string token is used as ID of once, and a lock is added when once is executed to avoid the problem of inaccurate judgment of token under multi-threading.

You can send token when you use it


DispatchQueue.once(token: "com.vectorform.test") { 
 print( "Do This Once!" ) 
} 

Or you can use UUID:


private let _onceToken = NSUUID().uuidString 
 
DispatchQueue.once(token: _onceToken) { 
 print( "Do This Once!" ) 
} 

conclusion

The singleton pattern is not without merit; it is useful in scenarios such as logging services, peripheral management, and so on. But most of the time, the singleton mode may increase the complexity of the system due to unclear dependencies and global Shared mutable state, resulting in a series of unknown problems. If you're using a lot of singletons in your current code, I hope this article has freed you from the need to build a more robust system.


Related articles: