• Design Patterns on iOS Using Swift — Part 2/2
  • Lorenzo Boaro
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: iWeslie
  • Proofread by: Swants

In this two-part tutorial, you’ll learn about common design patterns for building iOS applications and how to apply them in your own applications.

What’s New: This tutorial has been updated by the translator for iOS 12, Xcode 10, and Swift 4.2. The original post was posted by tutorial team member Eli Ganem.

Welcome back to part 2 of our introduction to iOS Design Patterns tutorial! In part 1, you learned about some of the basic patterns in Cocoa, such as MVC, singleton, and decorator patterns.

In the final installment, you’ll learn about other basic design patterns that emerge in iOS and OS X development: adapters, observers, and memos. Let’s get started!

An introduction to

You can get started by downloading the project at the end of Part 1.

This is the music library application you left at the end of Part 1:

The original plan for the app included a scrollView at the top of the screen for switching between albums. But instead of writing a single purpose scrollView, why not make it reusable for any other view?

To make this scrollView reusable, all decisions about its content should be left to two other objects: its data source and its proxy. To use a scrollView, you should give it a declaration of the data source and proxy implementation methods, similar to how UITableView’s proxy methods work. As we discuss the next design pattern, you’ll be implementing it as well.

Adapter mode

Adapters allow classes with incompatible interfaces to wrap themselves in an object and expose a standard interface to interact with that object.

If you’re familiar with the adapter pattern, you’ll notice that Apple implements it in a slightly different way: protocols. You may be familiar with protocols such as UITableViewDelegate, UIScrollViewDelegate, NSCoding, and NSCopying. For example, using the NSCopying protocol, any class can provide a standard copy method.

How do I use the adapter pattern

The scrollView mentioned earlier looks like this:

To implement it now, right click on the View group in the project navigation bar, select New File > iOS > Cocoa Touch Class, then click Next and set the Class name to HorizontalScrollerView and inherit from UIView.

Open the HorizontalScrollerView. Swift and insert the following code above the HorizontalScroller class declares:

protocol HorizontalScrollerViewDataSource: class {
  // Asks the data source how many views it wants to display in the scrollView
  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int
  // Request the data source to return the view that should appear in the index
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView
}
Copy the code

It defines a protocol, called HorizontalScrollerViewDataSource it perform two operations: request and display the number of the view inside the scrollView and should be specific index according to the view.

Defined in this agreement below to add another protocol called HorizontalScrollerViewDelegate.

protocol HorizontalScrollerViewDelegate: class {
  // Notify the agent that the index view has been selected
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int)
}
Copy the code

This will cause the scrollView to notify some other object that one of its internal views has been selected.

** Note: ** Dividing the area of concern into different protocols makes the code look clearer. This way you can decide to follow a particular protocol and avoid using @objc to declare optional protocol methods.

In HorizontalScrollerView. Swift, add the following code to the HorizontalScrollerView class definition:

weak var dataSource: HorizontalScrollerViewDataSource?
weak var delegate: HorizontalScrollerViewDelegate?
Copy the code

Both the proxy and the data source are optional, so you don’t have to assign values to them, but any objects you set up here must follow the appropriate protocol.

Add the following code to the class:

/ / 1
private enum ViewConstants {
  static let Padding: CGFloat = 10
  static let Dimensions: CGFloat = 100
  static let Offset: CGFloat = 100
}

/ / 2
private let scroller = UIScrollView(a)/ / 3
private var contentViews = [UIView] ()Copy the code

A detailed explanation of each note is as follows:

  1. Define a privateenumMake the code layout easier to modify at design time. The size of the view inside the scrollView is 100 x 100, and the padding is 10
  2. Create a scrollView that contains multiple views
  3. Creates an array of all album covers

Next you need to implement the initializer. Add the following methods:

override init(frame: CGRect) {
  super.init(frame: frame)
  initializeScrollView()
}

required init? (coder aDecoder:NSCoder) {
  super.init(coder: aDecoder)
  initializeScrollView()
}

func initializeScrollView(a) {
  / / 1
  addSubview(scroller)

  / / 2
  scroller.translatesAutoresizingMaskIntoConstraints = false

  / / 3
  NSLayoutConstraint.activate([
    scroller.leadingAnchor.constraint(equalTo: self.leadingAnchor),
    scroller.trailingAnchor.constraint(equalTo: self.trailingAnchor),
    scroller.topAnchor.constraint(equalTo: self.topAnchor),
    scroller.bottomAnchor.constraint(equalTo: self.bottomAnchor)
  ])

  / / 4
  let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(scrollerTapped(gesture:)))
  scroller.addGestureRecognizer(tapRecognizer)
}
Copy the code

