Skip to content

Instantly share code, notes, and snippets.

@lucaswkuipers
Created June 21, 2022 05:55
Show Gist options
  • Save lucaswkuipers/0d6731682279b781b9c8417dac2c7ac7 to your computer and use it in GitHub Desktop.
Save lucaswkuipers/0d6731682279b781b9c8417dac2c7ac7 to your computer and use it in GitHub Desktop.
import UIKit
protocol ReusableViewControllerDelegate {
func loadView()
func viewDidLoad()
func viewWillAppear(_ animated: Bool)
func viewWillLayoutSubviews()
func viewDidLayoutSubviews()
func viewDidAppear(_ animated: Bool)
func viewWillDisappear(_ animated: Bool)
func viewDidDisappear(_ animated: Bool)
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
func traitCollectionDidChange(from previousTraitCollection: UITraitCollection?, to currentTraitCollection: UITraitCollection?)
func didReceiveMemoryWarning()
}
extension ReusableViewControllerDelegate {
func loadView() {}
func viewDidLoad() {}
func viewWillAppear(_ animated: Bool) {}
func viewWillLayoutSubviews() {}
func viewDidLayoutSubviews() {}
func viewDidAppear(_ animated: Bool) {}
func viewWillDisappear(_ animated: Bool) {}
func viewDidDisappear(_ animated: Bool) {}
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {}
func traitCollectionDidChange(from previousTraitCollection: UITraitCollection?, to currentTraitCollection: UITraitCollection?) {}
func didReceiveMemoryWarning() {}
}
final class ReusableViewController: UIViewController {
var delegate: ReusableViewControllerDelegate?
private let contentView: UIView
private let isNavigationBarHidden: Bool
init(
with view: UIView,
title: String? = nil,
isNavigationBarHidden: Bool = false
) {
self.isNavigationBarHidden = isNavigationBarHidden
self.contentView = view
contentView.generateAccessibilityIdentifiers()
super.init(nibName: nil, bundle: nil)
self.title = title ?? view.getTitle()
}
required init?(coder: NSCoder) {
fatalError()
}
override func loadView() {
super.loadView()
self.view = contentView
delegate?.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
delegate?.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
delegate?.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = isNavigationBarHidden
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
delegate?.viewWillLayoutSubviews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
delegate?.viewDidLayoutSubviews()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
delegate?.viewDidAppear(animated)
removeDuplicatesFromNavigation()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.viewWillDisappear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
delegate?.viewDidDisappear(animated)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
delegate?.viewWillTransition(to: size, with: coordinator)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
delegate?.traitCollectionDidChange(from: previousTraitCollection, to: traitCollection)
}
override func didReceiveMemoryWarning() {
delegate?.didReceiveMemoryWarning()
}
}
import XCTest
final class ReusableViewControllerTests: XCTestCase {
func test_init_setsViewCorrectly() {
let (sut, view, _) = makeSUT()
XCTAssertEqual(sut.view, view)
}
func test_appearingTransition_callsDelegateCorrectly() {
let (sut, _, delegateSpy) = makeSUT()
sut.appearingTransition(isAnimated: true)
sut.appearingTransition(isAnimated: false)
XCTAssertEqual(
delegateSpy.receivedMessages,
[
.loadView,
.viewDidLoad,
.viewWillAppear(true),
.viewDidAppear(true),
.viewWillAppear(false),
.viewDidAppear(false)
]
)
}
func test_disappearingTransition_callsDelegateCorrectly() {
let (sut, _, delegateSpy) = makeSUT()
sut.disappearingTransition(isAnimated: true)
sut.disappearingTransition(isAnimated: false)
XCTAssertEqual(
delegateSpy.receivedMessages,
[
.viewWillDisappear(true),
.viewDidDisappear(true),
.viewWillDisappear(false),
.viewDidDisappear(false)
]
)
}
func test_layoutView_callsDelegateCorrectly() {
let (sut, _, delegateSpy) = makeSUT()
sut.view.setNeedsLayout()
sut.view.layoutIfNeeded()
XCTAssertEqual(
delegateSpy.receivedMessages,
[
.loadView,
.viewDidLoad,
.viewWillLayoutSubviews,
.viewDidLayoutSubviews
]
)
}
func test_viewWillTransition_callsDelegateCorrectly() {
let (sut, _, delegateSpy) = makeSUT()
let size = CGSize.fixture()
sut.viewWillTransition(to: size, with: AnyTransitionCoordinator())
XCTAssertEqual(
delegateSpy.receivedMessages,
[
.viewWillTransition(size: size)
]
)
}
private final class DelegateSpy: ReusableViewControllerDelegate {
enum Message: Equatable {
case loadView
case viewDidLoad
case viewWillAppear(_ animated: Bool)
case viewWillLayoutSubviews
case viewDidLayoutSubviews
case viewDidAppear(_ animated: Bool)
case viewWillDisappear(_ animated: Bool)
case viewDidDisappear(_ animated: Bool)
case viewWillTransition(size: CGSize)
}
private(set) var receivedMessages: [Message] = []
func loadView() {
receivedMessages.append(.loadView)
}
func viewDidLoad() {
receivedMessages.append(.viewDidLoad)
}
func viewWillAppear(_ animated: Bool) {
receivedMessages.append(.viewWillAppear(animated))
}
func viewWillLayoutSubviews() {
receivedMessages.append(.viewWillLayoutSubviews)
}
func viewDidLayoutSubviews() {
receivedMessages.append(.viewDidLayoutSubviews)
}
func viewDidAppear(_ animated: Bool) {
receivedMessages.append(.viewDidAppear(animated))
}
func viewWillDisappear(_ animated: Bool) {
receivedMessages.append(.viewWillDisappear(animated))
}
func viewDidDisappear(_ animated: Bool) {
receivedMessages.append(.viewDidDisappear(animated))
}
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
receivedMessages.append(.viewWillTransition(size: size))
}
}
private func makeSUT() -> (ReusableViewController, UIView, DelegateSpy) {
let view = UIView()
let sut = ReusableViewController(with: view)
let delegateSpy = DelegateSpy()
sut.delegate = delegateSpy
trackForMemoryLeaks(sut)
trackForMemoryLeaks(delegateSpy)
return (sut, view, delegateSpy)
}
}
import UIKit
extension UIView {
func getTitle() -> String {
let title = String(describing: type(of: self)).camelCaseToWords().replacingOccurrences(of: " View", with: "")
return title
}
}
import UIKit
extension UIView {
private var typeName: String { String(describing: type(of: self)) }
func generateAccessibilityIdentifiers() {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
guard let view = child.value as? UIView else { continue }
guard let identifier = child.label?.replacingOccurrences(of: ".storage", with: "") else { continue }
view.accessibilityIdentifier = "\(identifier)"
}
}
subscript(subview accessibilityIdentifier: String) -> UIView? {
return subviews.first { $0.accessibilityIdentifier == accessibilityIdentifier }
}
}
import XCTest
extension XCTestCase {
func trackForMemoryLeaks(_ instance: AnyObject) {
addTeardownBlock { [weak instance] in
XCTAssertNil(instance, "Instance should have been deallocated. Potencial memory leak")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment