diff --git a/iOS/Example/BeagleDemo/BeagleDemo.xcodeproj/project.pbxproj b/iOS/Example/BeagleDemo/BeagleDemo.xcodeproj/project.pbxproj index 318526c83f..f1d1890209 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo.xcodeproj/project.pbxproj +++ b/iOS/Example/BeagleDemo/BeagleDemo.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 6E4EE6C1240E970000624DFB /* ScreenDeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4EE6C0240E970000624DFB /* ScreenDeepLink.swift */; }; 6E4EE6C7240E9C0B00624DFB /* ViewLayoutHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4EE6C6240E9C0B00624DFB /* ViewLayoutHelper.swift */; }; 6E8E1CC1249C57300053AD58 /* SimpleForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8E1CC0249C57300053AD58 /* SimpleForm.swift */; }; + 6EA428722514457F00AEE741 /* ImageScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA428712514457F00AEE741 /* ImageScreen.swift */; }; 6ED5B09E240464520070CB9D /* BeagleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED5B09D240464520070CB9D /* BeagleStyle.swift */; }; 7685B043249A879F007EDED0 /* SendRequestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7685B042249A879F007EDED0 /* SendRequestScreen.swift */; }; 932925B22327E0E400A61F01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932925B12327E0E400A61F01 /* AppDelegate.swift */; }; @@ -27,7 +28,7 @@ 93D24A742514F9DA005A5CBD /* BeagleViewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D24A732514F9DA005A5CBD /* BeagleViewScreen.swift */; }; 93D8DC0D248965D10070CB41 /* ComponentInteractionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D8DC0C248965D10070CB41 /* ComponentInteractionScreen.swift */; }; 93EAFA4C24083C7B00B73522 /* ListViewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EAFA4B24083C7B00B73522 /* ListViewScreen.swift */; }; - 98A7F36C23A926C600E88C70 /* TabViewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7F36B23A926C600E88C70 /* TabViewScreen.swift */; }; + 98A7F36C23A926C600E88C70 /* TabBarScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7F36B23A926C600E88C70 /* TabBarScreen.swift */; }; 98E08D2324228DD200FBD21B /* WebViewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E08D2224228DD200FBD21B /* WebViewScreen.swift */; }; 98F3D41124ED770F006DAB05 /* NavigateTypeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3D41024ED770F006DAB05 /* NavigateTypeScreen.swift */; }; A038C8FD2407073D001164B1 /* FormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A038C8FC2407073D001164B1 /* FormScreen.swift */; }; @@ -104,6 +105,7 @@ 6E4EE6C0240E970000624DFB /* ScreenDeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenDeepLink.swift; sourceTree = ""; }; 6E4EE6C6240E9C0B00624DFB /* ViewLayoutHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewLayoutHelper.swift; sourceTree = ""; }; 6E8E1CC0249C57300053AD58 /* SimpleForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleForm.swift; sourceTree = ""; }; + 6EA428712514457F00AEE741 /* ImageScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScreen.swift; sourceTree = ""; }; 6ED5B09D240464520070CB9D /* BeagleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeagleStyle.swift; sourceTree = ""; }; 7685B042249A879F007EDED0 /* SendRequestScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendRequestScreen.swift; sourceTree = ""; }; 932925AE2327E0E400A61F01 /* BeagleDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BeagleDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -117,7 +119,7 @@ 93D24A732514F9DA005A5CBD /* BeagleViewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeagleViewScreen.swift; sourceTree = ""; }; 93D8DC0C248965D10070CB41 /* ComponentInteractionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentInteractionScreen.swift; sourceTree = ""; }; 93EAFA4B24083C7B00B73522 /* ListViewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewScreen.swift; sourceTree = ""; }; - 98A7F36B23A926C600E88C70 /* TabViewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewScreen.swift; sourceTree = ""; }; + 98A7F36B23A926C600E88C70 /* TabBarScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScreen.swift; sourceTree = ""; }; 98E08D2224228DD200FBD21B /* WebViewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewScreen.swift; sourceTree = ""; }; 98F3D41024ED770F006DAB05 /* NavigateTypeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateTypeScreen.swift; sourceTree = ""; }; A038C8FC2407073D001164B1 /* FormScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormScreen.swift; sourceTree = ""; }; @@ -262,9 +264,10 @@ 6E4EE6C0240E970000624DFB /* ScreenDeepLink.swift */, 7685B042249A879F007EDED0 /* SendRequestScreen.swift */, 6E8E1CC0249C57300053AD58 /* SimpleForm.swift */, - 98A7F36B23A926C600E88C70 /* TabViewScreen.swift */, + 98A7F36B23A926C600E88C70 /* TabBarScreen.swift */, 6E4EE6C6240E9C0B00624DFB /* ViewLayoutHelper.swift */, 98E08D2224228DD200FBD21B /* WebViewScreen.swift */, + 6EA428712514457F00AEE741 /* ImageScreen.swift */, 4F36AB2D251E7A5A0036F839 /* GlobalContextScreen.swift */, ); path = Screens; @@ -524,11 +527,12 @@ A0642DCB23968A020028AFF3 /* MainScreen.swift in Sources */, C80AF1342493F0950097D200 /* OtherComponent.swift in Sources */, C09F4100249AAB0F002605EE /* TextContainerWithAction.swift in Sources */, + 6EA428722514457F00AEE741 /* ImageScreen.swift in Sources */, 2576F469250ACF6C000CDB2A /* NativeViewController.swift in Sources */, C8A2F6F42464C1F9002198B1 /* AutoDecodable.generated.swift in Sources */, 93EAFA4C24083C7B00B73522 /* ListViewScreen.swift in Sources */, 932925B22327E0E400A61F01 /* AppDelegate.swift in Sources */, - 98A7F36C23A926C600E88C70 /* TabViewScreen.swift in Sources */, + 98A7F36C23A926C600E88C70 /* TabBarScreen.swift in Sources */, A057D59824C9FE1D001F6835 /* ErrorView.swift in Sources */, 98E08D2324228DD200FBD21B /* WebViewScreen.swift in Sources */, A0642DC82395A1840028AFF3 /* DeeplinkScreenManager.swift in Sources */, diff --git a/iOS/Example/BeagleDemo/BeagleDemo/AppDelegate.swift b/iOS/Example/BeagleDemo/BeagleDemo/AppDelegate.swift index 7a4f334e48..30eed49707 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/AppDelegate.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/AppDelegate.swift @@ -27,7 +27,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let deepLinkHandler = DeeplinkScreenManager.shared deepLinkHandler[.lazyComponentEndpoint] = LazyComponentScreen.self deepLinkHandler[.pageViewEndpoint] = PageViewScreen.self - deepLinkHandler[.tabViewEndpoint] = TabViewScreen.self + deepLinkHandler[.tabBarEndpoint] = TabBarScreen.self deepLinkHandler[.formEndpoint] = FormScreen.self deepLinkHandler[.customComponentEndpoint] = CustomComponentScreen.self deepLinkHandler[.screenDeeplinkEndpoint] = ScreenDeepLink.self @@ -38,6 +38,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { deepLinkHandler[.simpleFormEndpoint] = SimpleFormScreen.self deepLinkHandler[.navigateStep1Endpoint] = NavigateStep1Screen.self deepLinkHandler[.navigateStep2Endpoint] = NavigateStep2Screen.self + deepLinkHandler[.imageEndpoint] = ImageScreen.self deepLinkHandler[.globalContextEndpoint] = GlobalContexScreen.self deepLinkHandler[.beagleView] = BeagleViewScreen.self diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/Contents.json b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/Contents.json index 999a25bdf0..b438c8fc57 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/Contents.json +++ b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/Contents.json @@ -1,21 +1,23 @@ { "images" : [ { - "idiom" : "universal", "filename" : "beagle.png", + "idiom" : "universal", "scale" : "1x" }, { + "filename" : "beagle@2x.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "beagle@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@2x.png b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@2x.png new file mode 100644 index 0000000000..01ae23c255 Binary files /dev/null and b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@2x.png differ diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@3x.png b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@3x.png new file mode 100644 index 0000000000..892b25ae59 Binary files /dev/null and b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/beagle.imageset/beagle@3x.png differ diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/Contents.json b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/Contents.json new file mode 100644 index 0000000000..b6e51d570c --- /dev/null +++ b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "buracoNegro.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/buracoNegro.jpg b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/buracoNegro.jpg new file mode 100644 index 0000000000..6b408d3167 Binary files /dev/null and b/iOS/Example/BeagleDemo/BeagleDemo/Assets.xcassets/blackHole.imageset/buracoNegro.jpg differ diff --git a/iOS/Example/BeagleDemo/BeagleDemo/BeagleStyle.swift b/iOS/Example/BeagleDemo/BeagleDemo/BeagleStyle.swift index 99a351cf67..613e74dcf7 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/BeagleStyle.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/BeagleStyle.swift @@ -97,7 +97,7 @@ struct AppTheme { } static func tabView() -> (UIView?) -> Void { - return BeagleStyle.tabBar(backgroundColor: .clear, indicatorColor: .demoGray, selectedTextColor: .demoGray, unselectedTextColor: .demoDarkGray, selectedIconColor: .demoGray, unselectedIconColor: .demoDarkGray) + return BeagleStyle.tabBar(backgroundColor: .clear, indicatorColor: .demoGray, selectedTextColor: .demoGray, unselectedTextColor: .demoDarkGray) } static func textInput() -> (UITextField?) -> Void { diff --git a/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/AutoDecodable.generated.swift b/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/AutoDecodable.generated.swift index 0aa1eb279b..fda65c457a 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/AutoDecodable.generated.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/AutoDecodable.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/Equality.generated.swift b/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/Equality.generated.swift index 3b549669b5..aeec380173 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/Equality.generated.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/CodeGeneration/Generated/Equality.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Constants/Constants.swift b/iOS/Example/BeagleDemo/BeagleDemo/Constants/Constants.swift index 8c4cc00c67..ce4dbbb42a 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Constants/Constants.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/Constants/Constants.swift @@ -26,7 +26,7 @@ extension String { // MARK: - Endpoint static let lazyComponentEndpoint = "lazycomponent" static let pageViewEndpoint = "pageview" - static let tabViewEndpoint = "tabview" + static let tabBarEndpoint = "tabbar" static let listViewEndpoint = "listview" static let formEndpoint = "form" static let customComponentEndpoint = "customComponent" @@ -42,6 +42,7 @@ extension String { static let componentInterationEndpoint = "componentInteractionText" static let conditionActionEndpoint = "conditionActionText" static let simpleFormEndpoint = "simpleFormComponent" + static let imageEndpoint = "image" // MARK: - URL static let webViewURL = "https://maps.google.com/" diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/ImageScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/ImageScreen.swift new file mode 100644 index 0000000000..6861a4c7ed --- /dev/null +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/ImageScreen.swift @@ -0,0 +1,118 @@ +/* + * 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 UIKit +import Beagle +import BeagleSchema + +struct ImageScreen: DeeplinkScreen { + + init(path: String, data: [String: String]?) { + // Intentionally unimplemented... + } + + func screenController() -> UIViewController { + return Beagle.screen(.declarative(screen)) + } + + var screen: Screen { + return Screen( + navigationBar: NavigationBar(title: "Image"), + child: container() + ) + } + + private let sizeImage = WidgetProperties( + style: Style( + size: Size().height(100).width(100), + flex: Flex().alignSelf(.center) + ) + ) + + private func container() -> Container { + return Container(context: Context( + id: "img", + value: [ + "remote": .string(.networkImageBeagle), + "local": "imageBeagle", + "pathLocal": ["_beagleImagePath_": "local", "mobileId": "imageBeagle"], + "pathRemote": ["_beagleImagePath_": "remote", "url": .string(.networkImageBeagle)] + ] + )) { + ScrollView { + createText(text: "Image url with context!") + Image( + .remote(Image.Remote(url: "@{img.remote}")), + widgetProperties: sizeImage + ) + + createText(text: "Image mobileId with context!") + Image( + .local("@{img.local}"), + widgetProperties: sizeImage + ) + createText(text: "Image with type remote path with context!") + Image( + "@{img.pathRemote}", + widgetProperties: sizeImage + ) + createText(text: "Image with type local path with context!") + Image( + "@{img.pathLocal}", + widgetProperties: sizeImage + ) + createText(text: "Image url without context!") + Image( + .remote(.init(url: .networkImageBeagle)), + widgetProperties: sizeImage + ) + + createText(text: "Image mobileId without context!") + Image( + .local("beagle"), + widgetProperties: sizeImage + ) + + Button( + text: "Chage Context", + onPress: [ + SetContext( + contextId: "img", + value: [ + "remote": "https://cdn.eso.org/images/screen/eso1907a.jpg", + "local": "beagle", + "pathLocal": ["_beagleImagePath_": "local", "mobileId": "beagle"], + "pathRemote": ["_beagleImagePath_": "remote", "url": "https://cdn.eso.org/images/screen/eso1907a.jpg"] + ] + ) + ] + ) + } + } + } + + private func createText(text: String) -> Text { + return Text( + .value(text), + widgetProperties: WidgetProperties( + style: Style( + margin: EdgeValue().vertical(10), + flex: Flex().alignSelf(.center) + ) + ) + ) + } +} diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/ListViewScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/ListViewScreen.swift index fb28f63c6d..ac0883afe9 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Screens/ListViewScreen.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/ListViewScreen.swift @@ -29,14 +29,24 @@ struct ListViewScreen: DeeplinkScreen { var screen: Screen { return Screen( - navigationBar: NavigationBar(title: "ListView"), - child: listView + navigationBar: NavigationBar( + title: "ListView", + navigationBarItems: [ + NavigationBarItem( + image: "@{img}", + text: "", + action: SetContext(contextId: "img", value: "imageBeagle") + ) + ] + ), + child: listView, + context: Context(id: "img", value: "informationImage") ) } var listView = ListView(direction: .horizontal) { - Touchable(onPress: [Navigate.pushView(.remote(.init(url: .value(.textLazyComponentEndpoint))))]) { - Text("0000") + Touchable(onPress: [Navigate.pushView(.remote(.init(url: .textLazyComponentEndpoint)))]) { + Text("0000") } Text("0001", widgetProperties: .init(style: Style(size: Size().width(100).height(100)))) Text("0002") @@ -55,11 +65,11 @@ struct ListViewScreen: DeeplinkScreen { Text("0011") Text("0012") Text("0013") - Image(.value(.local("beagle"))) + Image(.local("beagle")) Text("0014") Text("0015") Text("0016") - Image(.value(.remote(.init(url: .networkImageBeagle)))) + Image(.remote(.init(url: .networkImageBeagle))) Text("0017") Text("0018") Text("0019") diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/MainScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/MainScreen.swift index bd2ce17666..ee6c11af8a 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Screens/MainScreen.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/MainScreen.swift @@ -51,13 +51,17 @@ struct MainScreen: DeeplinkScreen { onPress: [Navigate.openNativeRoute(.init(route: .pageViewEndpoint))] ) Button( - text: "Tab View", - onPress: [Navigate.openNativeRoute(.init(route: .tabViewEndpoint))] + text: "Tab Bar", + onPress: [Navigate.openNativeRoute(.init(route: .tabBarEndpoint))] ) Button( text: "List View", onPress: [Navigate.openNativeRoute(.init(route: .listViewEndpoint))] ) + Button( + text: "Image", + onPress: [Navigate.openNativeRoute(.init(route: .imageEndpoint))] + ) Button( text: "Form", onPress: [Navigate.openNativeRoute(.init(route: .formEndpoint))] @@ -104,7 +108,7 @@ struct MainScreen: DeeplinkScreen { ) Button( text: "Sample BFF", - onPress: [Navigate.pushView(.remote(.init(url: .value(.componentsEndpoint))))] + onPress: [Navigate.pushView(.remote(.init(url: .componentsEndpoint)))] ) } } @@ -114,4 +118,5 @@ struct MainScreen: DeeplinkScreen { title: "Beagle Demo" ) } + } diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/PageViewScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/PageViewScreen.swift index 8e931d8f0c..26911969bc 100644 --- a/iOS/Example/BeagleDemo/BeagleDemo/Screens/PageViewScreen.swift +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/PageViewScreen.swift @@ -47,7 +47,7 @@ struct Page { Text("Text with alignment attribute set to center", alignment: Expression.value(.center)) Text("Text with alignment attribute set to right", alignment: Expression.value(.right)) Text("Text with alignment attribute set to left", alignment: Expression.value(.left)) - Image(.value(.remote(.init(url: .networkImageBeagle)))) + Image(.remote(.init(url: .networkImageBeagle))) } } } diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabBarScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabBarScreen.swift new file mode 100644 index 0000000000..73c9466ec0 --- /dev/null +++ b/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabBarScreen.swift @@ -0,0 +1,146 @@ +/* + * 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 UIKit +import Beagle +import BeagleSchema + +struct TabBarScreen: DeeplinkScreen { + init(path: String, data: [String: String]?) { + } + + func screenController() -> UIViewController { + return Beagle.screen(.declarative(screen)) + } + + var screen = Screen(navigationBar: NavigationBar(title: "TabBar")) { + Container(context: + Context(id: "tab", + value: [ + "currentTab": 0, + "icon": [ + "tab1": "beagle", + "tab4": "imageBeagle", + "tab5": "informationImage", + "tab8": "blackHole" + ] + ] + ), widgetProperties: .init(Flex().grow(1))) { + TabBar( + items: [ + TabBarItem(icon: "@{tab.icon.tab1}"), + TabBarItem(title: "Tab 2"), + TabBarItem(title: "Tab 3"), + TabBarItem(icon: "@{tab.icon.tab4}", title: "Tab 4"), + TabBarItem(icon: "@{tab.icon.tab5}"), + TabBarItem(title: "Tab 6"), + TabBarItem(title: "Tab 7"), + TabBarItem(icon: "@{tab.icon.tab8}", title: "Tab 8") + ], + styleId: .tabViewStyle, + currentTab: "@{tab.currentTab}", + onTabSelection: [SetContext(contextId: "tab", path: "currentTab", value: "@{onTabSelection}")] + ) + PageView( + onPageChange: [SetContext(contextId: "tab", path: "currentTab", value: "@{onPageChange}")], + currentPage: "@{tab.currentTab}" + ) { + Container(widgetProperties: .init(Flex().alignContent(.center))) { + Text("Text1 Tab 1") + Image(.remote(.init(url: .networkImageBeagle, placeholder: "imageBeagle"))) + Text("Text2 Tab 1") + Button(text: "change Context Tab 1", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "beagle", + "tab4": "imageBeagle", + "tab5": "informationImage", + "tab8": "blackHole" + ]) + ]) + } + Container(widgetProperties: .init(Flex().justifyContent(.center).alignItems(.center))) { + Text("Text1 Tab 2") + Text("Text2 Tab 2") + Button(text: "change Context Tab 2", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "blackHole", + "tab4": "informationImage", + "tab5": "imageBeagle", + "tab8": "beagle" + ]) + ]) + } + Container(widgetProperties: .init(Flex().justifyContent(.flexStart))) { + Text("Text1 Tab 3") + Text("Text2 Tab 3") + } + Container(widgetProperties: .init(Flex().alignItems(.center))) { + Text("Text1 Tab 4") + Text("Text2 Tab 4") + Button(text: "change Context Tab 4", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "informationImage", + "tab4": "blackHole", + "tab5": "beagle", + "tab8": "imageBeagle" + ]) + ]) + } + Container(widgetProperties: .init(Flex().alignContent(.center))) { + Text("Text1 Tab 5") + Image(.remote(.init(url: .networkImageBeagle, placeholder: "imageBeagle"))) + Text("Text2 Tab 5") + Button(text: "change Context Tab 5", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "beagle", + "tab4": "informationImage", + "tab5": "beagle", + "tab8": "blackHole" + ]) + ]) + } + Container(widgetProperties: .init(Flex().justifyContent(.center).alignItems(.center))) { + Text("Text1 Tab 6") + Text("Text2 Tab 6") + Button(text: "change Context Tab 6", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "beagle", + "tab4": "informationImage", + "tab5": "beagle", + "tab8": "blackHole" + ]) + ]) + } + Container(widgetProperties: .init(Flex().justifyContent(.flexStart))) { + Text("Text1 Tab 7") + Text("Text2 Tab 7") + } + Container(widgetProperties: .init(Flex().alignItems(.center))) { + Text("Text1 Tab 8") + Text("Text2 Tab 8") + Button(text: "change Context tab 8", onPress: [ + SetContext(contextId: "tab", path: "icon", value: [ + "tab1": "imageBeagle", + "tab4": "beagle", + "tab5": "blackHole", + "tab8": "informationImage" + ]) + ]) + } + } + } + } +} diff --git a/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabViewScreen.swift b/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabViewScreen.swift deleted file mode 100644 index 04f436b00b..0000000000 --- a/iOS/Example/BeagleDemo/BeagleDemo/Screens/TabViewScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 UIKit -import Beagle -import BeagleSchema - -struct TabViewScreen: DeeplinkScreen { - init(path: String, data: [String: String]?) { - } - - func screenController() -> UIViewController { - return Beagle.screen(.declarative(screen)) - } - - var screen = Screen(navigationBar: NavigationBar(title: "TabView")) { - Container(context: Context(id: "currentTab", value: 0), widgetProperties: .init(Flex().grow(1))) { - TabBar( - items: [ - TabBarItem(icon: "beagle"), - TabBarItem(title: "Tab 1"), - TabBarItem(title: "Tab 2"), - TabBarItem(icon: "beagle", title: "Tab 3") - ], - currentTab: "@{currentTab}", - onTabSelection: [SetContext(contextId: "currentTab", value: "@{onTabSelection}")] - ) - PageView( - onPageChange: [SetContext(contextId: "currentTab", value: "@{onPageChange}")], - currentPage: "@{currentTab}" - ) { - Container(widgetProperties: .init(Flex().alignContent(.center))) { - Text("Text1 Tab 0") - Image(.value(.remote(.init(url: .networkImageBeagle, placeholder: "imageBeagle")))) - Text("Text2 Tab 0") - } - Container(widgetProperties: .init(Flex().justifyContent(.center).alignItems(.center))) { - Text("Text1 Tab 1") - Text("Text2 Tab 1") - } - Container(widgetProperties: .init(Flex().justifyContent(.flexStart))) { - Text("Text1 Tab 2") - Text("Text2 Tab 2") - } - Container(widgetProperties: .init(Flex().alignItems(.center))) { - Text("Text1 Tab 3") - Text("Text2 Tab 3") - } - } - } - } -} diff --git a/iOS/Schema/Sources/Action/Types/Navigate/Navigate.swift b/iOS/Schema/Sources/Action/Types/Navigate/Navigate.swift index 88a970815b..7a54e68753 100644 --- a/iOS/Schema/Sources/Action/Types/Navigate/Navigate.swift +++ b/iOS/Schema/Sources/Action/Types/Navigate/Navigate.swift @@ -96,14 +96,13 @@ extension Route { /// A screen that should be rendered in case of request fail. public let fallback: Screen? - public init(url: Expression, shouldPrefetch: Bool = false, fallback: Screen? = nil) { - self.url = url - self.shouldPrefetch = shouldPrefetch - self.fallback = fallback - } - - @available(*, deprecated, message: "It was deprecated in version 1.2.2 and will be removed in a future version. Please use constructor with bind") - public init(url: String, shouldPrefetch: Bool = false, fallback: Screen? = nil) { + /// Constructs a new path to a remote screen. + /// + /// - Parameters: + /// - url: Contains the navigation endpoint. Since its a _ExpressibleString_ type you can pass a Expression or a regular String. + /// - shouldPrefetch: Changes _when_ this screen is requested. + /// - fallback: A screen that should be rendered in case of request fail. + public init(url: StringOrExpression, shouldPrefetch: Bool = false, fallback: Screen? = nil) { self.url = "\(url)" self.shouldPrefetch = shouldPrefetch self.fallback = fallback diff --git a/iOS/Schema/Sources/CodeGeneration/Generated/AutoDecodable.generated.swift b/iOS/Schema/Sources/CodeGeneration/Generated/AutoDecodable.generated.swift index 50d40a1500..e3846da410 100644 --- a/iOS/Schema/Sources/CodeGeneration/Generated/AutoDecodable.generated.swift +++ b/iOS/Schema/Sources/CodeGeneration/Generated/AutoDecodable.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Schema/Sources/CodeGeneration/Generated/Equality.generated.swift b/iOS/Schema/Sources/CodeGeneration/Generated/Equality.generated.swift index 1f052aed34..3b0aac87ac 100644 --- a/iOS/Schema/Sources/CodeGeneration/Generated/Equality.generated.swift +++ b/iOS/Schema/Sources/CodeGeneration/Generated/Equality.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Schema/Sources/ContextExpression/Expression.swift b/iOS/Schema/Sources/ContextExpression/Expression.swift index ec8b55f88e..66fc54dc6a 100644 --- a/iOS/Schema/Sources/ContextExpression/Expression.swift +++ b/iOS/Schema/Sources/ContextExpression/Expression.swift @@ -14,6 +14,9 @@ * limitations under the License. */ +/// It's a `String` that will be treated internally as an `Expression` if passed a value like "@{someExression}". Otherwise, it will be just a normal `String`. +public typealias StringOrExpression = String + public enum Expression { case value(T) case expression(ContextExpression) diff --git a/iOS/Schema/Sources/ServerDrivenComponent/Screen/NavigationBar.swift b/iOS/Schema/Sources/ServerDrivenComponent/Screen/NavigationBar.swift index 8537191347..e6365479b6 100644 --- a/iOS/Schema/Sources/ServerDrivenComponent/Screen/NavigationBar.swift +++ b/iOS/Schema/Sources/ServerDrivenComponent/Screen/NavigationBar.swift @@ -45,7 +45,7 @@ public struct NavigationBar: Decodable, AutoInitiable { public struct NavigationBarItem: Decodable, AccessibilityComponent, IdentifiableComponent { public let id: String? - public let image: String? + public let image: StringOrExpression? public let text: String public let action: RawAction public let accessibility: Accessibility? diff --git a/iOS/Schema/Sources/ServerDrivenComponent/TabBar/TabBar.swift b/iOS/Schema/Sources/ServerDrivenComponent/TabBar/TabBar.swift index 34e78814d8..86c75bab94 100644 --- a/iOS/Schema/Sources/ServerDrivenComponent/TabBar/TabBar.swift +++ b/iOS/Schema/Sources/ServerDrivenComponent/TabBar/TabBar.swift @@ -37,12 +37,12 @@ public struct TabBar: RawComponent, AutoInitiableAndDecodable { } public struct TabBarItem: Decodable, AutoInitiable { - public let icon: String? + public let icon: StringOrExpression? public let title: String? // sourcery:inline:auto:TabBarItem.Init public init( - icon: String? = nil, + icon: StringOrExpression? = nil, title: String? = nil ) { self.icon = icon diff --git a/iOS/Schema/Sources/ServerDrivenComponent/TabView/TabView.swift b/iOS/Schema/Sources/ServerDrivenComponent/TabView/TabView.swift index 3ef2d8f474..19436a26fe 100644 --- a/iOS/Schema/Sources/ServerDrivenComponent/TabView/TabView.swift +++ b/iOS/Schema/Sources/ServerDrivenComponent/TabView/TabView.swift @@ -16,12 +16,12 @@ public struct TabItem: Decodable { - public let icon: String? + public let icon: StringOrExpression? public let title: String? public let child: RawComponent public init( - icon: String? = nil, + icon: StringOrExpression? = nil, title: String? = nil, child: RawComponent ) { @@ -31,7 +31,7 @@ public struct TabItem: Decodable { } public init( - icon: String? = nil, + icon: StringOrExpression? = nil, title: String? = nil, @ChildBuilder _ child: () -> RawComponent diff --git a/iOS/Schema/Sources/Widgets/Image/Image.swift b/iOS/Schema/Sources/Widgets/Image/Image.swift index 5360d9744e..08cf15d16e 100644 --- a/iOS/Schema/Sources/Widgets/Image/Image.swift +++ b/iOS/Schema/Sources/Widgets/Image/Image.swift @@ -33,9 +33,19 @@ public struct Image: RawWidget, AutoDecodable { self.widgetProperties = widgetProperties } - indirect public enum ImagePath: Decodable { + public init( + _ path: ImagePath, + mode: ImageContentMode? = nil, + widgetProperties: WidgetProperties = WidgetProperties() + ) { + self.path = .value(path) + self.mode = mode + self.widgetProperties = widgetProperties + } + + public enum ImagePath: Decodable { case remote(Remote) - case local(String) + case local(StringOrExpression) enum CodingKeys: String, CodingKey { case type = "_beagleImagePath_" @@ -61,10 +71,10 @@ public struct Image: RawWidget, AutoDecodable { public extension Image { struct Remote: Decodable { - public let url: String + public let url: StringOrExpression public let placeholder: String? - public init(url: String, placeholder: String? = nil) { + public init(url: StringOrExpression, placeholder: String? = nil) { self.url = url self.placeholder = placeholder } diff --git a/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj b/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj index 226d21daba..6aafe6a49a 100644 --- a/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj +++ b/iOS/Sources/Beagle/Beagle.xcodeproj/project.pbxproj @@ -58,7 +58,6 @@ 7CA2E83D249822D400B24782 /* BeagleLoggerProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA2E83C249822D400B24782 /* BeagleLoggerProxyTests.swift */; }; 7CC2D9B424C614FB0059D5F9 /* TabBar+Renderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC2D9B324C614FB0059D5F9 /* TabBar+Renderable.swift */; }; 7CC2D9B624C6150E0059D5F9 /* TabBarUIComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC2D9B524C6150E0059D5F9 /* TabBarUIComponent.swift */; }; - 7CC2D9BA24C785750059D5F9 /* TabBarUIComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC2D9B924C785750059D5F9 /* TabBarUIComponentTests.swift */; }; 7CC737FE245729B1004FBC16 /* FormDataStoreHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC737FD245729B1004FBC16 /* FormDataStoreHandlerTests.swift */; }; 7CDF84732416AE0D00234F40 /* UnitValueExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDF84712416ADCB00234F40 /* UnitValueExtensionTests.swift */; }; 7CDF84852416DABC00234F40 /* StyleBuildersExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDF84842416DABC00234F40 /* StyleBuildersExtensionTests.swift */; }; @@ -95,11 +94,9 @@ 981DB2EF23748BAB001CE5E0 /* UnitValueExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981DB2EE23748BAB001CE5E0 /* UnitValueExtension.swift */; }; 983E8C862396C7570018C6CA /* TabView+Renderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C852396C7570018C6CA /* TabView+Renderable.swift */; }; 983E8C8C2396C7FA0018C6CA /* TabViewUIComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C8B2396C7FA0018C6CA /* TabViewUIComponent.swift */; }; - 983E8C8E2396C81C0018C6CA /* TabBarCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C8D2396C81C0018C6CA /* TabBarCollectionViewCell.swift */; }; + 983E8C8E2396C81C0018C6CA /* TabBarItemUIComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C8D2396C81C0018C6CA /* TabBarItemUIComponent.swift */; }; 983E8C912396DF430018C6CA /* TabViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C902396DF430018C6CA /* TabViewTests.swift */; }; - 983E8C972396DFAA0018C6CA /* TabBarCollectionViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C962396DFAA0018C6CA /* TabBarCollectionViewCellTests.swift */; }; 983E8C992396DFC90018C6CA /* TabViewUIComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983E8C982396DFC90018C6CA /* TabViewUIComponentTests.swift */; }; - 9848A53023AAF1C2002474AD /* ContainerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9848A52F23AAF1C2002474AD /* ContainerIndicatorView.swift */; }; 988F81FB23E24ECA008562BE /* UIView+Subviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988F81FA23E24ECA008562BE /* UIView+Subviews.swift */; }; 98B7D95623DB2722002622AC /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B7D95523DB2722002622AC /* Observable.swift */; }; 98E18D16249BE714009D6A54 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E18D15249BE714009D6A54 /* Alert.swift */; }; @@ -320,7 +317,6 @@ 7CA2E83C249822D400B24782 /* BeagleLoggerProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeagleLoggerProxyTests.swift; sourceTree = ""; }; 7CC2D9B324C614FB0059D5F9 /* TabBar+Renderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabBar+Renderable.swift"; sourceTree = ""; }; 7CC2D9B524C6150E0059D5F9 /* TabBarUIComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarUIComponent.swift; sourceTree = ""; }; - 7CC2D9B924C785750059D5F9 /* TabBarUIComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarUIComponentTests.swift; sourceTree = ""; }; 7CC737FD245729B1004FBC16 /* FormDataStoreHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormDataStoreHandlerTests.swift; sourceTree = ""; }; 7CDF84712416ADCB00234F40 /* UnitValueExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitValueExtensionTests.swift; sourceTree = ""; }; 7CDF84842416DABC00234F40 /* StyleBuildersExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleBuildersExtensionTests.swift; sourceTree = ""; }; @@ -366,11 +362,9 @@ 981DB2EE23748BAB001CE5E0 /* UnitValueExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitValueExtension.swift; sourceTree = ""; }; 983E8C852396C7570018C6CA /* TabView+Renderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabView+Renderable.swift"; sourceTree = ""; }; 983E8C8B2396C7FA0018C6CA /* TabViewUIComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewUIComponent.swift; sourceTree = ""; }; - 983E8C8D2396C81C0018C6CA /* TabBarCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCollectionViewCell.swift; sourceTree = ""; }; + 983E8C8D2396C81C0018C6CA /* TabBarItemUIComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarItemUIComponent.swift; sourceTree = ""; }; 983E8C902396DF430018C6CA /* TabViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewTests.swift; sourceTree = ""; }; - 983E8C962396DFAA0018C6CA /* TabBarCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCollectionViewCellTests.swift; sourceTree = ""; }; 983E8C982396DFC90018C6CA /* TabViewUIComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewUIComponentTests.swift; sourceTree = ""; }; - 9848A52F23AAF1C2002474AD /* ContainerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIndicatorView.swift; sourceTree = ""; }; 988F81FA23E24ECA008562BE /* UIView+Subviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Subviews.swift"; sourceTree = ""; }; 98B7D95523DB2722002622AC /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 98E18D15249BE714009D6A54 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; @@ -715,8 +709,7 @@ 7C8C21C924DC8A4800B95374 /* TabBarItemExtension.swift */, 7CC2D9B324C614FB0059D5F9 /* TabBar+Renderable.swift */, 7CC2D9B524C6150E0059D5F9 /* TabBarUIComponent.swift */, - 983E8C8D2396C81C0018C6CA /* TabBarCollectionViewCell.swift */, - 9848A52F23AAF1C2002474AD /* ContainerIndicatorView.swift */, + 983E8C8D2396C81C0018C6CA /* TabBarItemUIComponent.swift */, 7CC2D9B824C785630059D5F9 /* Tests */, ); path = "TabBar+Renderable"; @@ -726,8 +719,6 @@ isa = PBXGroup; children = ( 7C7B249324C78F43005A4D3D /* __Snapshots__ */, - 983E8C962396DFAA0018C6CA /* TabBarCollectionViewCellTests.swift */, - 7CC2D9B924C785750059D5F9 /* TabBarUIComponentTests.swift */, 7C7B249124C78E4C005A4D3D /* TabBarTests.swift */, ); path = Tests; @@ -1914,7 +1905,7 @@ A024CF0F24991E42009BB92D /* UIView+Style.swift in Sources */, C08D4E1F24857B4E00AAAC0A /* NavigationBar+UIKit.swift in Sources */, A04B19FA23ECA3F7007B4279 /* Screen+Renderable.swift in Sources */, - 983E8C8E2396C81C0018C6CA /* TabBarCollectionViewCell.swift in Sources */, + 983E8C8E2396C81C0018C6CA /* TabBarItemUIComponent.swift in Sources */, C09F4106249AB146002605EE /* KeyedDecodingContainerExtension.swift in Sources */, C08D4E9A248594EA00AAAC0A /* Image+Renderable.swift in Sources */, 7CFE3A752412E3B50087F664 /* StyleBuildersExtension.swift in Sources */, @@ -1957,7 +1948,6 @@ 93EE0CC3252E56BC0032BE77 /* BindingConfigurator.swift in Sources */, 6EB3368C24A52D34000A9D17 /* SubmitForm.swift in Sources */, E531D6B12359FD18007EEBA7 /* LoadingView.swift in Sources */, - 9848A53023AAF1C2002474AD /* ContainerIndicatorView.swift in Sources */, C08D4E1E24857B4800AAAC0A /* Container+Renderable.swift in Sources */, 57A80981243CF4620034D180 /* DependencyCacheManager.swift in Sources */, C85E1CCF23EB493200C1E323 /* NetworkClientDefault.swift in Sources */, @@ -2032,7 +2022,6 @@ C08D4E472485804D00AAAC0A /* LazyComponentTests.swift in Sources */, 93ADB87923FAD31D005B7CD2 /* ComposeComponentTests.swift in Sources */, 7CDF84852416DABC00234F40 /* StyleBuildersExtensionTests.swift in Sources */, - 7CC2D9BA24C785750059D5F9 /* TabBarUIComponentTests.swift in Sources */, E79D846D23AABFA100BAE9FA /* BeaglePrefetchHelperTests.swift in Sources */, 25CDD46D24EC4A4600CC49CB /* OperationNumberEvaluationTests.swift in Sources */, C08D4E8A2485945700AAAC0A /* WebViewTests.swift in Sources */, @@ -2042,7 +2031,6 @@ 25CDD47424ED666D00CC49CB /* OperationStringEvaluationTests.swift in Sources */, C0291889247D5BFA00C3AA13 /* ComponentFromJsonFile.swift in Sources */, C829CF2323B197CF001B7225 /* FormInputTests.swift in Sources */, - 983E8C972396DFAA0018C6CA /* TabBarCollectionViewCellTests.swift in Sources */, A0EF0ABE238B05A10019BE83 /* ValidatorHandlerTests.swift in Sources */, 93EE0C94252CA16C0032BE77 /* SetContextTests.swift in Sources */, 93B9746E23A287A800B0D1CF /* NSErrorExtensionTests.swift in Sources */, diff --git a/iOS/Sources/Beagle/CodeGeneration/Generated/AutoDecodable.generated.swift b/iOS/Sources/Beagle/CodeGeneration/Generated/AutoDecodable.generated.swift index 2e425d31f1..3cdc41ac42 100644 --- a/iOS/Sources/Beagle/CodeGeneration/Generated/AutoDecodable.generated.swift +++ b/iOS/Sources/Beagle/CodeGeneration/Generated/AutoDecodable.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Sources/Beagle/CodeGeneration/Generated/Equality.generated.swift b/iOS/Sources/Beagle/CodeGeneration/Generated/Equality.generated.swift index 2c4837333d..71eb2c67b8 100644 --- a/iOS/Sources/Beagle/CodeGeneration/Generated/Equality.generated.swift +++ b/iOS/Sources/Beagle/CodeGeneration/Generated/Equality.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT /* diff --git a/iOS/Sources/Beagle/Sources/Caching/Prefetch/Tests/BeaglePrefetchHelperTests.swift b/iOS/Sources/Beagle/Sources/Caching/Prefetch/Tests/BeaglePrefetchHelperTests.swift index 50899b0063..09d19a69a8 100644 --- a/iOS/Sources/Beagle/Sources/Caching/Prefetch/Tests/BeaglePrefetchHelperTests.swift +++ b/iOS/Sources/Beagle/Sources/Caching/Prefetch/Tests/BeaglePrefetchHelperTests.swift @@ -51,7 +51,7 @@ final class BeaglePrefetchHelperTests: XCTestCase { let reference = CacheReference(identifier: url, data: jsonData, hash: "123") //When - sut.prefetchComponent(newPath: .init(url: .value(url), shouldPrefetch: true)) + sut.prefetchComponent(newPath: .init(url: url, shouldPrefetch: true)) cacheManager.addToCache(reference) let result = dependencies.cacheManager?.getReference(identifiedBy: url) @@ -73,9 +73,9 @@ final class BeaglePrefetchHelperTests: XCTestCase { //When cacheManager.addToCache(reference) - sut.prefetchComponent(newPath: .init(url: .value(url), shouldPrefetch: true)) + sut.prefetchComponent(newPath: .init(url: url, shouldPrefetch: true)) let result1 = dependencies.cacheManager?.getReference(identifiedBy: url) - sut.prefetchComponent(newPath: .init(url: .value(url), shouldPrefetch: true)) + sut.prefetchComponent(newPath: .init(url: url, shouldPrefetch: true)) let result2 = dependencies.cacheManager?.getReference(identifiedBy: url) //Then @@ -95,24 +95,24 @@ final class BeaglePrefetchHelperTests: XCTestCase { Navigate.openNativeRoute(.init(route: path, data: data)), Navigate.resetApplication(.declarative(Screen(child: container))), - Navigate.resetApplication(.remote(.init(url: .value(path), shouldPrefetch: true))), - Navigate.resetApplication(.remote(.init(url: .value(path), shouldPrefetch: false))), + Navigate.resetApplication(.remote(.init(url: path, shouldPrefetch: true))), + Navigate.resetApplication(.remote(.init(url: path, shouldPrefetch: false))), Navigate.resetStack(.declarative(Screen(child: container))), - Navigate.resetStack(.remote(.init(url: .value(path), shouldPrefetch: true))), - Navigate.resetStack(.remote(.init(url: .value(path), shouldPrefetch: false))), + Navigate.resetStack(.remote(.init(url: path, shouldPrefetch: true))), + Navigate.resetStack(.remote(.init(url: path, shouldPrefetch: false))), Navigate.pushStack(.declarative(Screen(child: container))), - Navigate.pushStack(.remote(.init(url: .value(path), shouldPrefetch: true))), - Navigate.pushStack(.remote(.init(url: .value(path), shouldPrefetch: false))), + Navigate.pushStack(.remote(.init(url: path, shouldPrefetch: true))), + Navigate.pushStack(.remote(.init(url: path, shouldPrefetch: false))), Navigate.pushStack(.declarative(Screen(child: container)), controllerId: "customId"), - Navigate.pushStack(.remote(.init(url: .value(path), shouldPrefetch: true)), controllerId: "customId"), - Navigate.pushStack(.remote(.init(url: .value(path), shouldPrefetch: false)), controllerId: "customId"), + Navigate.pushStack(.remote(.init(url: path, shouldPrefetch: true)), controllerId: "customId"), + Navigate.pushStack(.remote(.init(url: path, shouldPrefetch: false)), controllerId: "customId"), Navigate.pushView(.declarative(Screen(child: container))), - Navigate.pushView(.remote(.init(url: .value(path), shouldPrefetch: true))), - Navigate.pushView(.remote(.init(url: .value(path), shouldPrefetch: false))), + Navigate.pushView(.remote(.init(url: path, shouldPrefetch: true))), + Navigate.pushView(.remote(.init(url: path, shouldPrefetch: false))), Navigate.popStack, Navigate.popView, diff --git a/iOS/Sources/Beagle/Sources/Components/Button+Renderable/Tests/ButtonTests.swift b/iOS/Sources/Beagle/Sources/Components/Button+Renderable/Tests/ButtonTests.swift index 8d9c00acdd..c16c28a473 100644 --- a/iOS/Sources/Beagle/Sources/Components/Button+Renderable/Tests/ButtonTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/Button+Renderable/Tests/ButtonTests.swift @@ -76,7 +76,7 @@ final class ButtonTests: XCTestCase { controller.dependencies = BeagleScreenDependencies(preFetchHelper: prefetch) let navigatePath = "path-to-prefetch" - let navigate = Navigate.pushStack(.remote(.init(url: .value(navigatePath)))) + let navigate = Navigate.pushStack(.remote(.init(url: navigatePath))) let button = Button(text: "prefetch", onPress: [navigate]) // When diff --git a/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Image+Renderable.swift b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Image+Renderable.swift index bbc2f963c3..3b729780cd 100644 --- a/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Image+Renderable.swift +++ b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Image+Renderable.swift @@ -20,22 +20,47 @@ import BeagleSchema extension Image: Widget { public func toView(renderer: BeagleRenderer) -> UIView { - let image = UIImageView(frame: .zero) - image.clipsToBounds = true - image.contentMode = (mode ?? .fitCenter).toUIKit() - var token: RequestToken? + let image = BeagleImageView(with: mode) + + switch path { + case .value(let path): + observeFields(path, renderer, image) + case .expression: + observePath(renderer, image) + } + + return image + } + private func observeFields(_ path: Image.ImagePath, _ renderer: BeagleRenderer, _ image: BeagleImageView) { + switch path { + case .local(let mobileId): + let expression: Expression = "\(mobileId)" + renderer.observe(expression, andUpdateManyIn: image) { mobileId in + guard let mobileId = mobileId else { return } + self.setImageFromAsset(named: mobileId, bundle: renderer.controller.dependencies.appBundle, imageView: image) + } + case .remote(let remote): + let expression: Expression = "\(remote.url)" + renderer.observe(expression, andUpdateManyIn: image) { url in + guard let url = url else { return } + image.token?.cancel() + image.token = self.setRemoteImage(from: url, placeholder: remote.placeholder, imageView: image, renderer: renderer) + } + } + } + + private func observePath(_ renderer: BeagleRenderer, _ image: BeagleImageView) { renderer.observe(path, andUpdateManyIn: image) { path in - token?.cancel() + image.token?.cancel() switch path { case .local(let mobileId): self.setImageFromAsset(named: mobileId, bundle: renderer.controller.dependencies.appBundle, imageView: image) case .remote(let remote): - token = self.setRemoteImage(from: remote.url, placeholder: remote.placeholder, imageView: image, renderer: renderer) + image.token = self.setRemoteImage(from: remote.url, placeholder: remote.placeholder, imageView: image, renderer: renderer) case .none: () } } - return image } private func setImageFromAsset(named: String, bundle: Bundle, imageView: UIImageView) { @@ -68,3 +93,17 @@ extension Image: Widget { } } } + +private class BeagleImageView: UIImageView { + var token: RequestToken? + + init(with mode: ImageContentMode?) { + super.init(frame: .zero) + clipsToBounds = true + contentMode = (mode ?? .fitCenter).toUIKit() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/ImageTests.swift b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/ImageTests.swift index c46fe3e33e..bf01a610fc 100644 --- a/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/ImageTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/ImageTests.swift @@ -91,17 +91,17 @@ class ImageTests: XCTestCase { func testCancelRequest() { //Given - let image = Image("@{img.path}") + let image = Image(.remote(.init(url: "@{url}"))) let dependency = BeagleDependencies() let imageDownloader = ImageDownloaderStub(imageResult: .success(Data())) dependency.imageDownloader = imageDownloader let container = Container(children: [image]) let controller = BeagleScreenViewController(viewModel: .init(screenType:.declarative(container.toScreen()), dependencies: dependency)) - let action = SetContext(contextId: "img", path: "path", value: ["_beagleImagePath_": "local", "mobileId": "shuttle"]) + let action = SetContext(contextId: "url", value: "www.com.br") let view = image.toView(renderer: controller.renderer) //When - view.setContext(Context(id: "img", value: ["path": ["_beagleImagePath_": "remote", "url": "www.com.br"]])) + view.setContext(Context(id: "url", value: "www.beagle.com.br")) controller.bindings.config() action.execute(controller: controller, origin: view) @@ -111,7 +111,7 @@ class ImageTests: XCTestCase { func testInvalidURL() { // Given - let component = Image(.value(.remote(.init(url: "www.com")))) + let component = Image(.remote(.init(url: "www.com"))) // When let imageView = renderer.render(component) as? UIImageView @@ -122,7 +122,7 @@ class ImageTests: XCTestCase { func testPlaceholder() { // Given - let component = Image(.value(.remote(.init(url: "www.com", placeholder: "test_image_square-x")))) + let component = Image(.remote(.init(url: "www.com", placeholder: "test_image_square-x"))) // When let placeholderView = renderer.render(component) as? UIImageView @@ -134,7 +134,7 @@ class ImageTests: XCTestCase { func testImageLeak() { // Given - let component = Image("@{img.path}", mode: .fitXY) + let component = Image(.remote(.init(url: "@{img.path}")), mode: .fitXY) let controller = BeagleScreenViewController(viewModel: .init(screenType:.declarative(component.toScreen()), dependencies: BeagleDependencies())) var view = component.toView(renderer: controller.renderer) @@ -147,6 +147,42 @@ class ImageTests: XCTestCase { // Then XCTAssertNil(weakView) } + + func testImageWithPathCancelRequest() { + //Given + let image = Image("@{img.path}") + let dependency = BeagleDependencies() + let imageDownloader = ImageDownloaderStub(imageResult: .success(Data())) + dependency.imageDownloader = imageDownloader + let container = Container(children: [image]) + let controller = BeagleScreenViewController(viewModel: .init(screenType:.declarative(container.toScreen()), dependencies: dependency)) + let action = SetContext(contextId: "img", path: "path", value: ["_beagleImagePath_": "local", "mobileId": "shuttle"]) + let view = image.toView(renderer: controller.renderer) + + //When + view.setContext(Context(id: "img", value: ["path": ["_beagleImagePath_": "remote", "url": "www.com.br"]])) + controller.bindings.config() + action.execute(controller: controller, origin: view) + + // Then + XCTAssertTrue(imageDownloader.token.didCallCancel) + } + + func testLocalImageWithContext() { + //Given + let container = Container( + children: [ + Image(.local("@{mobileId}")) + ], + context: Context(id: "mobileId", value: "test_image_square-x") + ) + + //When + let controller = BeagleScreenViewController(viewModel: .init(screenType:.declarative(container.toScreen()), dependencies: dependencies)) + + // Then + assertSnapshotImage(controller.view, size: ImageSize.custom(CGSize(width: 100, height: 100))) + } } private struct ImageDownloaderMock: ImageDownloader { diff --git a/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/__Snapshots__/ImageTests/testLocalImageWithContext.1.png b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/__Snapshots__/ImageTests/testLocalImageWithContext.1.png new file mode 100644 index 0000000000..f09d22cb27 Binary files /dev/null and b/iOS/Sources/Beagle/Sources/Components/Image+Renderable/Tests/__Snapshots__/ImageTests/testLocalImageWithContext.1.png differ diff --git a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/NavigationBar+UIKit.swift b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/NavigationBar+UIKit.swift index a55b216777..b106d38cd9 100644 --- a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/NavigationBar+UIKit.swift +++ b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/NavigationBar+UIKit.swift @@ -40,11 +40,7 @@ extension NavigationBarItem { self.controller = controller super.init() if let localImage = barItem.image { - image = UIImage( - named: localImage, - in: controller.dependencies.appBundle, - compatibleWith: nil - )?.withRenderingMode(.alwaysOriginal) + handleContextOnNavigationBarImage(icon: localImage) accessibilityHint = barItem.text } else { title = barItem.text @@ -55,6 +51,23 @@ extension NavigationBarItem { ViewConfigurator.applyAccessibility(barItem.accessibility, to: self) } + private func handleContextOnNavigationBarImage(icon: String) { + let expression: Expression = "\(icon)" + let renderer = controller?.renderer + + // Since `BeagleScreenViewController` creates a different view hierarchy, to get the correct hierarchy we need to use the `view` from our `controller`. + guard case .view(let view) = controller?.content else { return } + + renderer?.observe(expression, andUpdateManyIn: view) { icon in + guard let icon = icon else { return } + self.image = UIImage( + named: icon, + in: self.controller?.dependencies.appBundle, + compatibleWith: nil + )?.withRenderingMode(.alwaysOriginal) + } + } + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/ScreenComponentTests.swift b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/ScreenComponentTests.swift index 699c0a81b6..b62ec8c46d 100644 --- a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/ScreenComponentTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/ScreenComponentTests.swift @@ -92,6 +92,30 @@ final class ScreenComponentTests: XCTestCase { assertSnapshotImage(viewController, size: .custom(CGSize(width: 300, height: 200))) } + func testNavigationBarItemWithContextOnImage() { + // Given + let dependencies = BeagleDependencies() + dependencies.appBundle = Bundle(for: ScreenComponentTests.self) + + let barItem = NavigationBarItem(image: "@{image}", text: "", action: ActionDummy()) + + let screen = Screen( + safeArea: SafeArea.all, + navigationBar: .init(title: "title", showBackButton: true, navigationBarItems: [barItem]), + child: Text("test"), + context: Context(id: "image", value: "shuttle") + ) + + // When + let controller = BeagleScreenViewController(viewModel: .init( + screenType: .declarative(screen), + dependencies: dependencies + )) + + // Then + assertSnapshotImage(controller.view, size: .custom(CGSize(width: 150, height: 80))) + } + func test_action_shouldBeTriggered() { // Given let action = ActionSpy() @@ -116,7 +140,7 @@ final class ScreenComponentTests: XCTestCase { controller.dependencies = BeagleScreenDependencies(preFetchHelper: prefetch) let navigatePath = "button-item-prefetch" - let navigate = Navigate.pushView(.remote(.init(url: .value(navigatePath), shouldPrefetch: true))) + let navigate = Navigate.pushView(.remote(.init(url: navigatePath, shouldPrefetch: true))) let barItem = NavigationBarItem(text: "Item", action: navigate) let screen = ScreenComponent( navigationBar: NavigationBar(title: "Prefetch", navigationBarItems: [barItem]), diff --git a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/__Snapshots__/ScreenComponentTests/testNavigationBarItemWithContextOnImage.1.png b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/__Snapshots__/ScreenComponentTests/testNavigationBarItemWithContextOnImage.1.png new file mode 100644 index 0000000000..35ace7d589 Binary files /dev/null and b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Screen/Tests/__Snapshots__/ScreenComponentTests/testNavigationBarItemWithContextOnImage.1.png differ diff --git a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Style/StyleViewConfigurator.swift b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Style/StyleViewConfigurator.swift index df9d4f64f0..5dd3260499 100644 --- a/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Style/StyleViewConfigurator.swift +++ b/iOS/Sources/Beagle/Sources/Components/Layout+Renderable/Style/StyleViewConfigurator.swift @@ -80,17 +80,15 @@ final class StyleViewConfigurator: StyleViewConfiguratorProtocol { func markDirty() { view?.yoga.markDirty() - var view: UIView? = self.view + var view = self.view while let currentView = view { - if !currentView.yoga.isEnabled { - currentView.superview?.invalidateIntrinsicContentSize() - currentView.setNeedsLayout() - break + if !(currentView.superview?.yoga.isEnabled ?? false) { + view?.setNeedsLayout() } view = view?.superview } } - + // MARK: - Private Methods private func applyYogaProperties(from style: Style, to layout: YGLayout) { diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/ContainerIndicatorView.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/ContainerIndicatorView.swift deleted file mode 100644 index a6fa593b85..0000000000 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/ContainerIndicatorView.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 UIKit - -final class ContainerIndicatorView: UIView { - lazy var indicatorView: UIView = { - let view = UIView() - view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.backgroundColor = .black - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupView() { - addSubview(indicatorView) - } -} diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBar+Renderable.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBar+Renderable.swift index 604eb15d94..4347b01729 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBar+Renderable.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBar+Renderable.swift @@ -20,24 +20,21 @@ import BeagleSchema extension TabBar: ServerDrivenComponent { public func toView(renderer: BeagleRenderer) -> UIView { - let view = TabBarUIComponent(model: .init(tabIndex: 0, tabBarItems: items)) - + let tabBarScroll = TabBarUIComponent(model: .init(tabBarItems: items, styleId: styleId, renderer: renderer)) + if let currentTab = currentTab { - renderer.observe(currentTab, andUpdateManyIn: view) { + renderer.observe(currentTab, andUpdateManyIn: tabBarScroll) { if let tab = $0 { - view.scrollTo(page: tab) + tabBarScroll.model.tabIndex = tab + tabBarScroll.scrollTo(page: tab) } } } - - view.onTabSelection = { tab in - renderer.controller.execute(actions: self.onTabSelection, with: "onTabSelection", and: .int(tab), origin: view) + + tabBarScroll.onTabSelection = { tab in + renderer.controller.execute(actions: self.onTabSelection, with: "onTabSelection", and: .int(tab), origin: tabBarScroll) } - if let styleId = styleId { - view.beagle.applyStyle(for: view as UIView, styleId: styleId, with: renderer.controller) - } - - return view + return tabBarScroll } } diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarCollectionViewCell.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarCollectionViewCell.swift deleted file mode 100644 index 2bdc233f8e..0000000000 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarCollectionViewCell.swift +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 UIKit -import BeagleSchema - -extension TabBarCollectionViewCell { - struct Model { - var selectedTextColor: UIColor? - var unselectedTextColor: UIColor? - var selectedIconColor: UIColor? - var unselectedIconColor: UIColor? - } -} - -final class TabBarCollectionViewCell: UICollectionViewCell { - - // MARK: - UIComponents - - lazy var stackView: UIStackView = { - let stack = UIStackView() - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .center - stack.spacing = 5 - stack.translatesAutoresizingMaskIntoConstraints = false - return stack - }() - - lazy var icon: UIImageView = { - let icon = UIImageView() - icon.invalidateIntrinsicContentSize() - icon.contentMode = .scaleAspectFit - icon.translatesAutoresizingMaskIntoConstraints = false - return icon - }() - - lazy var title: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - var model: Model? - - override var isSelected: Bool { - didSet { - setupSelectionAppearance() - } - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - contentView.addSubview(stackView) - stackView.anchorTo(superview: contentView) - stackView.addArrangedSubview(icon) - stackView.addArrangedSubview(title) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - func setupTab(with tab: TabBarItem) { - switch tab.itemContentType { - case let .both(iconName, text): - icon.heightAnchor.constraint(lessThanOrEqualToConstant: 30).isActive = true - title.text = text - icon.image = model?.selectedIconColor == nil ? UIImage(named: iconName): UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate) - title.font = UIFont.systemFont(ofSize: 13) - icon.isHidden = false - title.isHidden = false - - case .icon(let iconName): - icon.widthAnchor.constraint(lessThanOrEqualToConstant: 35).isActive = true - icon.image = model?.selectedIconColor == nil ? UIImage(named: iconName): UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate) - icon.isHidden = false - title.isHidden = true - - case .title(let text): - title.isHidden = false - icon.isHidden = true - title.sizeToFit() - title.text = text - case .none: - title.isHidden = true - icon.isHidden = true - } - setupSelectionAppearance() - } - - private func setupSelectionAppearance() { - guard let model = model else { return } - switch styleVerification(model: model) { - case .both: - title.textColor = isSelected ? model.selectedTextColor : model.unselectedTextColor - icon.tintColor = isSelected ? model.selectedIconColor : model.unselectedIconColor - case .icon: - icon.tintColor = isSelected ? model.selectedIconColor : model.unselectedIconColor - title.textColor = isSelected ? .black : .gray - case .text: - title.textColor = isSelected ? model.selectedTextColor : model.unselectedTextColor - icon.tintColor = isSelected ? .black : .gray - default: - title.textColor = isSelected ? .black : .gray - icon.tintColor = isSelected ? .black : .gray - } - } - - private func styleVerification(model: Model) -> StyleEnabler { - switch (model.selectedIconColor, model.unselectedIconColor, model.selectedTextColor, model.unselectedTextColor) { - case let (selectedIconColor?, unselectedIconColor?, selectedTextColor?, unselectedTextColor?): - return .both(iconSelectedColor: selectedIconColor, - iconUnselectedColor: unselectedIconColor, - textSelectedColor: selectedTextColor, - textUnselectedColor: unselectedTextColor) - case let (selectedIconColor?, unselectedIconColor?, _, _): - return .icon(selectedIconColor, unselectedIconColor) - case let (_, _, selectedTextColor?, unselectedTextColor?): - return .text(selectedTextColor, unselectedTextColor) - default: - return .none - } - } - - private enum StyleEnabler { - case icon(UIColor, UIColor) - case text(UIColor, UIColor) - case both(iconSelectedColor: UIColor, iconUnselectedColor: UIColor, textSelectedColor: UIColor, textUnselectedColor: UIColor) - case none - } -} diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarItemUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarItemUIComponent.swift new file mode 100644 index 0000000000..20f46b5c3e --- /dev/null +++ b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarItemUIComponent.swift @@ -0,0 +1,137 @@ +/* + * 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 UIKit +import BeagleSchema + +final class TabBarItemUIComponent: UIView { + + // MARK: - UIComponents + + private lazy var icon: UIImageView = { + let icon = UIImageView() + icon.invalidateIntrinsicContentSize() + icon.contentMode = .scaleAspectFit + return icon + }() + + private lazy var title: UILabel = { + let label = UILabel() + label.sizeToFit() + label.textAlignment = .center + return label + }() + + // MARK: - Properties + + var index: Int? + var theme: TabBarTheme? { + didSet { + setupSelectionAppearance() + } + } + + var isSelected: Bool? { + didSet { + setupSelectionAppearance() + } + } + + private var renderer: BeagleRenderer? + + // MARK: - Initialization + + init( + index: Int, + renderer: BeagleRenderer + ) { + super.init(frame: .zero) + self.renderer = renderer + self.index = index + addSubview(icon) + addSubview(title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + func setupTab(with tab: TabBarItem) { + switch tab.itemContentType { + + case let .both(iconName, text): + title.text = text + title.font = .systemFont(ofSize: 14) + + handleContextOnImage(iconName: iconName) + + title.style.setup(Style().display(.flex)) + icon.style.setup(Style() + .display(.flex) + .size(Size().width(30).height(30)) + ) + + case .icon(let iconName): + + handleContextOnImage(iconName: iconName) + + title.style.setup(Style().display(.none)) + icon.style.setup(Style() + .display(.flex) + .size(Size().width(35).height(35)) + ) + + case .title(let text): + title.text = text + title.font = .systemFont(ofSize: 16) + + title.style.setup(Style().display(.flex).flex(Flex().alignSelf(.center))) + icon.style.setup(Style().display(.none)) + + case .none: + title.style.setup(Style().display(.none)) + icon.style.setup(Style().display(.none)) + } + } + + private func setupSelectionAppearance() { + let appearanceColor = getSelectedAppearanceColors() + title.textColor = appearanceColor.textColor + icon.tintColor = appearanceColor.iconColor + } + + private func getSelectedAppearanceColors() -> (textColor: UIColor, iconColor: UIColor) { + guard let theme = theme, let isSelected = isSelected else { return (.black, .black) } + if isSelected { + return (theme.selectedTextColor ?? UIColor.black, theme.selectedIconColor ?? UIColor.black) + } + return (theme.unselectedTextColor ?? UIColor.gray, theme.unselectedIconColor ?? UIColor.gray) + } + + private func handleContextOnImage(iconName: String) { + let expression: Expression = "\(iconName)" + + renderer?.observe(expression, andUpdateManyIn: self) { icon in + 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) + } + } + } +} diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarUIComponent.swift index ea6f2a826f..40edce6141 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/TabBarUIComponent.swift @@ -17,60 +17,47 @@ import UIKit import BeagleSchema + +struct TabBarTheme { + var selectedTextColor: UIColor? + var unselectedTextColor: UIColor? + var selectedIconColor: UIColor? + var unselectedIconColor: UIColor? +} -final class TabBarUIComponent: UIView { +final class TabBarUIComponent: UIScrollView { // MARK: - Model struct Model { - var tabIndex: Int + var tabIndex: Int? var tabBarItems: [TabBarItem] - var selectedTextColor: UIColor? - var unselectedTextColor: UIColor? - var selectedIconColor: UIColor? - var unselectedIconColor: UIColor? + var styleId: String? + var renderer: BeagleRenderer } - - // MARK: - Properties - private var shouldScrollToCurrentTab = true - private var shouldAnimateOnCellDisplay = false - private var containerWidthConstraint: NSLayoutConstraint? - private let tabBarPreferedHeight: CGFloat = 65 - private let collectionViewCellHeight: CGFloat = 55 + // MARK: - Properties + private var shouldCreateTabItemsView = true private let tabItemMinimumHorizontalMargin: CGFloat = 40 private let tabItemIconMinimunWidth: CGFloat = 75 var model: Model - + var tabItemViews = [Int: TabBarItemUIComponent]() var onTabSelection: ((_ tab: Int) -> Void)? - + // MARK: - UI - lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumInteritemSpacing = 0 - layout.minimumLineSpacing = 0 - let collection = UICollectionView( - frame: CGRect(), - collectionViewLayout: layout - ) - collection.backgroundColor = .clear - collection.register(TabBarCollectionViewCell.self, forCellWithReuseIdentifier: TabBarCollectionViewCell.className) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.showsHorizontalScrollIndicator = false - collection.dataSource = self - collection.delegate = self - return collection + private lazy var contentView: UIView = { + let view = UIView() + return view }() - lazy var containerIndicator: ContainerIndicatorView = { - let view = ContainerIndicatorView() - view.translatesAutoresizingMaskIntoConstraints = false + lazy var indicatorView: UIView = { + let view = UIView() + view.backgroundColor = .black return view }() - + // MARK: - Initialization init( @@ -78,8 +65,8 @@ final class TabBarUIComponent: UIView { ) { self.model = model super.init(frame: .zero) - setupLayout() - scrollTo(page: 0) + setupScrollView() + setupContentView() } @available(*, unavailable) @@ -87,66 +74,173 @@ final class TabBarUIComponent: UIView { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + if let contentView = subviews.first { + contentSize = contentView.frame.size + resetTabItemsStyle() + + // Creates tabItems only after view it's already in superview hierarchy + if superview != nil && shouldCreateTabItemsView { + setupTabBarItems() + setupIndicatorViewStyle(for: tabItemViews[0]) + style.applyLayout() + scrollTo(page: model.tabIndex ?? 0) + } + } + } + // MARK: - Layout - override var frame: CGRect { - didSet { - if collectionView.visibleCells.count != 0, shouldScrollToCurrentTab { - scrollTo(page: model.tabIndex) - shouldScrollToCurrentTab.toggle() - } + private func resetTabItemsStyle() { + tabItemViews.forEach { key, item in + let size = getContentSize(forItem: model.tabBarItems[key].itemContentType) + setupTabBarItemStyle(for: item, with: size.width) + } + guard let selectedTabItem = tabItemViews[model.tabIndex ?? 0] else { return } + setupIndicatorViewStyle(for: selectedTabItem) + model.renderer.controller.setNeedsLayout(component: self) + } + + private func setupScrollView() { + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + style.setup(Style( + size: Size().height(65), flex: Flex().flexDirection(.row).shrink(0)) + ) + } + + private func setupContentView() { + contentView.style.setup(Style( + flex: Flex(flexDirection: .row, grow: 0, shrink: 0)) + ) + contentView.addSubview(indicatorView) + addSubview(contentView) + } + + func setupTabBarItems() { + var index = 0 + shouldCreateTabItemsView = false + model.tabBarItems.forEach { item in + let size = getContentSize(forItem: item.itemContentType) + let itemView = createTabBarItemsView(with: item, index: index) + setupTabBarItemStyle(for: itemView, with: size.width) + tabItemViews[index] = itemView + index += 1 + contentView.addSubview(itemView) } + + if let styleId = model.styleId { + beagle.applyStyle(for: self as UIView, styleId: styleId, with: model.renderer.controller) + } + } + + private func createTabBarItemsView(with item: TabBarItem, index: Int) -> TabBarItemUIComponent { + let itemView = TabBarItemUIComponent(index: index, renderer: model.renderer) + itemView.setupTab(with: item) + + let tap = UITapGestureRecognizer(target: self, action: #selector(didSelectTabItem(sender:))) + itemView.addGestureRecognizer(tap) + itemView.isUserInteractionEnabled = true + return itemView + } + + @objc func didSelectTabItem(sender: UITapGestureRecognizer) { + guard let tabItem = sender.view as? TabBarItemUIComponent, let index = tabItem.index else { return } + model.tabIndex = index + setupTabBarItemsTheme(for: index) + onTabSelection?(index) + } + + private func setupTabBarItemsTheme(for currentIndex: Int) { + tabItemViews.forEach { _, item in + item.isSelected = currentIndex == item.index + } + } +} + +// MARK: - Style +private extension TabBarUIComponent { + func setupTabBarItemStyle(for item: TabBarItemUIComponent, with width: CGFloat) { + item.style.setup( + Style( + size: Size().height(62).width(.init(value: Double(width), type: .real)), + position: EdgeValue().left(5), + flex: Flex() + .alignItems(.center) + .justifyContent(.spaceEvenly)) + ) } - override func sizeThatFits(_ size: CGSize) -> CGSize { - .init(width: size.width, height: tabBarPreferedHeight) + func setupIndicatorViewStyle(for selectedItem: TabBarItemUIComponent?) { + guard let selectedItem = selectedItem else { return } + indicatorView.style.setup( + Style( + size: Size().height(3).width(.init(value: Double(selectedItem.bounds.width), type: .real)), + position: EdgeValue(left: UnitValue(value: Double(selectedItem.frame.origin.x), type: .real)), + positionType: .absolute, + flex: Flex().alignSelf(.flexEnd) + ) + ) } + +} + +// MARK: - TabBarItem Size + +private extension TabBarUIComponent { - private lazy var tabBarItensFreeHorizontalSpace: CGFloat = { + func getTabBarItensFreeHorizontalSpace() -> CGFloat { let tabBarItems = model.tabBarItems let tabBarItemsAvailableSpace = frame.width let tabItensRequiredSpace = tabBarItems.reduce(0) { result, item -> CGFloat in if let title = item.title { - return result + getCellMinimumWidth(for: title) + return result + getItemMinimumWidth(for: title) } return result + tabItemIconMinimunWidth } return tabItensRequiredSpace <= tabBarItemsAvailableSpace ? (tabBarItemsAvailableSpace - tabItensRequiredSpace) / CGFloat(tabBarItems.count) : 0 - }() + } - private func getCellMinimumWidth(for text: String) -> CGFloat { + func getItemMinimumWidth(for text: String) -> CGFloat { let label = UILabel() label.numberOfLines = 1 label.text = text return label.intrinsicContentSize.width + tabItemMinimumHorizontalMargin } + + func getContentSize(forItem item: TabBarItem.ItemContentType) -> CGSize { + switch item { + case .both(_, let title): + let minimumCellWidth = getItemMinimumWidth(for: title) + let width = minimumCellWidth <= tabItemIconMinimunWidth ? tabItemIconMinimunWidth : minimumCellWidth + return getContentSize(forWidth: width) + case .title(let title): + return getContentSize(forWidth: getItemMinimumWidth(for: title)) + default: + return getContentSize(forWidth: tabItemIconMinimunWidth) + } + } - private func setupLayout() { - addSubview(collectionView) - collectionView.anchor(top: topAnchor, left: leftAnchor, right: rightAnchor) - collectionView.heightAnchor.constraint(lessThanOrEqualToConstant: tabBarPreferedHeight).isActive = true - collectionView.addSubview(containerIndicator) - collectionView.bringSubviewToFront(containerIndicator.indicatorView) - - containerIndicator.anchor(bottom: collectionView.bottomAnchor, bottomConstant: -tabBarPreferedHeight, heightConstant: 3) - containerWidthConstraint = NSLayoutConstraint(item: containerIndicator.indicatorView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100) - containerWidthConstraint?.isActive = true + func getContentSize(forWidth width: CGFloat) -> CGSize { + CGSize(width: width + getTabBarItensFreeHorizontalSpace(), height: 62) } } // MARK: - Animation private extension TabBarUIComponent { - private func moveIndicatorView(to cell: UICollectionViewCell?) { - guard let cell = cell else { return } + private func moveIndicatorView(to tabItem: TabBarItemUIComponent) { UIView.animate( withDuration: 0.2, delay: 0, options: .curveLinear, animations: { - self.containerIndicator.indicatorView.frame.origin.x = cell.frame.origin.x - self.containerWidthConstraint?.constant = cell.frame.width - self.layoutIfNeeded() + self.setupIndicatorViewStyle(for: tabItem) + + // TODO: setNeedLayout should call layoutIfNeeded + self.model.renderer.controller.setNeedsLayout(component: self) + self.model.renderer.controller.view.layoutIfNeeded() } ) } @@ -157,72 +251,14 @@ private extension TabBarUIComponent { extension TabBarUIComponent { func scrollTo(page: Int) { model.tabIndex = page - let indexPath = IndexPath(item: page, section: 0) - collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically) - collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - guard let cell = collectionView.cellForItem(at: indexPath) else { - shouldAnimateOnCellDisplay = true - return - } - moveIndicatorView(to: cell) - } -} - -// MARK: - UICollection View Delegate and DataSource Extension - -extension TabBarUIComponent: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { - - func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - if indexPath.row == model.tabIndex, shouldAnimateOnCellDisplay { - moveIndicatorView(to: cell) - shouldAnimateOnCellDisplay.toggle() - } - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return model.tabBarItems.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let item = model.tabBarItems[indexPath.row] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabBarCollectionViewCell.className, for: indexPath) - guard let tabBarCell = cell as? TabBarCollectionViewCell else { return cell } + guard let view = tabItemViews[page] else { return } - tabBarCell.model = TabBarCollectionViewCell.Model( - selectedTextColor: model.selectedTextColor, - unselectedTextColor: model.unselectedTextColor, - selectedIconColor: model.selectedIconColor, - unselectedIconColor: model.unselectedIconColor + let visibleRect = CGRect( + origin: CGPoint(x: max(0, view.center.x - (frame.width / 2)), y: 0), + size: bounds.size ) - tabBarCell.setupTab(with: item) - - return tabBarCell - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return getCellSize(forContent: model.tabBarItems[indexPath.row].itemContentType) - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let index = Int(indexPath.row) - scrollTo(page: index) - onTabSelection?(index) - } - - private func getCellSize(forContent content: TabBarItem.ItemContentType) -> CGSize { - switch content { - case .both(_, let title): - let minimumCellWidth = getCellMinimumWidth(for: title) - let width = minimumCellWidth <= tabItemIconMinimunWidth ? tabItemIconMinimunWidth : minimumCellWidth - return getCellSize(forWidth: width) - case .title(let title): - return getCellSize(forWidth: getCellMinimumWidth(for: title)) - default: - return getCellSize(forWidth: tabItemIconMinimunWidth) - } - } - - private func getCellSize(forWidth width: CGFloat) -> CGSize { - CGSize(width: width + tabBarItensFreeHorizontalSpace, height: collectionViewCellHeight) + scrollRectToVisible(visibleRect, animated: true) + setupTabBarItemsTheme(for: page) + moveIndicatorView(to: view) } } diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarCollectionViewCellTests.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarCollectionViewCellTests.swift deleted file mode 100644 index 5c500d81ea..0000000000 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarCollectionViewCellTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 -import BeagleSchema - -final class TabBarCollectionViewCellTests: XCTestCase { - - func test_setupShouldSetTabItems() { - // Given - let sut = TabBarCollectionViewCell(frame: .zero) - - // When - sut.setupTab(with: TabBarItem(icon: "icon", title: "Tab")) - - let innerComponentView = Mirror(reflecting: sut).children.first - - // Then - XCTAssert(innerComponentView?.value is UIStackView) - XCTAssert(sut.contentView.subviews.count == 1) - } - - func test_setupShouldSetTabItemsWithIconOnly() { - // Given - let sut = TabBarCollectionViewCell(frame: .zero) - - // When - sut.setupTab(with: TabBarItem(icon: "icon")) - - let innerComponentView = Mirror(reflecting: sut).children.first - let stackView = innerComponentView?.value as? UIStackView - - // Then - XCTAssertNotNil(stackView) - XCTAssert(stackView?.subviews[0].isHidden == false) - - } - - func test_setupShouldSetTabItemsWithTitleOnly() { - // Given - let sut = TabBarCollectionViewCell(frame: .zero) - - // When - sut.setupTab(with: TabBarItem(title: "Tab 1")) - - let innerComponentView = Mirror(reflecting: sut).children.first - let stackView = innerComponentView?.value as? UIStackView - - // Then - XCTAssertNotNil(stackView) - XCTAssert(stackView?.subviews[1].isHidden == false) - } - -} diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarTests.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarTests.swift index 5b858fa9ce..afe8f9f08b 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarTests.swift @@ -21,16 +21,95 @@ import SnapshotTesting import BeagleSchema class TabBarTests: XCTestCase { - - func test_viewWithTabBar() { - let tabBar = TabBar(items: [ - TabBarItem(title: "TAB 1"), - TabBarItem(title: "TAB 2"), - TabBarItem(title: "TAB 3") - ]) - - let screen = Beagle.screen(.declarative(tabBar.toScreen())) - assertSnapshotImage(screen) + + var dependencies: BeagleDependencies { + // swiftlint:disable implicit_getter + get { + let dependency = BeagleDependencies() + dependency.appBundle = Bundle(for: TabBarTests.self) + return dependency + } } + lazy var controller = BeagleControllerStub(dependencies: dependencies) + lazy var renderer = BeagleRenderer(controller: controller) + + private let imageSize = ImageSize.custom(CGSize(width: 150, height: 80)) + + func testCurrentTabWithContext() { + // Given + let screen = Screen( + child: TabBar( + items: [ + TabBarItem(icon: "shuttle"), + TabBarItem(icon: "shuttle", title: "Tab 2") + ], + currentTab: "@{tab}"), + context: Context(id: "tab", value: 1) + ) + + // When + let controller = BeagleScreenViewController(viewModel: .init( + screenType: .declarative(screen), + dependencies: dependencies + )) + + // Then + assertSnapshotImage(controller.view, size: imageSize) + } + + func testImageWithContext() { + // Given + let screen = Screen( + child: TabBar( + items: [ + TabBarItem(icon: "@{image}"), + TabBarItem(icon: "@{image}", title: "Tab 2") + ]), + context: Context(id: "image", value: "shuttle") + ) + + // When + let controller = BeagleScreenViewController(viewModel: .init( + screenType: .declarative(screen), + dependencies: dependencies + )) + + // Then + assertSnapshotImage(controller.view, size: imageSize) + } + + func testTabSelection() { + // Given + let index = 1 + var didCalledOnTabSelection = false + var passedIndex: Int? + + let sut = TabBarUIComponent(model: .init( + tabIndex: 0, + tabBarItems: [ + TabBarItem(icon: "shuttle"), + TabBarItem(icon: "shuttle", title: "Tab 2") + ], + renderer: renderer) + ) + + sut.setupTabBarItems() + + // When + sut.onTabSelection = { index in + didCalledOnTabSelection = true + passedIndex = index + } + + guard let gestureRecognizer = sut.tabItemViews[index]?.gestureRecognizers?.first as? UITapGestureRecognizer else { + XCTFail("TabBarItem of index 1 has no gesture recognizer.") + return + } + sut.didSelectTabItem(sender: gestureRecognizer) + + // Then + XCTAssert(didCalledOnTabSelection) + XCTAssert(passedIndex == index) + } } diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarUIComponentTests.swift b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarUIComponentTests.swift deleted file mode 100644 index 6fb584ad11..0000000000 --- a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/TabBarUIComponentTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -/* - * 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 -import BeagleSchema - -class TabBarUIComponentTests: XCTestCase { - - private lazy var model = TabBarUIComponent.Model( - tabIndex: 0, - tabBarItems: [ - TabBarItem(icon: "beagle", title: "Tab 1"), - TabBarItem(icon: "beagle", title: "Tab 2") - ] - ) - - private lazy var sut = TabBarUIComponent(model: model) - - func test_didSelectItemAt_shouldCallOnTabSelectionClosure() { - // Given - let collectionView = sut.collectionView - let index = 1 - let indexPath = IndexPath(item: index, section: 0) - var didCalledOnTabSelection = false - var passedIndex: Int? - - // When - sut.onTabSelection = { index in - didCalledOnTabSelection = true - passedIndex = index - } - - sut.collectionView(collectionView, didSelectItemAt: indexPath) - - // Then - XCTAssert(didCalledOnTabSelection) - XCTAssert(passedIndex == index) - } - -} diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testCurrentTabWithContext.1.png b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testCurrentTabWithContext.1.png new file mode 100644 index 0000000000..85c3b10b8d Binary files /dev/null and b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testCurrentTabWithContext.1.png differ diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testImageWithContext.1.png b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testImageWithContext.1.png new file mode 100644 index 0000000000..e26f6de492 Binary files /dev/null and b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/testImageWithContext.1.png differ diff --git a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/test_viewWithTabBar.1.png b/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/test_viewWithTabBar.1.png deleted file mode 100644 index 357af406f0..0000000000 Binary files a/iOS/Sources/Beagle/Sources/Components/TabBar+Renderable/Tests/__Snapshots__/TabBarTests/test_viewWithTabBar.1.png and /dev/null differ diff --git a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabView+Renderable.swift b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabView+Renderable.swift index 73c033bfa1..4325675a2c 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabView+Renderable.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabView+Renderable.swift @@ -20,7 +20,7 @@ import BeagleSchema extension TabView: ServerDrivenComponent { public func toView(renderer: BeagleRenderer) -> UIView { - let model = TabViewUIComponent.Model(tabIndex: 0, tabViewItems: children) + let model = TabViewUIComponent.Model(tabIndex: 0, tabViewItems: children, renderer: renderer) let tabView = TabViewUIComponent(model: model, renderer: renderer) tabView.style.setup(Style(size: Size().width(100%), flex: Flex().grow(1))) diff --git a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabViewUIComponent.swift b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabViewUIComponent.swift index b4d9e1d623..7d646875b4 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabViewUIComponent.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/TabViewUIComponent.swift @@ -25,6 +25,7 @@ extension TabViewUIComponent { var unselectedTextColor: UIColor? var selectedIconColor: UIColor? var unselectedIconColor: UIColor? + var renderer: BeagleRenderer // sourcery:inline:auto:TabViewUIComponent.Model.Init init( @@ -33,7 +34,8 @@ extension TabViewUIComponent { selectedTextColor: UIColor? = nil, unselectedTextColor: UIColor? = nil, selectedIconColor: UIColor? = nil, - unselectedIconColor: UIColor? = nil + unselectedIconColor: UIColor? = nil, + renderer: BeagleRenderer ) { self.tabIndex = tabIndex self.tabViewItems = tabViewItems @@ -41,6 +43,7 @@ extension TabViewUIComponent { self.unselectedTextColor = unselectedTextColor self.selectedIconColor = selectedIconColor self.unselectedIconColor = unselectedIconColor + self.renderer = renderer } // sourcery:end } @@ -59,6 +62,7 @@ final class TabViewUIComponent: UIView { indicatorView: nil, controller: renderer.controller ) + view.style.setup(Style(flex: Flex().grow(1))) view.translatesAutoresizingMaskIntoConstraints = false return view } @@ -75,10 +79,7 @@ final class TabViewUIComponent: UIView { model: .init( tabIndex: model.tabIndex, tabBarItems: model.tabViewItems.map { TabBarItem(icon: $0.icon, title: $0.title) }, - selectedTextColor: model.selectedTextColor, - unselectedTextColor: model.unselectedTextColor, - selectedIconColor: model.selectedIconColor, - unselectedIconColor: model.unselectedIconColor + renderer: model.renderer ) ) self.contentView = Self.contentView(items: model.tabViewItems, renderer: renderer) @@ -97,6 +98,7 @@ final class TabViewUIComponent: UIView { tabBar.onTabSelection = { [weak self] index in guard let self = self else { return } self.contentView.swipeToPage(at: index) + self.tabBar.scrollTo(page: index) } } diff --git a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/Tests/TabViewUIComponentTests.swift b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/Tests/TabViewUIComponentTests.swift index d64561913a..cc571715a4 100644 --- a/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/Tests/TabViewUIComponentTests.swift +++ b/iOS/Sources/Beagle/Sources/Components/TabView+Renderable/Tests/TabViewUIComponentTests.swift @@ -21,6 +21,9 @@ import BeagleSchema final class TabViewUIComponentTests: XCTestCase { // MARK: - Variables + private lazy var controller = BeagleControllerStub() + private lazy var renderer = BeagleRenderer(controller: controller) + private lazy var component = TabView(children: [ TabItem(icon: "beagle", title: "Tab 1", child: Container(children: [ @@ -38,7 +41,7 @@ final class TabViewUIComponentTests: XCTestCase { ) ]) - private lazy var model = TabViewUIComponent.Model(tabIndex: 0, tabViewItems: component.children) + private lazy var model = TabViewUIComponent.Model(tabIndex: 0, tabViewItems: component.children, renderer: renderer) private lazy var sut = TabViewUIComponent(model: model, renderer: .init(controller: controllerStub)) @@ -93,11 +96,20 @@ final class TabViewUIComponentTests: XCTestCase { } func test_whenChangedTabs_shouldChangeCurrentPage() { - let tabBar = sut.tabBar - tabBar.collectionView(tabBar.collectionView, didSelectItemAt: IndexPath(item: 1, section: 0)) + // Given + sut.tabBar.setupTabBarItems() + let tabItem = sut.tabBar.tabItemViews[1] + + guard let gestureRecognizer = tabItem?.gestureRecognizers?.first as? UITapGestureRecognizer else { + XCTFail("TabItem of index 1 has no gesture recognizer.") + return + } + + // When + sut.tabBar.didSelectTabItem(sender: gestureRecognizer) + + // Then XCTAssert(sut.contentView.model.currentPage == 1) - tabBar.collectionView(tabBar.collectionView, didSelectItemAt: IndexPath(item: 0, section: 0)) - XCTAssert(sut.contentView.model.currentPage == 0) } } diff --git a/iOS/Sources/Beagle/Sources/ContextExpression/Tests/UIView+ContextTests.swift b/iOS/Sources/Beagle/Sources/ContextExpression/Tests/UIView+ContextTests.swift index bf642afdc5..a7cf33cadd 100644 --- a/iOS/Sources/Beagle/Sources/ContextExpression/Tests/UIView+ContextTests.swift +++ b/iOS/Sources/Beagle/Sources/ContextExpression/Tests/UIView+ContextTests.swift @@ -143,10 +143,10 @@ final class UIViewContextTests: XCTestCase { // Given var view: UIView? = UIView() weak var weakReference = view - + let contextId = "context" view?.setContext(Context(id: contextId, value: .empty)) - + let binding = Binding(context: contextId, path: .init(nodes: [])) let singleExpression = SingleExpression.value(.binding(binding)) let multipleExpression = MultipleExpression(nodes: [.expression(singleExpression)]) diff --git a/iOS/Sources/Beagle/Sources/Logger/Tests/BeagleLoggerTests.swift b/iOS/Sources/Beagle/Sources/Logger/Tests/BeagleLoggerTests.swift index 27e2e45e82..4b5141e7ae 100644 --- a/iOS/Sources/Beagle/Sources/Logger/Tests/BeagleLoggerTests.swift +++ b/iOS/Sources/Beagle/Sources/Logger/Tests/BeagleLoggerTests.swift @@ -42,7 +42,7 @@ class BeagleLoggerTests: XCTestCase { Log.form(.keyDuplication(data: ["key": "value"])), Log.navigation(.cantPopToAlreadyCurrentScreen(identifier: "identifier")), - Log.navigation(.didReceiveAction(Navigate.pushView(.remote(.init(url: .value(path)))))), + Log.navigation(.didReceiveAction(Navigate.pushView(.remote(.init(url: path))))), Log.navigation(.didReceiveAction(Navigate.openNativeRoute(.init(route: path)))), Log.navigation(.didReceiveAction(Navigate.openNativeRoute(.init(route: path, data: ["key": "value"])))), Log.navigation(.errorTryingToPopScreenOnNavigatorWithJustOneScreen), diff --git a/iOS/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift b/iOS/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift index f6e384b7db..fc8d95e7aa 100644 --- a/iOS/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift +++ b/iOS/Sources/Beagle/Sources/Renderer/BeagleScreenViewController.swift @@ -194,7 +194,7 @@ public class BeagleScreenViewController: BeagleController { } } - // MARK: - + // MARK: - Update View fileprivate func updateView(state: ViewModel.State) { switch state { @@ -249,6 +249,9 @@ public class BeagleScreenViewController: BeagleController { extension BeagleControllerProtocol where Self: UIViewController { public func setNeedsLayout(component: UIView) { dependencies.style(component).markDirty() + if let beagleView = view.superview as? BeagleView { + beagleView.invalidateIntrinsicContentSize() + } viewIfLoaded?.setNeedsLayout() } } diff --git a/iOS/Sources/Beagle/Sources/Theme/Theme.swift b/iOS/Sources/Beagle/Sources/Theme/Theme.swift index 5814f7fb48..cbc8ebb6fb 100644 --- a/iOS/Sources/Beagle/Sources/Theme/Theme.swift +++ b/iOS/Sources/Beagle/Sources/Theme/Theme.swift @@ -77,11 +77,16 @@ public struct BeagleStyle { return { guard let tabBar = $0 as? TabBarUIComponent else { return } tabBar.backgroundColor = backgroundColor - tabBar.containerIndicator.indicatorView.backgroundColor = indicatorColor - tabBar.model.selectedTextColor = selectedTextColor - tabBar.model.unselectedTextColor = unselectedTextColor - tabBar.model.selectedIconColor = selectedIconColor - tabBar.model.unselectedIconColor = unselectedIconColor + tabBar.indicatorView.backgroundColor = indicatorColor + tabBar.tabItemViews.forEach { _, item in + item.theme = TabBarTheme( + selectedTextColor: selectedTextColor, + unselectedTextColor: unselectedTextColor, + selectedIconColor: selectedIconColor, + unselectedIconColor: unselectedIconColor + ) + } + } } diff --git a/iOS/Sources/Beagle/Sources/Theme/ThemeTests.swift b/iOS/Sources/Beagle/Sources/Theme/ThemeTests.swift index 1164653a90..acd140f877 100644 --- a/iOS/Sources/Beagle/Sources/Theme/ThemeTests.swift +++ b/iOS/Sources/Beagle/Sources/Theme/ThemeTests.swift @@ -105,11 +105,15 @@ final class ThemeTests: XCTestCase { func test_tabViewWithStyle_shouldReturnAFunctionThatChangesTabViewStyle() { // Given + + let controller = BeagleControllerStub() + let renderer = BeagleRenderer(controller: controller) + let backgroundColor: UIColor = .clear let indicatorColor: UIColor = .blue let tabItem = TabBarItem(title: "Tab") let tabBar = TabBarUIComponent( - model: TabBarUIComponent.Model(tabIndex: 0, tabBarItems: [tabItem, tabItem]) + model: TabBarUIComponent.Model(tabIndex: 0, tabBarItems: [tabItem, tabItem], renderer: renderer) ) // When @@ -117,6 +121,6 @@ final class ThemeTests: XCTestCase { //Then XCTAssertEqual(backgroundColor, tabBar.backgroundColor) - XCTAssertEqual(indicatorColor, tabBar.containerIndicator.indicatorView.backgroundColor) + XCTAssertEqual(indicatorColor, tabBar.indicatorView.backgroundColor) } }