This work is done in initializeScrollView(). Here’s the breakdown:

  1. Adding a subviewUIScrollViewThe instance
  2. Turn off autoresizingMask so you can use custom constraints
  3. Applying the constraint to the scrollView, you want the scrollView to be completely filledHorizontalScrollerView
  4. Create tap gesture. It detects the touch event on the scrollView and checks to see if the album cover has been clicked. If so, it will notifyHorizontalScrollerViewThe agent. There will be a compile error here, because the scrollerTapped(gesture:) method has not been implemented, and you will implement it next.

Now add the following method:

func scrollToView(at index: Int, animated: Bool = true) {
  let centralView = contentViews[index]
  let targetCenter = centralView.center
  let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)
  scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: animated)
}
Copy the code

This method retrieves and centers the view for a particular index. It will be called by the following method (which you also need to add to the class) :

@objc func scrollerTapped(gesture: UITapGestureRecognizer) {
  let location = gesture.location(in: scroller)
  guard
    let index = contentViews.index(where: {$0.frame.contains(location)})
    else { return} delegate? .horizontalScrollerView(self, didSelectViewAt: index)
  scrollToView(at: index)
}
Copy the code

This method looks for the click location in the scrollView and, if it exists, looks for the index of the first contentView containing that location.

If the contentView is clicked, notify the agent and scroll the view to the center.

Next add the following to access the album cover from the scroller:

func view(at index :Int) -> UIView {
  return contentViews[index]
}
Copy the code

View (at:) returns only the view at the specified index, which you will use later to highlight the album cover you clicked on.

Now add the following code to refresh the scrollView:

func reload(a) {
  Check if there is a data source. If not, return.
  guard let dataSource = dataSource else {
    return
  }

  // 2. Delete all old contentViews
  contentViews.forEach { $0.removeFromSuperview() }

  // 3. XValue is the starting x coordinate of each view in scrollView
  var xValue = ViewConstants.Offset
  // 4. Get and add a new View
  contentViews = (0..<dataSource.numberOfViews(in: self)).map {
    index in
    // 5. Add the View in the correct position
    xValue += ViewConstants.Padding
    let view = dataSource.horizontalScrollerView(self, viewAt: index)
    view.frame = CGRect(x: xValue, y: ViewConstants.Padding, width: ViewConstants.Dimensions, height: ViewConstants.Dimensions)
    scroller.addSubview(view)
    xValue += ViewConstants.Dimensions + ViewConstants.Padding
    return view
  }
  / / 6
  scroller.contentSize = CGSize(width: xValue + ViewConstants.Offset, height: frame.size.height)
}
Copy the code

The Reload method in UITableView is modeled after the reloadData, which reloads all the data used to construct the scrollView.

The corresponding explanation for each comment is as follows:

  1. Check if the data source exists before performing any reload.
  2. Since you want to clear the album cover, you also need to remove all existing Views.
  3. All views are positioned from a given offset. It is currently 100, but you can change the constant at the top of the fileViewConstants.OffsetTo make adjustments easily.
  4. Request the number of views from the data source and use it to create a new contentView array.
  5. HorizontalScrollerViewAsk a view for its data sources one at a time and lay them out horizontally using the previously defined population.
  6. After all the views are laid out, set the offset of the scrollView to allow the user to scroll through all the album covers.

Call the reload method when your data changes.

The last thing the HorizontalScrollerView needs to implement is to make sure that the album you’re looking at is always in the center of the scrollView. To do this, you need to perform some calculations as the user drags the scrollView with his finger.

Add the following methods below:

private func centerCurrentView(a) {
  let centerRect = CGRect(
    origin: CGPoint(x: scroller.bounds.midX - ViewConstants.Padding, y: 0),
    size: CGSize(width: ViewConstants.Padding, height: bounds.height)
  )

  guard let selectedIndex = contentViews.index(where: {$0.frame.intersects(centerRect) })
    else { return }
  let centralView = contentViews[selectedIndex]
  let targetCenter = centralView.center
  let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)

  scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: true) delegate? .horizontalScrollerView(self, didSelectViewAt: selectedIndex)
}
Copy the code

