From cfc11d23b56bec68ed201706b4dacd9747c67971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Ara=C3=BAjo?= Date: Thu, 18 Mar 2021 16:56:13 -0300 Subject: [PATCH] make unowned references weak --- .../Screens/NativeViewController.swift | 4 +- .../Beagle/Beagle.xcodeproj/project.pbxproj | 4 ++ .../Deprecated/DeprecatedAnalytics.swift | 2 +- .../Form/Form+Extensions.swift | 4 +- .../Submit/SubmitFormGestureRecognizer.swift | 2 +- .../Lazy/LazyComponent+Extensions.swift | 12 ++--- .../Lazy/Tests/LazyComponentTests.swift | 6 +-- .../ListView/ListViewController.swift | 8 +-- .../ListView/ListViewUIComponent.swift | 4 +- .../PageView/ComponentHostController.swift | 16 +++--- .../PageView/PageView+Extensions.swift | 2 +- .../PageView/PageViewUIComponent.swift | 6 +-- .../Screen/ScreenComponent+Extensions.swift | 2 +- .../Screen/Tests/ScreenComponentTests.swift | 4 +- .../TabBar/TabBar+Extensions.swift | 5 +- .../TabBar/TabBarItemUIComponent.swift | 9 ++-- .../TabBar/TabBarUIComponent.swift | 8 ++- .../Touchable/Touchable+Extensions.swift | 8 +-- .../WebView/WebViewUIComponent.swift | 2 +- .../Widget/Button/Button+Extensions.swift | 4 +- .../Widget/Image/Image+Extensions.swift | 8 +-- .../TextInput/TextInput+Extensions.swift | 2 +- .../Expression/Expression.swift | 4 +- .../Sources/Renderer/BeagleRenderer.swift | 10 ++-- .../Renderer/BindingConfigurator.swift | 18 +++++-- .../Sources/Renderer/LayoutManager.swift | 15 +++--- .../Tests/BindingConfiguratorTests.swift | 54 +++++++++++++++++++ 27 files changed, 146 insertions(+), 77 deletions(-) create mode 100644 iOS/Sources/Beagle/Sources/Renderer/Tests/BindingConfiguratorTests.swift diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/NativeViewController.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/NativeViewController.swift index 93ba48acca..38300c001a 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Screens/NativeViewController.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/NativeViewController.swift @@ -45,7 +45,9 @@ class NativeViewController: UIViewController { ) }) - private lazy var serverDrivenBeagleView = BeagleView(.init(url: .textLazyComponentEndpoint)) { state in + private lazy var serverDrivenBeagleView = BeagleView(.init(url: .textLazyComponentEndpoint)) { + [weak self] state in + guard let self = self else { return } switch state { case .started: self.loadingLabel.isHidden = false diff --git a/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj b/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj index fefbdcdcbd..a6c7149f34 100644 --- a/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj +++ b/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj @@ -206,6 +206,7 @@ A05C74DD2498162B009DBB66 /* FormValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05C74DC2498162B009DBB66 /* FormValidationTests.swift */; }; A068234C238430C500CEE57A /* ValidatorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A068234B238430C500CEE57A /* ValidatorProvider.swift */; }; A068234E2384320000CEE57A /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A068234D2384320000CEE57A /* Validator.swift */; }; + A0C4BF152600EEEC004B0422 /* BindingConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C4BF142600EEEB004B0422 /* BindingConfiguratorTests.swift */; }; A0D608D82448DF8700F7D851 /* ScreenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0D608D52448DF8600F7D851 /* ScreenType.swift */; }; A0D608DA2448DF8700F7D851 /* BeagleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0D608D72448DF8700F7D851 /* BeagleNavigationController.swift */; }; A0EB6BD725910FC8001A3E65 /* AnalyticsRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0EB6BD425910FC8001A3E65 /* AnalyticsRecord.swift */; }; @@ -573,6 +574,7 @@ A05C74DC2498162B009DBB66 /* FormValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormValidationTests.swift; sourceTree = ""; }; A068234B238430C500CEE57A /* ValidatorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorProvider.swift; sourceTree = ""; }; A068234D2384320000CEE57A /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; + A0C4BF142600EEEB004B0422 /* BindingConfiguratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingConfiguratorTests.swift; sourceTree = ""; }; A0D608D52448DF8600F7D851 /* ScreenType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenType.swift; sourceTree = ""; }; A0D608D72448DF8700F7D851 /* BeagleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeagleNavigationController.swift; sourceTree = ""; }; A0EB6BD425910FC8001A3E65 /* AnalyticsRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsRecord.swift; sourceTree = ""; }; @@ -1876,6 +1878,7 @@ 2532D5DB25099ABB001D1469 /* BeagleViewTests.swift */, 93D24A56251402B8005A5CBD /* AutoLayoutWrapperTests.swift */, 93EE0CA3252D0EE50032BE77 /* ViewConfiguratorTests.swift */, + A0C4BF142600EEEB004B0422 /* BindingConfiguratorTests.swift */, ); path = Tests; sourceTree = ""; @@ -2568,6 +2571,7 @@ C08D4E3624857CBA00AAAC0A /* URLOpenerTests.swift in Sources */, 9398CA5A255325730003A010 /* PathTests.swift in Sources */, A05C74DD2498162B009DBB66 /* FormValidationTests.swift in Sources */, + A0C4BF152600EEEC004B0422 /* BindingConfiguratorTests.swift in Sources */, E5A7763F237DA1F3002F5515 /* FormTests.swift in Sources */, C08D4E472485804D00AAAC0A /* LazyComponentTests.swift in Sources */, 93ADB87923FAD31D005B7CD2 /* ComposeComponentTests.swift in Sources */, diff --git a/iOS/Sources/Beagle/Sources/Analytics/Deprecated/DeprecatedAnalytics.swift b/iOS/Sources/Beagle/Sources/Analytics/Deprecated/DeprecatedAnalytics.swift index 3d040e8cdd..e02092da9e 100644 --- a/iOS/Sources/Beagle/Sources/Analytics/Deprecated/DeprecatedAnalytics.swift +++ b/iOS/Sources/Beagle/Sources/Analytics/Deprecated/DeprecatedAnalytics.swift @@ -27,7 +27,7 @@ final class EventsGestureRecognizer: UITapGestureRecognizer { let events: [Event] weak var controller: BeagleController? - init(events: [Event], controller: BeagleController) { + init(events: [Event], controller: BeagleController?) { self.events = events self.controller = controller super.init(target: nil, action: nil) diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Form+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Form+Extensions.swift index 4e4f065767..74ca654180 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Form+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Form+Extensions.swift @@ -34,12 +34,12 @@ extension Deprecated.Form { registerFormSubmit(view: childView) if !hasFormSubmit { - renderer.controller.dependencies.logger.log(Log.form(.submitNotFound(form: self))) + renderer.dependencies.logger.log(Log.form(.submitNotFound(form: self))) } return childView } - private func register(formView: UIView, submitView: UIView, controller: BeagleController) { + private func register(formView: UIView, submitView: UIView, controller: BeagleController?) { let gestureRecognizer = SubmitFormGestureRecognizer( form: self, formView: formView, diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Submit/SubmitFormGestureRecognizer.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Submit/SubmitFormGestureRecognizer.swift index e2c008414b..34e32b23c3 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Submit/SubmitFormGestureRecognizer.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Form/Submit/SubmitFormGestureRecognizer.swift @@ -23,7 +23,7 @@ final class SubmitFormGestureRecognizer: UITapGestureRecognizer { weak var formSubmitView: UIView? weak var controller: BeagleController? - init(form: Deprecated.Form, formView: UIView, formSubmitView: UIView, controller: BeagleController) { + init(form: Deprecated.Form, formView: UIView, formSubmitView: UIView, controller: BeagleController?) { self.form = form self.formView = formView self.formSubmitView = formSubmitView diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/LazyComponent+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/LazyComponent+Extensions.swift index 4f4acb172f..dcc26cdd1d 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/LazyComponent+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/LazyComponent+Extensions.swift @@ -25,14 +25,14 @@ extension LazyComponent { } private func lazyLoad(initialState view: UIView, renderer: BeagleRenderer) { - renderer.controller.dependencies.repository.fetchComponent(url: path, additionalData: nil, useCache: false) { + renderer.dependencies.repository.fetchComponent(url: path, additionalData: nil, useCache: false) { [weak view] result in guard let view = view else { return } switch result { case .success(let component): view.update(lazyLoaded: component, renderer: renderer) case .failure(let error): - renderer.controller.serverDrivenState = .error( + renderer.controller?.serverDrivenState = .error( .lazyLoad(error), self.retryClosure(initialState: view, renderer: renderer) ) @@ -55,7 +55,7 @@ extension UIView { ) { if let updatable = self as? OnStateUpdatable, updatable.onUpdateState(component: lazyLoaded) { - renderer.controller.setNeedsLayout(component: self) + renderer.controller?.setNeedsLayout(component: self) } else { DispatchQueue.main.async { self.replace(with: lazyLoaded, renderer: renderer) @@ -74,9 +74,9 @@ extension UIView { superview.insertSubview(newView, belowSubview: self) removeFromSuperview() - if renderer.controller.dependencies.style(self).isFlexEnabled { - renderer.controller.dependencies.style(newView).isFlexEnabled = true + if renderer.dependencies.style(self).isFlexEnabled { + renderer.dependencies.style(newView).isFlexEnabled = true } - renderer.controller.setNeedsLayout(component: newView) + renderer.controller?.setNeedsLayout(component: newView) } } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/Tests/LazyComponentTests.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/Tests/LazyComponentTests.swift index af38a3edd9..39a58f222c 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/Tests/LazyComponentTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Lazy/Tests/LazyComponentTests.swift @@ -77,9 +77,8 @@ final class LazyComponentTests: XCTestCase { initialState: ComponentDummy(resultView: initialView) ) let repository = LazyRepositoryStub() - let controller = BeagleControllerStub() + let controller = BeagleControllerStub(dependencies: BeagleScreenDependencies(repository: repository)) let renderer = BeagleRenderer(controller: controller) - controller.dependencies = BeagleScreenDependencies(repository: repository) let view = sut.toView(renderer: renderer) repository.componentCompletion?(.success(ComponentDummy())) @@ -97,9 +96,8 @@ final class LazyComponentTests: XCTestCase { initialState: ComponentDummy(resultView: initialView) ) let repository = LazyRepositoryStub() - let controller = BeagleControllerStub() + let controller = BeagleControllerStub(dependencies: BeagleScreenDependencies(repository: repository)) let renderer = BeagleRenderer(controller: controller) - controller.dependencies = BeagleScreenDependencies(repository: repository) // When let view = sut.toView(renderer: renderer) diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewController.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewController.swift index a6cb0fcddd..21331efc8c 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewController.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewController.swift @@ -78,16 +78,16 @@ final class ListViewController: UIViewController { extension ListViewController: BeagleControllerProtocol { var dependencies: BeagleDependenciesProtocol { - return renderer.controller.dependencies + return renderer.dependencies } var serverDrivenState: ServerDrivenState { - get { return renderer.controller.serverDrivenState } - set { renderer.controller.serverDrivenState = newValue } + get { return renderer.controller?.serverDrivenState ?? .finished } + set { renderer.controller?.serverDrivenState = newValue } } var screenType: ScreenType { - return renderer.controller.screenType + return renderer.controller?.screenType ?? .declarativeText("") } var screen: Screen? { diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewUIComponent.swift index 9e2f7b9cd7..68322450e7 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/ListView/ListViewUIComponent.swift @@ -100,7 +100,7 @@ final class ListViewUIComponent: UIView { collection.showsVerticalScrollIndicator = model.isScrollIndicatorVisible let parentController = listController.renderer.controller - parentController.addChild(listController) + parentController?.addChild(listController) addSubview(listController.view) listController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] listController.view.frame = bounds @@ -194,7 +194,7 @@ final class ListViewUIComponent: UIView { if items?.count == 0 || didReachScrollThreshol { onScrollEndExecuted = true - renderer.controller.execute(actions: model.onScrollEnd, event: "onScrollEnd", origin: self) + renderer.controller?.execute(actions: model.onScrollEnd, event: "onScrollEnd", origin: self) } } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/ComponentHostController.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/ComponentHostController.swift index 238ea7fa79..ed8f844c4f 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/ComponentHostController.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/ComponentHostController.swift @@ -24,29 +24,29 @@ class ComponentHostController: BeagleController { let bindings = Bindings() var dependencies: BeagleDependenciesProtocol { - return renderer.controller.dependencies + return renderer.dependencies } var serverDrivenState: ServerDrivenState { - get { renderer.controller.serverDrivenState } - set { renderer.controller.serverDrivenState = newValue } + get { renderer.controller?.serverDrivenState ?? .finished } + set { renderer.controller?.serverDrivenState = newValue } } var screenType: ScreenType { - return renderer.controller.screenType + return renderer.controller?.screenType ?? .declarativeText("") } var screen: Screen? { - return renderer.controller.screen + return renderer.controller?.screen } func addOnInit(_ onInit: [Action], in view: UIView) { - renderer.controller.addOnInit(onInit, in: view) + renderer.controller?.addOnInit(onInit, in: view) } func execute(actions: [Action]?, event: String?, origin: UIView) { - renderer.controller.execute(actions: actions, event: event, origin: origin) + renderer.controller?.execute(actions: actions, event: event, origin: origin) } func execute(actions: [Action]?, with contextId: String, and contextValue: DynamicObject, origin: UIView) { - renderer.controller.execute(actions: actions, with: contextId, and: contextValue, origin: origin) + renderer.controller?.execute(actions: actions, with: contextId, and: contextValue, origin: origin) } public func addBinding(expression: ContextExpression, in view: UIView, update: @escaping (T?) -> Void) { diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageView+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageView+Extensions.swift index 3361680dfc..78eb9481fa 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageView+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageView+Extensions.swift @@ -37,7 +37,7 @@ extension PageView { if let actions = onPageChange { view.onPageChange = { [weak view] page in guard let view = view else { return } - renderer.controller.execute(actions: actions, with: "onPageChange", and: .int(page), origin: view) + renderer.controller?.execute(actions: actions, with: "onPageChange", and: .int(page), origin: view) } } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageViewUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageViewUIComponent.swift index 8a8cf33423..13a4b49f50 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageViewUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/PageView/PageViewUIComponent.swift @@ -52,7 +52,7 @@ class PageViewUIComponent: UIView { init( model: Model, indicatorView: PageIndicatorUIView?, - controller: BeagleController + controller: BeagleController? ) { self.model = model self.indicatorView = indicatorView @@ -76,8 +76,8 @@ class PageViewUIComponent: UIView { navigationOrientation: .horizontal ) - private func setupLayout(controller: BeagleController) { - controller.addChild(pageViewController) + private func setupLayout(controller: BeagleController?) { + controller?.addChild(pageViewController) addSubview(pageViewController.view) pageViewController.didMove(toParent: controller) diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/ScreenComponent+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/ScreenComponent+Extensions.swift index e0fb56c614..c23decaa93 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/ScreenComponent+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/ScreenComponent+Extensions.swift @@ -20,7 +20,7 @@ extension ScreenComponent { public func toView(renderer: BeagleRenderer) -> UIView { - prefetch(helper: renderer.controller.dependencies.preFetchHelper) + prefetch(helper: renderer.dependencies.preFetchHelper) return buildChildView(renderer: renderer) } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/Tests/ScreenComponentTests.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/Tests/ScreenComponentTests.swift index af52fa50da..7057b2cd73 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/Tests/ScreenComponentTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Screen/Tests/ScreenComponentTests.swift @@ -134,9 +134,9 @@ final class ScreenComponentTests: XCTestCase { func test_shouldPrefetchNavigateAction() { let prefetch = BeaglePrefetchHelpingSpy() - let controller = BeagleControllerStub() + let dependencies = BeagleScreenDependencies(preFetchHelper: prefetch) + let controller = BeagleControllerStub(dependencies: dependencies) let renderer = BeagleRenderer(controller: controller) - controller.dependencies = BeagleScreenDependencies(preFetchHelper: prefetch) let navigatePath = "button-item-prefetch" let navigate = Navigate.pushView(.remote(.init(url: navigatePath, shouldPrefetch: true))) diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBar+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBar+Extensions.swift index 02b6e740fd..109ac11492 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBar+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBar+Extensions.swift @@ -29,8 +29,9 @@ extension TabBar { } } - tabBarScroll.onTabSelection = { tab in - renderer.controller.execute(actions: self.onTabSelection, with: "onTabSelection", and: .int(tab), origin: tabBarScroll) + tabBarScroll.onTabSelection = { [weak tabBarScroll] tab in + guard let view = tabBarScroll else { return } + renderer.controller?.execute(actions: self.onTabSelection, with: "onTabSelection", and: .int(tab), origin: view) } return tabBarScroll diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarItemUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarItemUIComponent.swift index aedfea7f5b..9ffa97750d 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarItemUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarItemUIComponent.swift @@ -124,12 +124,13 @@ final class TabBarItemUIComponent: UIView { private func handleContextOnImage(iconName: String) { let expression: Expression = "\(iconName)" - - renderer?.observe(expression, andUpdateManyIn: self) { icon in + + renderer?.observe(expression, andUpdateManyIn: self) { [weak self] icon in + guard let self = self else { return } if let icon = icon { self.icon.image = self.theme?.selectedIconColor == nil ? - UIImage(named: icon, in: self.renderer?.controller.dependencies.appBundle, compatibleWith: nil) : - UIImage(named: icon, in: self.renderer?.controller.dependencies.appBundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) + UIImage(named: icon, in: self.renderer?.dependencies.appBundle, compatibleWith: nil) : + UIImage(named: icon, in: self.renderer?.dependencies.appBundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) } } } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarUIComponent.swift index 4f90ffbd51..1e5d7755e9 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/TabBar/TabBarUIComponent.swift @@ -97,7 +97,7 @@ final class TabBarUIComponent: UIScrollView { } guard let selectedTabItem = tabItemViews[model.tabIndex ?? 0] else { return } setupIndicatorViewStyle(for: selectedTabItem) - model.renderer.controller.setNeedsLayout(component: self) + model.renderer.controller?.setNeedsLayout(component: self) } private func setupScrollView() { @@ -234,10 +234,8 @@ private extension TabBarUIComponent { options: .curveLinear, animations: { self.setupIndicatorViewStyle(for: tabItem) - - // TODO: setNeedLayout should call layoutIfNeeded - self.model.renderer.controller.setNeedsLayout(component: self) - self.model.renderer.controller.view.layoutIfNeeded() + self.model.renderer.controller?.setNeedsLayout(component: self) + self.model.renderer.controller?.view.layoutIfNeeded() } ) } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Touchable/Touchable+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Touchable/Touchable+Extensions.swift index f0c7da53a6..25f350dfca 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Touchable/Touchable+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/Touchable/Touchable+Extensions.swift @@ -36,11 +36,11 @@ extension Touchable { } register(events: events, inView: childView, controller: renderer.controller) - prefetchComponent(helper: renderer.controller.dependencies.preFetchHelper) + prefetchComponent(helper: renderer.dependencies.preFetchHelper) return childView } - private func register(events: [Event], inView view: UIView, controller: BeagleController) { + private func register(events: [Event], inView view: UIView, controller: BeagleController?) { let eventsGestureRecognizer = EventsGestureRecognizer( events: events, controller: controller @@ -49,10 +49,10 @@ extension Touchable { view.isUserInteractionEnabled = true } - private func prefetchComponent(helper: BeaglePrefetchHelping) { + private func prefetchComponent(helper: BeaglePrefetchHelping?) { onPress.forEach { action in guard let newPath = (action as? Navigate)?.newPath else { return } - helper.prefetchComponent(newPath: newPath) + helper?.prefetchComponent(newPath: newPath) } } } diff --git a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/WebView/WebViewUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/WebView/WebViewUIComponent.swift index 195c1b1a9b..5982146113 100644 --- a/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/WebView/WebViewUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/ServerDrivenComponent/WebView/WebViewUIComponent.swift @@ -39,7 +39,7 @@ final class WebViewUIComponent: UIView { didSet { updateView() } } - init(url: String? = nil, controller: BeagleController) { + init(url: String? = nil, controller: BeagleController?) { self.url = url self.controller = controller super.init(frame: .zero) diff --git a/iOS/Sources/Beagle/Sources/Components/Widget/Button/Button+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/Widget/Button/Button+Extensions.swift index 2aa6778272..2d441a35ad 100644 --- a/iOS/Sources/Beagle/Sources/Components/Widget/Button/Button+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/Widget/Button/Button+Extensions.swift @@ -36,7 +36,7 @@ extension Button { } } - let preFetchHelper = renderer.controller.dependencies.preFetchHelper + let preFetchHelper = renderer.dependencies.preFetchHelper onPress? .compactMap { ($0 as? Navigate)?.newPath } .forEach { preFetchHelper.prefetchComponent(newPath: $0) } @@ -61,7 +61,7 @@ extension Button { required init( onPress: [Action]?, clickAnalyticsEvent: AnalyticsClick? = nil, - controller: BeagleController + controller: BeagleController? ) { super.init(frame: .zero) self.onPress = onPress diff --git a/iOS/Sources/Beagle/Sources/Components/Widget/Image/Image+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/Widget/Image/Image+Extensions.swift index cb032bbcc3..98a9db0519 100644 --- a/iOS/Sources/Beagle/Sources/Components/Widget/Image/Image+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/Widget/Image/Image+Extensions.swift @@ -37,7 +37,7 @@ extension Image { let expression: Expression = "\(mobileId)" renderer.observe(expression, andUpdateManyIn: image) { mobileId in guard let mobileId = mobileId, !mobileId.isEmpty else { return } - self.setImageFromAsset(named: mobileId, bundle: renderer.controller.dependencies.appBundle, imageView: image) + self.setImageFromAsset(named: mobileId, bundle: renderer.dependencies.appBundle, imageView: image) } case .remote(let remote): let expression: Expression = "\(remote.url)" @@ -54,7 +54,7 @@ extension Image { image.token?.cancel() switch path { case .local(let mobileId): - self.setImageFromAsset(named: mobileId, bundle: renderer.controller.dependencies.appBundle, imageView: image) + self.setImageFromAsset(named: mobileId, bundle: renderer.dependencies.appBundle, imageView: image) case .remote(let remote): image.token = self.setRemoteImage(from: remote.url, placeholder: remote.placeholder, imageView: image, renderer: renderer) case .none: () @@ -69,7 +69,7 @@ extension Image { private func setRemoteImage(from url: String, placeholder: String?, imageView: UIImageView, renderer: BeagleRenderer) -> RequestToken? { var imagePlaceholder: UIImage? if let placeholder = placeholder { - imagePlaceholder = UIImage(named: placeholder, in: renderer.controller.dependencies.appBundle, compatibleWith: nil) + imagePlaceholder = UIImage(named: placeholder, in: renderer.dependencies.appBundle, compatibleWith: nil) imageView.image = imagePlaceholder } return lazyLoadImage(path: url, placeholderImage: imagePlaceholder, imageView: imageView, renderer: renderer) @@ -77,7 +77,7 @@ extension Image { private func lazyLoadImage(path: String, placeholderImage: UIImage?, imageView: UIImageView, renderer: BeagleRenderer) -> RequestToken? { let controller = renderer.controller - return controller.dependencies.imageDownloader.fetchImage(url: path, additionalData: nil) { + return renderer.dependencies.imageDownloader.fetchImage(url: path, additionalData: nil) { [weak imageView, weak controller] result in guard let imageView = imageView else { return } switch result { diff --git a/iOS/Sources/Beagle/Sources/Components/Widget/TextInput/TextInput+Extensions.swift b/iOS/Sources/Beagle/Sources/Components/Widget/TextInput/TextInput+Extensions.swift index 9931555f62..5543dacf03 100644 --- a/iOS/Sources/Beagle/Sources/Components/Widget/TextInput/TextInput+Extensions.swift +++ b/iOS/Sources/Beagle/Sources/Components/Widget/TextInput/TextInput+Extensions.swift @@ -115,7 +115,7 @@ extension TextInput: ServerDrivenComponent { onChange: [Action]? = nil, onBlur: [Action]? = nil, onFocus: [Action]? = nil, - controller: BeagleController + controller: BeagleController? ) { self.onChange = onChange self.onBlur = onBlur diff --git a/iOS/Sources/Beagle/Sources/ContextExpression/Expression/Expression.swift b/iOS/Sources/Beagle/Sources/ContextExpression/Expression/Expression.swift index 7711a7cb63..2e42809d7b 100644 --- a/iOS/Sources/Beagle/Sources/ContextExpression/Expression/Expression.swift +++ b/iOS/Sources/Beagle/Sources/ContextExpression/Expression/Expression.swift @@ -50,12 +50,12 @@ public struct MultipleExpression: Hashable { public extension Expression { func observe( view: UIView, - controller: BeagleControllerProtocol, + controller: BeagleControllerProtocol?, updateFunction: @escaping (T?) -> Void ) { switch self { case let .expression(expression): - controller.addBinding(expression: expression, in: view, update: updateFunction) + controller?.addBinding(expression: expression, in: view, update: updateFunction) case let .value(value): updateFunction(value) } diff --git a/iOS/Sources/Beagle/Sources/Renderer/BeagleRenderer.swift b/iOS/Sources/Beagle/Sources/Renderer/BeagleRenderer.swift index aaf9f46d52..0c72ee0a1e 100644 --- a/iOS/Sources/Beagle/Sources/Renderer/BeagleRenderer.swift +++ b/iOS/Sources/Beagle/Sources/Renderer/BeagleRenderer.swift @@ -23,10 +23,12 @@ public protocol DependencyRenderer { /// Use this class whenever you want to transform a Component into a UIView public struct BeagleRenderer { - public unowned var controller: BeagleController + public let dependencies: BeagleDependenciesProtocol + public private(set) weak var controller: BeagleController? internal init(controller: BeagleController) { self.controller = controller + self.dependencies = controller.dependencies } /// main function of this class. Call it to transform a Component into a UIView @@ -46,13 +48,13 @@ public struct BeagleRenderer { view.beagle.setupView(of: component) if let id = (component as? IdentifiableComponent)?.id { - controller.setIdentifier(id, in: view) + controller?.setIdentifier(id, in: view) } if let context = (component as? HasContext)?.context { - controller.setContext(context, in: view) + controller?.setContext(context, in: view) } if let onInit = (component as? InitiableComponent)?.onInit { - controller.addOnInit(onInit, in: view) + controller?.addOnInit(onInit, in: view) } if let style = (component as? StyleComponent)?.style { diff --git a/iOS/Sources/Beagle/Sources/Renderer/BindingConfigurator.swift b/iOS/Sources/Beagle/Sources/Renderer/BindingConfigurator.swift index a655b030ce..f57115cb2b 100644 --- a/iOS/Sources/Beagle/Sources/Renderer/BindingConfigurator.swift +++ b/iOS/Sources/Beagle/Sources/Renderer/BindingConfigurator.swift @@ -25,17 +25,25 @@ class Bindings { } } - func add(_ controller: UIViewController, _ expression: ContextExpression, _ view: UIView, _ update: @escaping (T?) -> Void) { - bindings.append { [weak self, weak view] in - guard let self = self else { return } + func add( + _ controller: UIViewController, + _ expression: ContextExpression, + _ view: UIView, + _ update: @escaping (T?) -> Void + ) { + bindings.append { [weak view, weak controller] in view?.configBinding( for: expression, - completion: self.bindBlock(controller, view, update) + completion: Self.bindBlock(controller, view, update) ) } } - private func bindBlock(_ controller: UIViewController, _ view: UIView?, _ update: @escaping (T?) -> Void) -> (T?) -> Void { + private static func bindBlock( + _ controller: UIViewController?, + _ view: UIView?, + _ update: @escaping (T?) -> Void + ) -> (T?) -> Void { return { [weak view, weak controller] value in update(value) view?.yoga.markDirty() diff --git a/iOS/Sources/Beagle/Sources/Renderer/LayoutManager.swift b/iOS/Sources/Beagle/Sources/Renderer/LayoutManager.swift index a8b11df2c5..c1ca0835ac 100644 --- a/iOS/Sources/Beagle/Sources/Renderer/LayoutManager.swift +++ b/iOS/Sources/Beagle/Sources/Renderer/LayoutManager.swift @@ -18,15 +18,15 @@ import UIKit final class LayoutManager { - private unowned var viewController: BeagleScreenViewController + private weak var viewController: BeagleScreenViewController? private var safeArea: SafeArea? { - return viewController.screen?.safeArea + return viewController?.screen?.safeArea } private var keyboardFrame = CGRect.zero private var keyboardHeight: CGFloat { - guard let view = viewController.viewIfLoaded else { return 0 } + guard let view = viewController?.viewIfLoaded else { return 0 } let viewFrame = view.convert(view.bounds, to: nil) let keyboardRect = keyboardFrame.intersection(viewFrame) return keyboardRect.isNull ? 0 : keyboardRect.height @@ -42,7 +42,8 @@ final class LayoutManager { } public func applyLayout() { - guard case .view(let view) = viewController.content else { return } + guard let viewController = viewController, + case .view(let view) = viewController.content else { return } let style = Style(padding: contentPadding) view.frame = viewController.view.bounds view.style.setup(style) @@ -73,12 +74,12 @@ final class LayoutManager { private var contentInsets: UIEdgeInsets { if #available(iOS 11.0, *) { - return viewController.viewIfLoaded?.safeAreaInsets ?? .zero + return viewController?.viewIfLoaded?.safeAreaInsets ?? .zero } return UIEdgeInsets( - top: viewController.topLayoutGuide.length, + top: viewController?.topLayoutGuide.length ?? 0, left: 0, - bottom: viewController.bottomLayoutGuide.length, + bottom: viewController?.bottomLayoutGuide.length ?? 0, right: 0 ) } diff --git a/iOS/Sources/Beagle/Sources/Renderer/Tests/BindingConfiguratorTests.swift b/iOS/Sources/Beagle/Sources/Renderer/Tests/BindingConfiguratorTests.swift new file mode 100644 index 0000000000..6d71417cec --- /dev/null +++ b/iOS/Sources/Beagle/Sources/Renderer/Tests/BindingConfiguratorTests.swift @@ -0,0 +1,54 @@ +/* + * 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 +@testable import Beagle + +class BindingConfiguratorTests: XCTestCase { + + func testBindingParticipatsShouldNotBeRetained() throws { + // Given + let sut = Bindings() + weak var view: UIView? + weak var controller: UIViewController? + + // When + (view, controller) = addBinding(to: sut, config: false) + // Then + XCTAssertNil(view) + XCTAssertNil(controller) + + // When + (view, controller) = addBinding(to: sut, config: true) + // Then + XCTAssertNil(view) + XCTAssertNil(controller) + } + + private func addBinding(to bindings: Bindings, config: Bool) -> (UIView, UIViewController) { + let expression = ContextExpression.single(.value(.literal(.string("text")))) + let controller = UIViewController() + let view = UILabel() + bindings.add(controller, expression, view) { [weak view] in + view?.text = $0 + } + if config { + bindings.config() + } + return (view, controller) + } + +}