This article is part of an email series I wrote to my subscribers to teach them how to refactor existing legacy code. If you want to join us in the next workshop, make sure you subscribe.
- What is wrong with this viewDidLoad() method?
- Don’t waste time writing tests for untestable code
- Breaking up a method into shorter methods with single responsibilities
- How to write clean code in a component architecture using Clean Swift
Now that you’ve seen the importance of breaking up a large method into multiple shorter methods and the benefits. You’ve also seen what these shorter methods look like.
However, the coordinator methods are still somewhat hairy.
Today, you’re going to see how the VIP components in the Clean Swift architecture can work together to solve the coordinator methods problem.
A little analogy may help.
If you need to model a chessboard, you’ll use a two-dimensional array to model the grid. You can divide the chessboard up into rows and columns. On the other hand, with a one-dimensional array, you would have to calculate where it should break into the next row.
In the Clean Swift architecture, you can also divide your code up into components and methods.
In line with the two-dimensional array analogy, you can think [component, method]
.
Certainly, you can take the one-dimensional approach. You can refactor your business logic into many methods within your view controller. But the methods can invoke one another. It is very easy, convenient, and tempting to take shortcuts. It tastes like spaghetti.
However, you can take the two-dimensional approach, and separate the responsibilities into components then methods.
You can turn the coordinator methods, displayFollowerPosts()
and updateFollowerPosts()
, into single responsibility methods spread across the view controller, interactor, and presenter.
The Interactor handles your business logic. The Presenter handles your presentation logic. The View Controller handles your display logic.
Therefore, Clean Swift is a component architecture.
Here’s what the componentized version of the original massive viewDidLoad()
method look like. Each method is doing ONE thing only.
ListPostsViewController.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
protocol ListPostsViewControllerInput { func displayFetchFollowerPosts(viewModel: ListPosts_FetchFollowerPosts_ViewModel) func displayFetchFollowerPostsFetchError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) func displayFetchFollowerPostsLoginError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) } protocol ListPostsViewControllerOutput { func fetchFollowerPosts(request: ListPosts_FetchFollowerPosts_Request) } class ListPostsViewController: UIViewController, ListPostsViewControllerInput, UITableViewDataSource { var output: ListPostsViewControllerOutput! var router: ListPostsRouter! @IBOutlet weak var loginButton: UIButton! @IBOutlet weak var tableView: UITableView! var posts: [ListPosts_FetchFollowerPosts_ViewModel.Post] = [] // MARK: Object lifecycle override func awakeFromNib() { super.awakeFromNib() ListPostsConfigurator.sharedInstance.configure(self) } // MARK: View lifecycle override func viewDidLoad() { super.viewDidLoad() fetchFollowerPostsOnLoad() } // MARK: Fetch follower Posts private func fetchFollowerPostsOnLoad() { let request = ListPosts_FetchFollowerPosts_Request() output.fetchFollowerPosts(request) } // MARK: Display fetch follower posts or errors func displayFetchFollowerPosts(viewModel: ListPosts_FetchFollowerPosts_ViewModel) { if let posts = viewModel.posts { refreshFollowerPosts(posts) hideLoginButton() } } func displayFetchFollowerPostsFetchError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) { if let error = viewModel.error { clearFollowerPosts() hideLoginButton() showAlert(error) } } func displayFetchFollowerPostsLoginError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) { if let _ = viewModel.error { showLoginButton() } } // MARK: Show/clear posts in table view private func refreshFollowerPosts(posts: [ListPosts_FetchFollowerPosts_ViewModel.Post]) { self.posts = posts tableView.reloadData() } private func clearFollowerPosts() { self.posts = [] tableView.reloadData() } // MARK: Show fetch error private func showAlert(error: String) { // ... } // MAARK: Show/hide login button private func showLoginButton() { loginButton.hidden = false } private func hideLoginButton() { loginButton.hidden = true } // MARK: Table view func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return posts.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let post = posts[indexPath.row] let cell = tableView.dequeueReusableCellWithIdentifier("PostCell")! cell.textLabel?.text = post.title cell.detailTextLabel?.text = post.publishedOn return cell } } |
ListPostsInteractor.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
protocol ListPostsInteractorInput { func fetchFollowerPosts(request: ListPosts_FetchFollowerPosts_Request) } protocol ListPostsInteractorOutput { func presentFetchFollowerPosts(response: ListPosts_FetchFollowerPosts_Response) } class ListPostsInteractor: ListPostsInteractorInput { var output: ListPostsInteractorOutput! var worker = ListPostsWorker() let userManager = UserManager() var recentPosts = [Post]() // MARK: Follower Posts func fetchFollowerPosts(request: ListPosts_FetchFollowerPosts_Request) { if let currentUser = userManager.loggedInUser() { let followers = userManager.followersForUser(currentUser) worker.fetchPostsByAllFollowers(followers, completionHandler: { (posts: [Post]?, error: PostManagerError?) -> () in if let error = error { self.handleFetchPostsByAllFollowersFailure(error) } else if let posts = posts { self.handleFetchPostsByAllFollowersSuccess(posts) } }) } else { handleLoggedInUserNotExist() } } private func handleFetchPostsByAllFollowersSuccess(posts: [Post]) { let posts = Array(posts.sort { $0 > $1 }.prefix(5)) let response = ListPosts_FetchFollowerPosts_Response(posts: posts, error: nil) output.presentFetchFollowerPosts(response) } private func handleFetchPostsByAllFollowersFailure(err: PostManagerError) { let error: ListPosts_FetchFollowerPosts_Error switch err { case .CannotFetch(let msg): error = ListPosts_FetchFollowerPosts_Error.CannotFetch(msg: msg) } let response = ListPosts_FetchFollowerPosts_Response(posts: nil, error: error) output.presentFetchFollowerPosts(response) } private func handleLoggedInUserNotExist() { let error = ListPosts_FetchFollowerPosts_Error.NotLoggedIn(msg: "User is not logged in") let response = ListPosts_FetchFollowerPosts_Response(posts: nil, error: error) output.presentFetchFollowerPosts(response) } } |
ListPostsPresenter.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
protocol ListPostsPresenterInput { func presentFetchFollowerPosts(response: ListPosts_FetchFollowerPosts_Response) } protocol ListPostsPresenterOutput: class { func displayFetchFollowerPosts(viewModel: ListPosts_FetchFollowerPosts_ViewModel) func displayFetchFollowerPostsFetchError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) func displayFetchFollowerPostsLoginError(viewModel: ListPosts_FetchFollowerPosts_ViewModel) } class ListPostsPresenter: ListPostsPresenterInput { weak var output: ListPostsPresenterOutput! var dateFormatter: NSDateFormatter { let df = NSDateFormatter() df.dateStyle = .MediumStyle df.timeStyle = .MediumStyle return df } // MARK: Follower Posts func presentFetchFollowerPosts(response: ListPosts_FetchFollowerPosts_Response) { if let posts = response.posts { if !posts.isEmpty { handlePresentFetchFollowerPostsSuccess(response.posts!) } } else if let error = response.error { switch error { case .CannotFetch: handlePresentFetchFollowerPostsFailure(error) case .NotLoggedIn: handlePresentFetchFollowerPostsLoginError(error) } } } private func handlePresentFetchFollowerPostsSuccess(followerPosts: [Post]) { let posts = formatFetchFollowerPosts(followerPosts) let viewModel = ListPosts_FetchFollowerPosts_ViewModel(posts: posts, error: nil) output.displayFetchFollowerPosts(viewModel) } private func handlePresentFetchFollowerPostsFailure(error: ListPosts_FetchFollowerPosts_Error) { let errorMsg = formatFetchFollowerPostsError(error) let viewModel = ListPosts_FetchFollowerPosts_ViewModel(posts: [], error: errorMsg) output.displayFetchFollowerPostsFetchError(viewModel) } private func handlePresentFetchFollowerPostsLoginError(error: ListPosts_FetchFollowerPosts_Error) { let errorMsg = formatFetchFollowerPostsError(error) let viewModel = ListPosts_FetchFollowerPosts_ViewModel(posts: [], error: errorMsg) output.displayFetchFollowerPostsLoginError(viewModel) } // MARK: Formatting private func formatFetchFollowerPosts(posts: [Post]?) -> [ListPosts_FetchFollowerPosts_ViewModel.Post] { var recentPosts: [ListPosts_FetchFollowerPosts_ViewModel.Post] = [] if let posts = posts { for post in posts { let title = post.title let author = "\(post.user.firstName) \(post.user.lastName)" let publishedOn = dateFormatter.stringFromDate(post.timestamp) let recentPost = ListPosts_FetchFollowerPosts_ViewModel.Post(title: title, author: author, publishedOn: publishedOn) recentPosts.append(recentPost) } } return recentPosts } private func formatFetchFollowerPostsError(error: ListPosts_FetchFollowerPosts_Error?) -> String? { var errorMsg: String? if let error = error { switch error { case .CannotFetch(let msg): errorMsg = "ERROR: Cannot fetch - \(msg)" case .NotLoggedIn(let msg): errorMsg = "ERROR: Cannot login - \(msg)" } } return errorMsg } } |
ListPostsWorker.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class ListPostsWorker { let postManager = PostManager() // MARK: Business Logic func fetchPostsByAllFollowers(followers: [User], completionHandler: (posts: [Post]?, error: PostManagerError?) -> ()) { var allFollowerPosts = [User: [Post]]() for follower in followers { fetchPostsByFollower(follower) { (posts: [Post]?, error: PostManagerError?) -> () in if let error = error { completionHandler(posts: nil, error: error) } else if let posts = posts { allFollowerPosts[follower] = posts if allFollowerPosts.count == followers.count { completionHandler(posts: Array(allFollowerPosts.values.flatten()), error: nil) } } } } } func fetchPostsByFollower(follower: User, completionHandler: (posts: [Post]?, error: PostManagerError?) -> ()) { postManager.fetchPostsForUser(follower) { (posts: [Post]?, error: PostManagerError?) -> () in if let error = error { completionHandler(posts: nil, error: error) } else if let posts = posts { completionHandler(posts: posts, error: nil) } } } } |
You can find the Xcode project at GitHub here.
Now the code looks vastly different than the original massive viewDidLoad()
method. Does it take a lot of effort to go from here to there? Is it worth it?
What if I tell you it takes less effort to write this better version than the original massive version?
How so? There is more code for sure.
Yes. More code. But, write faster. Think less. Higher productivity. More happiness.
If you follow the Clean Swift architecture from the beginning, you’ll be able to write small, single responsibility methods. Your code will naturally fall into the right place so that you don’t have to spend time to refactor it later. You’ll find joy in writing and testing code because it’s just intuitive. You’ll appreciate the simplicity a clean architecture can bring.
If you want to see how I apply Clean Swift to the same viewDidLoad()
method starting from scratch, I’m running a free LIVE workshop next week. More details on the date & time plus an outline to come in my next email.
In the meantime, you can send in your question in advance now. Just hit reply. I’ll try my best to fit your question in the Q&A following the workshop.