The code above takes into account the current offset of the scrollView and the size and padding of the view to calculate the current view’s distance from the center. The last line is important: once the view is centered, notify the agent that the selected view has changed.

To detect whether the user has dragged within the scrollView, you need to implement some UIScrollViewDelegate methods and add the following class extension to the bottom of the file. Be sure to add it under curly braces in the main class declaration!

extension HorizontalScrollerView: UIScrollViewDelegate {
  func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if! decelerate { centerCurrentView() } }func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    centerCurrentView()
  }
}
Copy the code

ScrollViewDidEndDragging (_:willDecelerate:) notifies the agent when the user has finished dragging, and willDecelerate is true if the scrollView has not completely stopped. When at the end of the scroll, the system call scrollViewDidEndDecelerating (_). In both cases, you should call the new method to center the current view, because the current view may have changed after the user drags the view.

Finally, don’t forget to set up the proxy by adding the following code to the beginning of initializeScrollView() :

scroller.delegate = self
Copy the code

Your HorizontalScrollerView is ready! If you look at the code you just wrote, you’ll see that the Album or AlbumView class doesn’t appear anywhere. This is great, because it means that the new scrollView is truly decoupled and reusable.

Compile the project to ensure that it will compile properly.

Now that the HorizontalScrollerView is complete, it’s time to use it in your application. First open main.storyboard. Click the gray rectangle view at the top, and then click Identity Inspector. Change the class name to HorizontalScrollerView as shown below:

Next open the Assistant Editor and drag a line from the gray rectangle view to ViewController.swift to create an IBOutlet and name it horizontalScrollerView, as shown below:

Next, open the ViewController. Swift, it is time to begin to implement some HorizontalScrollerViewDelegate method!

Add the following extension to the bottom of the file:

extension ViewController: HorizontalScrollerViewDelegate {
  func horizontalScrollerView(** horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int) {
    / / 1
    let previousAlbumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView
    previousAlbumView.highlightAlbum(false)
    / / 2
    currentAlbumIndex = index
    / / 3
    let albumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView
    albumView.highlightAlbum(true)
    / / 4
    showDataForAlbum(at: index)
  }
}
Copy the code

This is what happens when this proxy method is called:

  1. First you pick up the previously selected album, then deselect the album cover
  2. Stores the index of the current album cover that you just clicked on
  3. Gets the currently selected album cover and displays the highlighted status
  4. Display new album data in tableView

Next, it’s time to realize HorizontalScrollerViewDataSource. Add the following code to the end of the current file:

extension ViewController: HorizontalScrollerViewDataSource {
  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int {
    return allAlbums.count
  }

  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView {
    let album = allAlbums[index]
    let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), coverUrl: album.coverUrl)
    if currentAlbumIndex == index {
      albumView.highlightAlbum(true)}else {
      albumView.highlightAlbum(false)}return albumView
  }
}
Copy the code

As you can see, numberOfViews(in:) is a protocol method that returns the numberOfViews in a scrollView. Since scrollView will display the covers of all album data, count is the number of album records. In the horizontalScrollerView(_:viewAt:) you create a new AlbumView, highlight it if it is the selected album, and pass it to the horizontalScrollerView.

Almost done! It only takes three short methods to display a nice scrollView. You now need to set up the data source and proxy. Add the following code before showDataForAlbum(at:) in viewDidLoad:

horizontalScrollerView.dataSource = self
horizontalScrollerView.delegate = self
horizontalScrollerView.reload()
Copy the code

Compile and run your project to see a nice horizontal scrolling view:

Uh, wait a minute! The horizontal scroll view is in place, but where’s the album cover?

Oh, and you haven’t implemented the code to download the cover. To do this, you need to add methods to download images, and all of your requests to the server go through a layer of LibraryAPI that all new methods go through. But first, consider the following:

  1. AlbumViewShould not be directly withLibraryAPIYou don’t want to mix the logic in the view with the network request.
  2. For the same reason,LibraryAPINor should I knowAlbumViewThe existence of.
  3. When the cover is downloaded,LibraryAPINeed to informAlbumViewTo display the album.

Does it sound like it’s too hard? Don’t despair, you’ll learn how to do this using observer mode!

Observer model

In the observer pattern, one object notifies other objects of any state change, but the notified objects do not need to be related to each other, and we encourage this decoupled design approach. This pattern is most commonly used to notify other related objects when an object’s properties change.

