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.