> For the complete documentation index, see [llms.txt](https://wilden-chen.gitbook.io/swift-design-patterns/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wilden-chen.gitbook.io/swift-design-patterns/singleton/use-singleton.md).

# 如何使用單例模式

可以看下這個圖：

![](/files/-Lejh1qOk7qWCeEHkCFx)

這是一個日誌類，有一個屬性 (是一個單例對象) 和兩個方法 (`sharedInstance()` 和 `init()`)。

第一次調用 `sharedInstance()` 的時候， `instance` 屬性還沒有初始化。所以我們要創建一個新實例並且返回。

下一次你再調用 `sharedInstance()` 的時候，`instance` 已經初始化完成，直接返回即可。這個邏輯確保了這個類別只存在一個實體對象。

依據這個邏輯，我們也可以寫一個單例模式，通過這個類別來管理圖片數據資料。

我們可以新建一個檔案 `ImageCache.swift` ，並添加下面這些程式碼。

```swift
class ImageCache {
    static let shared = ImageCache()
    static func sharedInstance() -> ImageCache {
        return shared
    }
    private init() {}
}
```

在現代 Swift 中，宣告單例設計模式極為簡單且安全。我們只需要宣告一個公開的靜態常數 `static let shared = ImageCache()`，並且將初始化建構子 `init()` 設為私有 `private init()`，以確保外部無法透過其他方式實體化該類別。

在 Swift 中，靜態屬性 `static let` 在首次被存取時會自動進行延遲載入 (Lazy Loading)，且底層由 GCD (Grand Central Dispatch) 保證其**執行緒安全 (Thread-Safe)**，讀者不需要再像以前一樣手動檢查 `nil` 並進行初始化。

為了與原本專案的寫法相容，我們另外提供了一個 `sharedInstance()` 靜態方法來回傳這個單例：

這些就是單例模式的核心所在：確保類別在整個應用程式生命週期中僅存在唯一實體。

最後，藉由將 `init()` 方法設為 `private`，限制外部無法使用 `ImageCache()` 實體化，保證了單例模式的唯一性。

我們現在可以將這個單例作為圖片資料快取儲存，接下來我們繼續完成他全部的功能。

```swift
import Foundation
import UIKit

class ImageCache {
    static let shared = ImageCache()
    static func sharedInstance() -> ImageCache {
        return shared
    }
    private init() {

    }

    private var mImages:[String:UIImage] = [String:UIImage]()

    func saveImage(aImage:UIImage,aFilename: String) {
        mImages.updateValue(aImage, forKey: aFilename)
        saveLocalImage(aImage:aImage,aFilename: aFilename)
    }

    func getImage(aFilename: String) -> UIImage? {
        var _result:UIImage?
        if let _dicImage:UIImage = mImages[aFilename] {
            _result = _dicImage
        }else if let _docImage:UIImage = getLocalImage(aFilename:aFilename){
            _result = _docImage
        }
        return _result
    }

    private func saveLocalImage(aImage: UIImage, aFilename: String) {
        guard let _documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        let _fileURL = _documentsURL.appendingPathComponent(aFilename)
        let _data = aImage.pngData()
        do {
            try _data?.write(to: _fileURL, options: .atomic)
        } catch {
            print(error)
        }
    }

    private func getLocalImage(aFilename: String) -> UIImage? {
        guard let _documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
        let _fileURL = _documentsURL.appendingPathComponent(aFilename)
        do {
            let _data = try Data(contentsOf: _fileURL)
            return UIImage(data: _data)
        } catch {
            return nil
        }
    }
}
```

在這裡我們定義了一個私有屬性，用來存儲照片資料。這是一個字典變數，所以值為字串，在單例模式下，ImageCache 類別只會存在一份實體，同樣的在這類別裡面的`mImages:[String:UIImage]` 字典裡面，也只會存在一個實體。

我們會把圖片儲存到手機本機端，這樣可以避免一次又一次下載相同的照片。

程式內容很簡單直接，下載的照片會儲存在 Documents 目錄下，如果沒有檢查到暫存檔案， getImage() 方法則會返回 nil 。

我們開始運用看看： 打開`PhotoView.swift`這個類別，

```swift
    func downloadImage(aImageURL:String?,aCacheFileName:String?) {
        mImageView.image = nil
        if let _url = aImageURL, let _cacheName = aCacheFileName {
            mLoadingView.startAnimating()
            AF.request("\(_url)\(_cacheName)").response { aResponse in
                if aResponse.error == nil {
                    if let _data = aResponse.data, let _image = UIImage(data: _data) {
                        self.mImageView.image = _image
                        ImageCache.sharedInstance().saveImage(aImage: _image, aFilename: _cacheName)
                    } else {
                        print("no image")
                    }
                } else {
                    print("error:\(aResponse.error?.localizedDescription ?? "")")
                }
            }
        }
    }
```

下載完圖片以後，直接儲存到圖片快取裡面。

我們使用外觀模式隱藏了下載圖片的複雜程度。通知的發送者並不在乎圖片是如何從網上下載到本地的。

我們先開始做最開始的版型排版：

先打開 `MainViewController.swift` ，開始編輯裡面的 `viewDidLoad` ，並且加ㄧ個變數用來儲存會顯示的全部的 `PhotoView` ，在宣告一個變數用來建立 FlickrModel 的實體。

```swift
    private var mPhotoViews :[PhotoView] = [PhotoView]()
    private let mFlickrModel:FlickrModel = FlickrModel()
```

然後，我們在寫上測試到目前功能的程式碼：

```swift
    private func createDemoLayout() {
        mFlickrModel.loadFlickrData(aPageIndex: 1)

        for _index:UInt in 0..<10 {
            let _photoView  :PhotoView = PhotoView()
            let _boardWidth :CGFloat = 5.0
            let _dWidth     :CGFloat = (self.view.bounds.width - _boardWidth * 5.0)  / 4.0
            let _dHeight    :CGFloat = (self.view.bounds.height - _boardWidth * 6.0) / 5.0
            let _dX         :CGFloat = _boardWidth + CGFloat(_index % 4) * (_dWidth + _boardWidth)
            let _dY         :CGFloat = 50.0 + CGFloat(_index / 4) * (_dHeight + _boardWidth)
            _photoView.frame = CGRect(x: _dX, y: _dY, width: _dWidth, height: _dHeight)

            self.view.addSubview(_photoView)
            mPhotoViews.append(_photoView)
        }

        let _button:UIButton = UIButton()
        _button.frame = CGRect(x: 10.0, y: self.view.bounds.height - 30.0, width: self.view.bounds.width - 20.0, height: 25.0)
        _button.backgroundColor = UIColor.red
        self.view.addSubview(_button)

        _button.addTarget(self, action: #selector(self.onTestLoadImageHandler), for: .touchUpInside)
    }

    func onTestLoadImageHandler(){
        for _photoView:PhotoView in mPhotoViews {
            let _index:Int = mPhotoViews.firstIndex(of: _photoView)!
            let _vo:PhotoVO = mFlickrModel.getPhotos(aPageIndex: 1)[_index]
            _photoView.downloadImage(aImageURL: _vo.url, aCacheFileName: ".jpg")
        }
    }
```

然後，我們在 viewDidload 執行我們剛剛所寫的程式碼：

```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white

        createDemoLayout()
    }
```

而原本測試的 `PhotoDelegate`就先刪除吧，避免程式碼的雜亂。

在模擬器中執行，會發現一開始會出現 10 個整齊排列的藍色框框，這些都是我們寫的 PhotoView ，如果網路正常的情況下，會在 Xcode 裡的 Console 視窗出現 `LoadFlickrDataComplete!count:99` ，表示 Flickr 的資料載入完成，然後我們再按下最下面的紅色按鈕，載入圖片。

![](/files/-Lejh1qTY_W5PkPg2lbk)

完成到這一步的Demo：

* 查看原始碼
* [下載ZIP](https://github.com/wildenchen/swift-bethel-of-the-road/tree/eed3c8ff6c1141681b3d3b2c69fac406cd11c24c/DesignPatterns/workspaces/FlickrPhotos-Singleton.zip)