A common implementation requires an observer to listen to the state of another object. When a state change occurs, all observed objects are notified of the change.

If you stick to the MVC concept (and you do), you need to allow Model objects to communicate with View objects, but without direct references between them, which is where the observer pattern comes in.

Cocoa implements the observer pattern in two ways: notification and key value Listening (KVO).

notice

Not to be confused with push or local notifications, observer mode notifications are based on a subscription and publication model that allows objects (publishers) to send messages to other objects (subscribers or listeners) without the publisher ever needing to know anything about the subscribers.

Apple uses notifications a lot. For example, UIKeyboardWillShow and UIKeyboardWillHide notifications are sent when the keyboard is shown or hidden, respectively. When your application into the background, the system will send a UIApplicationDidEnterBackground notice.

How to Use notifications

Right click RWBlueLibrary and select New Group, then name Extension. Right-click the group again, and then select New File > iOS File > Swift, and sets the File name to NotificationExtension. Swift.

Copy the following code into the file:

extension Notification.Name {
  static let BLDownloadImage = Notification.Name("BLDownloadImageNotification")}Copy the code

You are using the Notification.Name extension for custom notifications. From now on, new notifications can be accessed as system notifications with.bldownloadimage.

Open albumView.swift and insert the following code at the end of the init(frame:coverUrl:) method:

NotificationCenter.default.post(name: .BLDownloadImage, object: self, userInfo: ["imageView": coverImageView, "coverUrl" : coverUrl])
Copy the code

This line of code sends a notification through the singleton of NotificationCenter containing the UIImageView to populate and the URL of the album image to download, all the information needed to perform the cover download task.

Add the following code to the init method in libraryapi.swift as an implementation of the currently empty initialization method:

NotificationCenter.default.addObserver(self, selector: #selector(downloadImage(with:)), name: .BLDownloadImage, object: nil)
Copy the code

This is the other side of the notification equation — the observer. Each time an AlbumView sends a BLDownloadImage notification, the LibraryAPI is notified because it has registered as the observer of the notification. The LibraryAPI then responds and calls downloadImage(with:).

Before you can implement downloadImage(with:), there’s one more thing to do. It might be a good idea to save the downloaded covers locally so that the application doesn’t have to download the same covers over and over again.

Open persistencyManager.swift and replace import Foundation with the following code:

import UIKit
Copy the code

This import is important because you’re dealing with UI objects, like UIImage.

Add the calculated property to the end of the class:

private var cache: URL {
  return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]}Copy the code

This variable returns the URL of the cache directory, which is a good place to store files that you can re-download at any time.

Now add the following two methods:

func saveImage(_ image: UIImage, filename: String) {
  let url = cache.appendingPathComponent(filename)
  guard let data = UIImagePNGRepresentation(image) else {
    return
  }
  try? data.write(to: url)
}

func getImage(with filename: String) -> UIImage? {
  let url = cache.appendingPathComponent(filename)
  guard let data = try? Data(contentsOf: url) else {
    return nil
  }
  return UIImage(data: data)
}
Copy the code

This code is very simple, the downloaded image will be stored in the Cache directory, and getImage(with:) will return nil if no match is found in the Cache directory.

Now open libraryapi.swift and change import Foundation to Import UIKit.

Add the following method at the end of the class:

@objc func downloadImage(with notification: Notification) {
  guard let userInfo = notification.userInfo,
    let imageView = userInfo["imageView"] as? UIImageView.let coverUrl = userInfo["coverUrl"] as? String.let filename = URL(string: coverUrl)? .lastPathComponentelse {
      return
  }

  if let savedImage = persistencyManager.getImage(with: filename) {
    imageView.image = savedImage
    return
  }

  DispatchQueue.global().async {
    let downloadedImage = self.httpClient.downloadImage(coverUrl) ?? UIImage(a)DispatchQueue.main.async {
      imageView.image = downloadedImage
      self.persistencyManager.saveImage(downloadedImage, filename: filename)
    }
  }
}
Copy the code

Here is a detailed explanation of the above two methods:

  1. downloadImageThe call is triggered by a notification, so the method takes a notification object as an argument. Fetch from the object passed to the notificationUIImageViewAnd the URL of image.
  2. If you have downloaded it before, thePersistencyManagerRetrieve an image from
  3. If the image has not already been downloadedHTTPClientRetrieval.
  4. Once the download is complete, display the image in imageView and use itPersistencyManagerSave it locally.

