Best Patterns for Extending Controller Responsibilities in Swift

eray diler
Hipo
Published in
5 min readApr 24, 2018

--

Our approach to app development is very iterative at Hipo and we often need to add new functionality to existing controllers. There are lots of different approaches you can follow while implementing these changes. We will investigate various options here to see which approach is the most maintainable and clean.

As an example, let’s assume we have a controller for publishing videos. VideoPostViewController would be a good name for our controller in this scenario. A basic skeleton for the controller would be like this:

class VideoPostViewController: BaseViewController {
private let videoURL: URL
private let uploadManager: VideoUploadManager

// MARK: - Initialization

init(with url: URL, and uploadManager: VideoUploadManager) {
self.videoURL = url
self.uploadManager = uploadManager
super.init()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View lifecycle

override func viewDidLoad() {
super.viewDidLoad()
...
}

// MARK: - Actions

func postButtonDidTap(_ button: UIButton) {
createVideoPost()
}

// MARK: - Video Upload

private func createVideoPost() {
...
}
}

This controller is initialized by the video’s local url, and an uploadManager instance is responsible for doing the actual heavy lifting of uploading the video to the server.

Now assume that we also need to call this controller from another part of the codebase with another parameter (with a PHAsset object for instance). We can apply this change by following three different approaches.

1. Optionals approach

We can simply add a new property into the existing controller which results in the following setup:

class VideoPostViewController: BaseViewController {
private let videoURL: URL?
private let asset: PHAsset?
private let uploadManager: VideoUploadManager

// MARK: - Initialization

init(with url: URL?, asset: PHAsset?, and uploadManager: VideoUploadManager) {
self.videoURL = url
self.uploadManager = uploadManager
super.init()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View lifecycle

override func viewDidLoad() {
super.viewDidLoad()

if let videoUrl = self.videoUrl {
// use url for some configuration
} else if let asset = self.asset {
// use asset for some configuration
}

...
}

// MARK: - Actions

func postButtonDidTap(_ button: UIButton) {
createVideoPost()
}

// MARK: - Video Upload

private func createVideoPost() {
...
}
}

Adding a new property caused both properties to be optionals. Because the properties are optionals now we must add nil checks or unwrapping lines whenever we want to use one of these properties. We don’t like this approach since it complicates the implementation and makes code flow harder to read.

2. State approach

Another option is to embed the optional properties into a state enum:

class VideoPostViewController: BaseViewController {
enum State {
case url(URL)
case asset(PHAsset)
}

private let uploadManager: VideoUploadManager

// MARK: - Initialization

init(with state: VideoPostViewController.State, and uploadManager: UploadManager) {
self.state = state
self.uploadManager = uploadManager

super.init()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View lifecycle

override func viewDidLoad() {
super.viewDidLoad()
switch state {
case .post(let post):
// configure view with the post
case .postIdentifier(let identifier):
// fetch post with the identifier
}
...
}

// MARK: - Actions

func postButtonDidTap(_ button: UIButton) {
createVideoPost()
}

// MARK: - Video Upload

private func createVideoPost() {
...
}
}

With this approach we avoid nil checks. However we still need to check which property is available to use whenever we need one of them. Also the controller ends up with lots of switch statements, because we have to check the state to access post or identifier values. This is still not ideal.

3. Fragmentation approach

As another option we can add a new controller and pass the asset parameter in init method. Thus, there is no need any optional or switch statements. This approach requires the existing code to be fragmented into multiple controllers, but ends up creating a much cleaner setup.

Another benefit is that code is easier to maintain because responsibilities are separated, and we don’t think twice while changing something if it affects or breaks another part.

We can use inheritance or composition patterns for implementing this approach.

Inheritance

We can move common code into a new base view controller and create another controller to be initialized with the asset parameter:

class BaseVideoPostViewController: BaseViewController {
private let uploadManager: VideoUploadManager

// MARK: - Initialization

init(with uploadManager: UploadManager) {
self.uploadManager = uploadManager

super.init()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View lifecycle

override func viewDidLoad() {
super.viewDidLoad()
...
}

// MARK: - Actions

func postButtonDidTap(_ button: UIButton) {
createVideoPost()
}

// MARK: - Video Upload

private func createVideoPost() {
...
}
}
class VideoURLPostViewController: BaseVideoPostViewController {
private let videoURL: URL
init(withVideoURL url: URL, and uploadManager: VideoUploadManager) {
self.videoURL = url
super.init(with: uploadManager)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class VideoAssetPostViewController: BaseVideoPostViewController {
private let asset: PHAsset
private lazy var progressView = UIProgressView()
private let assetFetcher: VideoAssetFetcher
init(with asset: PHAsset, and uploadManager: VideoUploadManager){
self.asset = asset
self.assetFetcher = VideoAssetFetcher(with: asset)

super.init(with: uploadManager)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

setupProgressViewLayout()
displayVideoThumbnail()

updateRightNavigationBarButton()
}
// MARK: - Layout

private func setupProgressViewLayout() {
...
}

fileprivate func displayVideoThumbnail() {
...
}

fileprivate func updateRightNavigationBarButton() {
...
}

// MARK: - Visibility

fileprivate func removeProgressViewLayoutAnimated() {
...
}
}

Notice that VideoAssetPostViewController has some additional properties and relevant UI logic as well. Because we need some additional effort to access a video url, or video image of a PHAsset variable, we should show a progress bar in the screen to show up while fetching video from PHAsset. This separation of logic saves us from thinking in a controller that already has lots of responsibilities, properties and methods, creating lightweight, easily understandable controllers rather than massive ones that are responsible for everything.

Composition

With the composition pattern we need to create another property to access BaseVideoPostViewController rather than inheriting it:

class VideoURLPostViewController: UIViewController {
private let baseVideoPostViewController: BaseVideoPostViewController
private let videoURL: URL

init(withVideoURL url: URL, and uploadManager: VideoUploadManager) {
self.videoURL = url
self.baseVideoPostViewController = BaseVideoPostViewController(with: uploadManager)
super.init(nibName: nil, bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class VideoAssetPostViewController: UIViewController {
private let baseVideoPostViewController: BaseVideoPostViewController
private let asset: PHAsset
private let assetFetcher: VideoAssetFetcher
private lazy var progressView = UIProgressView()

init(with asset: PHAsset, and uploadManager: VideoUploadManager) {
self.asset = asset
self.assetFetcher = VideoAssetFetcher(with: asset)
self.baseVideoPostViewController = BaseVideoPostViewController(with: uploadManager)

super.init(nibName: nil, bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Conclusion

In summary, whenever you need to modify a controller to add a new feature, it’s worth investigating whether fragmenting into a new controller would be the best choice. This usually makes the controllers easier to maintain while preventing code repetition.

Thank you for reading!

--

--