From 670c1cff8b3dcf2b51c1e1c8738ccc43130b1f05 Mon Sep 17 00:00:00 2001 From: "Bradley S. Maskell" Date: Wed, 29 Mar 2023 16:41:33 -0400 Subject: [PATCH] feat: enabled subclassing on BeagleScreenViewControllers to observe lifecycle events as well as server driven state events without having to use a navigation controller. (#52) --- .../Beagle/Beagle.xcodeproj/project.pbxproj | 8 +- ...ustomBeagleScreenViewControllerTests.swift | 82 +++++++++++++++++++ .../Renderer/BeagleScreenViewController.swift | 26 +++--- .../Renderer/BeagleScreenViewModel.swift | 10 +-- 4 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 Sources/Beagle/BeagleTests/Renderer/CustomBeagleScreenViewControllerTests.swift diff --git a/Sources/Beagle/Beagle.xcodeproj/project.pbxproj b/Sources/Beagle/Beagle.xcodeproj/project.pbxproj index 932464f..887b4ff 100644 --- a/Sources/Beagle/Beagle.xcodeproj/project.pbxproj +++ b/Sources/Beagle/Beagle.xcodeproj/project.pbxproj @@ -222,10 +222,10 @@ 9398CB10255591310003A010 /* RepresentableByParsableString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9398CB0F255591310003A010 /* RepresentableByParsableString.swift */; }; 93A2EE1C24EC4BB00085B3CF /* AddChildren+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A2EE1B24EC4BB00085B3CF /* AddChildren+Extensions.swift */; }; 93B9744F23A2833600B0D1CF /* NetworkClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B9743F23A2833600B0D1CF /* NetworkClientProtocol.swift */; }; - 93C12E2228E75E3F008C051C /* OperationConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C12E2128E75E3F008C051C /* OperationConversionTests.swift */; }; 93C12E1C28E4BE92008C051C /* listUsingIndex.json in Resources */ = {isa = PBXBuildFile; fileRef = 93C12E1B28E4BE92008C051C /* listUsingIndex.json */; }; 93C12E1E28E4BF1B008C051C /* listUsingCustomIndexName.json in Resources */ = {isa = PBXBuildFile; fileRef = 93C12E1D28E4BF1B008C051C /* listUsingCustomIndexName.json */; }; 93C12E2028E4C26C008C051C /* listViewUpdatingDataSource.json in Resources */ = {isa = PBXBuildFile; fileRef = 93C12E1F28E4C26C008C051C /* listViewUpdatingDataSource.json */; }; + 93C12E2228E75E3F008C051C /* OperationConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C12E2128E75E3F008C051C /* OperationConversionTests.swift */; }; 93D24A512513DF47005A5CBD /* AutoLayoutWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D24A502513DF47005A5CBD /* AutoLayoutWrapper.swift */; }; 93EE0CC3252E56BC0032BE77 /* BindingConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EE0CC2252E56BC0032BE77 /* BindingConfigurator.swift */; }; 93F7072F271A021000202E36 /* AutoCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93F7072E271A021000202E36 /* AutoCodable.swift */; }; @@ -259,6 +259,7 @@ A9C6F54E273D858B00576313 /* DependenciesContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C6F54D273D858900576313 /* DependenciesContainerTests.swift */; }; A9C6F550273D90FC00576313 /* InjectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C6F54F273D90FC00576313 /* InjectedTests.swift */; }; A9C6F555273DB76200576313 /* Singletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C6F553273DB73C00576313 /* Singletons.swift */; }; + B253523D29D1EB2000E492E8 /* CustomBeagleScreenViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B253523C29D1EB2000E492E8 /* CustomBeagleScreenViewControllerTests.swift */; }; B755B58A2686602700A7EDC5 /* GridViewDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B755B5892686602700A7EDC5 /* GridViewDecodeTests.swift */; }; B755B591268667D400A7EDC5 /* gridViewSpanCount.json in Resources */ = {isa = PBXBuildFile; fileRef = B755B590268667D400A7EDC5 /* gridViewSpanCount.json */; }; C0291889247D5BFA00C3AA13 /* ComponentFromJsonFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0291888247D5BFA00C3AA13 /* ComponentFromJsonFile.swift */; }; @@ -576,10 +577,10 @@ 9398CB0F255591310003A010 /* RepresentableByParsableString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepresentableByParsableString.swift; sourceTree = ""; }; 93A2EE1B24EC4BB00085B3CF /* AddChildren+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddChildren+Extensions.swift"; sourceTree = ""; }; 93B9743F23A2833600B0D1CF /* NetworkClientProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkClientProtocol.swift; sourceTree = ""; }; - 93C12E2128E75E3F008C051C /* OperationConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConversionTests.swift; sourceTree = ""; }; 93C12E1B28E4BE92008C051C /* listUsingIndex.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = listUsingIndex.json; sourceTree = ""; }; 93C12E1D28E4BF1B008C051C /* listUsingCustomIndexName.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = listUsingCustomIndexName.json; sourceTree = ""; }; 93C12E1F28E4C26C008C051C /* listViewUpdatingDataSource.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = listViewUpdatingDataSource.json; sourceTree = ""; }; + 93C12E2128E75E3F008C051C /* OperationConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConversionTests.swift; sourceTree = ""; }; 93D24A502513DF47005A5CBD /* AutoLayoutWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayoutWrapper.swift; sourceTree = ""; }; 93EE0CC2252E56BC0032BE77 /* BindingConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingConfigurator.swift; sourceTree = ""; }; 93F7072E271A021000202E36 /* AutoCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCodable.swift; sourceTree = ""; }; @@ -613,6 +614,7 @@ A9C6F54D273D858900576313 /* DependenciesContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependenciesContainerTests.swift; sourceTree = ""; }; A9C6F54F273D90FC00576313 /* InjectedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InjectedTests.swift; sourceTree = ""; }; A9C6F553273DB73C00576313 /* Singletons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Singletons.swift; sourceTree = ""; }; + B253523C29D1EB2000E492E8 /* CustomBeagleScreenViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBeagleScreenViewControllerTests.swift; sourceTree = ""; }; B755B5892686602700A7EDC5 /* GridViewDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewDecodeTests.swift; sourceTree = ""; }; B755B590268667D400A7EDC5 /* gridViewSpanCount.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = gridViewSpanCount.json; sourceTree = ""; }; C0291888247D5BFA00C3AA13 /* ComponentFromJsonFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentFromJsonFile.swift; sourceTree = ""; }; @@ -1443,6 +1445,7 @@ children = ( 9356CEE726248D440021EFC6 /* __Snapshots__ */, 9356CD45262482810021EFC6 /* BeagleScreenViewControllerTests.swift */, + B253523C29D1EB2000E492E8 /* CustomBeagleScreenViewControllerTests.swift */, 9356CD46262482810021EFC6 /* BeagleViewBuilderTests.swift */, 9356CD47262482810021EFC6 /* AutoLayoutWrapperTests.swift */, 9356CD48262482810021EFC6 /* Json */, @@ -2411,6 +2414,7 @@ 9356CE43262482820021EFC6 /* UnitValueExtensionTests.swift in Sources */, 9356CD8F262482820021EFC6 /* BeagleLoggerTests.swift in Sources */, A9C6F550273D90FC00576313 /* InjectedTests.swift in Sources */, + B253523D29D1EB2000E492E8 /* CustomBeagleScreenViewControllerTests.swift in Sources */, 9356CDA0262482820021EFC6 /* ActionRecordFactoryTests.swift in Sources */, 9356CE47262482820021EFC6 /* MirrorExtension.swift in Sources */, C0E569152476F026003D413F /* BeaglePrefetchHelpingSpy.swift in Sources */, diff --git a/Sources/Beagle/BeagleTests/Renderer/CustomBeagleScreenViewControllerTests.swift b/Sources/Beagle/BeagleTests/Renderer/CustomBeagleScreenViewControllerTests.swift new file mode 100644 index 0000000..8db9d31 --- /dev/null +++ b/Sources/Beagle/BeagleTests/Renderer/CustomBeagleScreenViewControllerTests.swift @@ -0,0 +1,82 @@ +// +/* + * Copyright 2020 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest +import SnapshotTesting +@testable import Beagle + +final class CustomBeagleScreenViewControllerTests: EnvironmentTestCase { + + func testLifecycleMethods() { + let viewController = CustomBeagleScreenViewControllerStub(ComponentStub(), controllerId: #function) + viewController.viewDidAppearExpectation.expectedFulfillmentCount = 1 + viewController.viewWillAppearExpectation.expectedFulfillmentCount = 1 + viewController.viewDidDisappearExpectation.expectedFulfillmentCount = 1 + + viewController.viewWillAppear(true) + viewController.viewDidAppear(true) + viewController.viewDidDisappear(true) + + wait( + for: [ + viewController.viewDidAppearExpectation, + viewController.viewWillAppearExpectation, + viewController.viewDidDisappearExpectation + ], + timeout: 0.1 + ) + } + + func testScreenDidChangeState() { + let viewController = CustomBeagleScreenViewControllerStub(ComponentStub(), controllerId: #function) + viewController.didChangeStateExpectation.expectedFulfillmentCount = 1 + + viewController.didChangeState(.started) + + wait(for: [viewController.didChangeStateExpectation], timeout: 0.1) + } + +} + + +class CustomBeagleScreenViewControllerStub: BeagleScreenViewController { + + private(set) var viewWillAppearExpectation: XCTestExpectation = .init(description: "viewWillAppearExpectation") + private(set) var viewDidAppearExpectation: XCTestExpectation = .init(description: "viewDidAppearExpectation") + private(set) var viewDidDisappearExpectation: XCTestExpectation = .init(description: "viewDidDisappearExpectation") + private(set) var didChangeStateExpectation: XCTestExpectation = .init(description: "didChangeStateExpectation") + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewWillAppearExpectation.fulfill() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewDidAppearExpectation.fulfill() + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + viewDidDisappearExpectation.fulfill() + } + + override func didChangeState(_ state: ServerDrivenState) { + super.didChangeState(state) + didChangeStateExpectation.fulfill() + } +} diff --git a/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift b/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift index d4e3a06..1e291ed 100644 --- a/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift +++ b/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift @@ -37,7 +37,7 @@ public protocol BeagleControllerProtocol: NSObjectProtocol { func setNeedsLayout(component: UIView) } -public class BeagleScreenViewController: BeagleController { +open class BeagleScreenViewController: BeagleController { private let viewModel: BeagleScreenViewModel @@ -96,7 +96,7 @@ public class BeagleScreenViewController: BeagleController { self.navigationControllerId = controllerId } - required init(viewModel: BeagleScreenViewModel, controllerId: String? = nil, config: BeagleConfiguration = GlobalConfiguration) { + required public init(viewModel: BeagleScreenViewModel, controllerId: String? = nil, config: BeagleConfiguration = GlobalConfiguration) { self.viewModel = viewModel self.navigationControllerId = controllerId self.config = config @@ -107,7 +107,7 @@ public class BeagleScreenViewController: BeagleController { } @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -152,38 +152,42 @@ public class BeagleScreenViewController: BeagleController { // MARK: - Lifecycle - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() initView() createContent() } - public override func viewWillAppear(_ animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateNavigationBar(animated: animated) } - public override func viewDidAppear(_ animated: Bool) { + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if case .view = content { viewModel.trackEventOnScreenAppeared() } } - public override func viewDidDisappear(_ animated: Bool) { + open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if case .view = content { viewModel.trackEventOnScreenDisappeared() } } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { executeOnInit() bindings.config() layoutManager.applyLayout() super.viewDidLayoutSubviews() } + open func didChangeState(_ state: ServerDrivenState) { + updateView(state: state) + } + private func executeOnInit() { for (view, actions) in onInit { execute(actions: actions, event: "onInit", origin: view) @@ -316,11 +320,7 @@ extension BeagleControllerProtocol where Self: UIViewController { // MARK: - Observer -extension BeagleScreenViewController: BeagleScreenStateObserver { - func didChangeState(_ state: ServerDrivenState) { - updateView(state: state) - } -} +extension BeagleScreenViewController: BeagleScreenStateObserver {} extension BeagleScreenViewController { enum Content { diff --git a/Sources/Beagle/Sources/Renderer/BeagleScreenViewModel.swift b/Sources/Beagle/Sources/Renderer/BeagleScreenViewModel.swift index 44bbb82..2885f23 100644 --- a/Sources/Beagle/Sources/Renderer/BeagleScreenViewModel.swift +++ b/Sources/Beagle/Sources/Renderer/BeagleScreenViewModel.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -class BeagleScreenViewModel { +public class BeagleScreenViewModel { // MARK: Properties @@ -40,13 +40,13 @@ class BeagleScreenViewModel { // MARK: Observer - public weak var stateObserver: BeagleScreenStateObserver? { + weak var stateObserver: BeagleScreenStateObserver? { didSet { stateObserver?.didChangeState(state) } } // MARK: Init - static func remote( + internal static func remote( _ remote: ScreenType.Remote, viewClient: ViewClientProtocol, resolver: DependenciesContainerResolving, @@ -70,7 +70,7 @@ class BeagleScreenViewModel { } } - required init( + internal required init( screenType: ScreenType, resolver: DependenciesContainerResolving = GlobalConfiguration.resolver ) { @@ -81,7 +81,7 @@ class BeagleScreenViewModel { _analyticsService = OptionalInjected(resolver) } - convenience init( + internal convenience init( screenType: ScreenType, resolver: DependenciesContainerResolving = GlobalConfiguration.resolver, beagleViewStateObserver: @escaping BeagleViewStateObserver