Again, you use appearance mode to hide the complex process of downloading images from other classes. The notification sender does not care whether the image is downloaded from the network or from local storage.

Compile and run your application, and now you can see the beautiful cover in the collectionView:

Stop your application and run it again. Note that there is no delay in loading covers because they are saved locally. You can even disconnect from the Internet and the app will still work perfectly. However, there is a strange thing here, the rotation-loaded animation never stops! What’s going on here?

You started the rotation animation when you downloaded the image, but you didn’t implement the logic to stop loading the animation after downloading the image. You should send notifications every time you download an image, but next you will use key value listening (KVO) to do this.

Key value Listening (KVO)

In KVO, an object can listen for any changes to a particular property, either its own property or that of another object. If you are interested, you can read the KVO development documentation for more information.

How to use key value listening

As mentioned above, the key-value listening mechanism allows an object to observe changes in properties. In your case, you can use keyvalue listeners to listen for changes to the image property in the UIImageView that displays the image.

Open AlbumView.swift and go to private Var indicatorView: UIActivityIndicatorView! Add the following attributes to the declaration of:

private var valueObservation: NSKeyValueObservation!
Copy the code

Before adding the cover’s imageView as a child view, add the following code to commonInit:

valueObservation = coverImageView.observe(\.image, options: [.new]) { [unowned self] observed, change in
  if change.newValue is UIImage {
      self.indicatorView.stopAnimating()
  }
}
Copy the code

This code uses imageView as the observer of the image property of the cover image. \. Image is a keyPath expression with this function enabled.

In Swift 4, the keyPath expression has the following form:

\ <type>.<property>.<subproperty>
Copy the code

Type can usually be inferred by the compiler, but you need to provide at least one property. In some cases, it may make sense to use attributes of attributes. In your case, we have specified the property name image and omitted the type name UIImageView.

A trailing closure specifies the closure to execute each time a property change is observed. In the code above, you stop the loaded rotation animation when the image property changes. Once this is done, the rotation animation will stop when the image is loaded.

Compile and run your project, and the rotation animation in the load will disappear:

Note: Always remember to delete your observers when they are destroyed, or your application will crash when objects try to send messages to non-existent observers! In this case, when the album view is removed, valueObservation will be destroyed, so listening will stop.

If you use your app a little bit and then terminate it, you’ll notice that your app state is not saved. When the app starts, the last album you view will not be the default album.

To correct this, you can use the next pattern from the previous list: memos.

Memo mode

The memo pattern captures and exposes the internal state of an object. In other words, it can store your stuff somewhere and restore it to its exposed state later without violating the principle of encapsulation. That is, private data is still private.

How to use memo mode

IOS uses the memo mode as part of state recovery. You can learn more by reading our tutorial, but essentially it stores and reapplies your application state so that the user goes back to the state of the last action.

To activate state recovery in your application, open Main.storyboard, select Navigation Controller, Then locate the Restoration ID field in the Identity Inspector and enter NavigationController.

Select the Pop Music Scene and enter the ViewController in the previous position. These ids tell the system that when your application reboots, you want to restore the state of these ViewControllers.

Add the following code to appdelegate.swift:

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
  return true
}

func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
  return true
}
Copy the code

The following code restores the open state of your application as a whole. Now add the following code to the Constants enumeration in ViewController.swift:

static let IndexRestorationKey = "currentAlbumIndex"
Copy the code

This static constant will be used to save and restore the index of the current album, now add the following code:

override func encodeRestorableState(with coder: NSCoder) {
  coder.encode(currentAlbumIndex, forKey: Constants.IndexRestorationKey)
  super.encodeRestorableState(with: coder)
}

override func decodeRestorableState(with coder: NSCoder) {
  super.decodeRestorableState(with: coder)
  currentAlbumIndex = coder.decodeInteger(forKey: Constants.IndexRestorationKey)
  showDataForAlbum(at: currentAlbumIndex)
  horizontalScrollerView.reload()
}
Copy the code

Here you will save the index (which is done when the application goes into the background) and restore it (which is done after loading the View in the Controller at application startup). After restoring the index, update the tableView and scrollView to show the updated selected status. One more thing to do is that you need to scroll the scrollView to the correct position. If you’re scrolling scrollView here, that’s not going to work because the view isn’t laid out yet. Now add code in the right place to scroll the scrollView to the corresponding view:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  horizontalScrollerView.scrollToView(at: currentAlbumIndex, animated: false)}Copy the code

Compile and run your app, click on one of the albums, then press the Home button to bring the app into the background (if you’re running on the emulator, you can also press Command+Shift+H), then stop running your app from Xcode and restart. Check to see if the album you chose before is in the middle:

If you look at the init method in the PersistencyManager, you’ll notice that the album data is hardcoded and recreated each time the PersistencyManager is created. But a better solution is to create the album list once and store it in a file. So how do you save the Album’s data to a file?

One option would be to iterate over the Album’s properties and save them to a PList file, then recreate the Album instance if needed, but this isn’t optimal because it requires you to write specific code based on the data or properties in each class. If you later create Movie classes with different properties, Saving and loading the data will require a new code rewrite.

In addition, you won’t be able to save private variables for each class instance, because external classes aren’t hard to access, which is why Apple created archiving and serialization mechanisms.

Archiving and serialization

One specific implementation of Apple’s memo pattern is through archiving and serialization. Prior to Swift 4, you had to go through a number of steps in order to serialize and save your custom types. For classes, you need to inherit from NSObject and follow the NSCoding protocol.

But value types like structs and enumerations require a child object that can extend NSObject and obey NSCoding.

Swift 4 solves this problem with the three types of classes, structs, and enumerations: [SE-0166].

How to use archiving and serialization

Open Album. Swift and have the Album follow Codable. This protocol allows classes in Swift to comply with both Encodable and Decodable. If all attributes are Codable, the implementation of the protocol is automatically generated by the compiler.

Your code will now look like this:

struct Album: Codable {
  let title : String
  let artist : String
  let genre : String
  let coverUrl : String
  let year : String
}
Copy the code

To encode objects, you need to use encoder. Open persistencyManager.swift and add the following code:

private var documents: URL {
  return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]}private enum Filenames {
  static let Albums = "albums.json"
}

func saveAlbums(a) {
  let url = documents.appendingPathComponent(Filenames.Albums)
  let encoder = JSONEncoder(a)guard let encodedData = try? encoder.encode(albums) else {
    return
  }
  try? encodedData.write(to: url)
}
Copy the code

Just like with Caches, you define a URL here to hold the file directory, which is a constant to store the filename path, and then a way to write your album data to the file, without actually writing much code!

Another part of the scheme is to decode the data back into concrete objects. You now need to replace the method of creating albums and loading long chunks of them from files. Download and unzip the JSON file and add it to your project.

Now replace the init method body in persistencyManager.swift with the following code:

let savedURL = documents.appendingPathComponent(Filenames.Albums)
var data = try? Data(contentsOf: savedURL)
if data == nil.let bundleURL = Bundle.main.url(forResource: Filenames.Albums, withExtension: nil) {
  data = try? Data(contentsOf: bundleURL)
}

if let albumData = data,
  let decodedAlbums = try? JSONDecoder().decode([Album].self, from: albumData) {
  albums = decodedAlbums
  saveAlbums()
}
Copy the code

Now you are loading the album data (if it exists) from a file in the Documents directory. If it doesn’t exist, load it from the startup file you added earlier, and then save it immediately, so it will be in the documentation directory the next time you start. JSONDecoder is so smart, you just tell it what type you want the file to include and it does all the rest for you!

You may also want to save the album data every time your app goes into the background, but I’m going to make this part a challenge for you to figure out how it works for yourself. Some of the patterns and techniques you’ve learned in these two tutorials will come in handy!

What’s next?

You can download the final project here.

In this tutorial you learned how to leverage the power of the iOS design pattern to perform complex tasks in a straightforward manner. You’ve learned a lot about iOS design patterns and concepts: singletons, MVC, agents, protocols, skins, observers, and memos.

Your end code will be poorly coupled, reusable, and readable. If other developers read your code, they can easily understand what each line of code does and what each class does in your application.

The key is not to use design patterns just because you are using them. However, when considering how to solve a particular problem, keep an eye on design patterns, especially in the early stages of designing an application. They will make your life easier as a developer, and the code will be better too!

A classic book on the subject of this article is Design Patterns: Elements of Reusable Object-oriented Software. For code examples, check out GitHub’s great project Design Patterns: Elements of Reusable Object-oriented Software for more Design Patterns in programming in Swift.

Finally, be sure to check out Swift Advanced Design Patterns and our video lesson iOS Design Patterns to learn more about Design Patterns!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.