ZupIT / beagle

A framework to help implement Server-Driven UI in your apps natively.
https://docs.usebeagle.io
Apache License 2.0
685 stars 90 forks source link

Height of a ServerDrivenComponent in Swift(UI) #1832

Closed TakasurAtWork closed 1 year ago

TakasurAtWork commented 1 year ago

Description

image

Steps To Reproduce

Provide a detailed list of steps that reproduce the issue.

  1. Declare a JSON like this:
  2. var containerB: String { """ { "_beagleComponent_": "beagle:text", "text": "containerB", "style": { "backgroundColor": "#003870", "size": { "height": { "value": 200, "type": "REAL" } } } } """ } var containerA: String { """ { "_beagleComponent_": "beagle:text", "text": "containerA", "style": { "backgroundColor": "#003870", "size": { "height": { "value": 500, "type": "REAL" } } } } """ }
  3. Use this converter: ` struct BeagleToSwiftUI: UIViewRepresentable { typealias UIViewType = UIView typealias Context = UIViewRepresentableContext

    let uiView: UIView

    init(uiView: UIView) { self.uiView = uiView self.uiView.setContentHuggingPriority(.required, for: .vertical) print(uiView.frame.height) // always print zero here }

    func makeUIView(context: Context) -> UIView { return self.uiView }

    func updateUIView(_ uiView: UIView, context: Context) {

    } } `

  4. Use this to calculate height in SwiftUI

Expected Results

It should give same height in SwiftUI world as defined in JSON?

Code example, screenshot, or link to a repository:

Attached already.

Tiagoperes commented 1 year ago

Hello @TakasurAtWork. Beagle was not built with SwiftUI in mind, but since you can wrap the SwiftUI view in a UIKit component, this should be possible.

We have done some tests with this and we know there could be some layout problems when using this approach. We don't consider it to be a bug because, as said before, Beagle wasn't intended for SwiftUI.

In any case, @dantes-git will take a look into this, it might be a real bug or at least there might be an easy workaround.

dantes-git commented 1 year ago

hello @TakasurAtWork ,

I didn't understand the problem. I believe there is missing information to reproduce the bug. As I understand it, you want to use a BeagleView inside a SwiftUI view? What is the structure/code of the SwiftUI component? Which one is displaying the wrong height? Could you give us more information about the problem?

TakasurAtWork commented 1 year ago

@dantes-git thank you for a quick response, please refer to this gist to reproduce, as you will notice every container has 300 points height but when I check it with geometry reader it reports 334 to me on an iPhone 14 Pro.

TakasurAtWork commented 1 year ago

One more question, can we get size of an SDUI component defined in beagle? For some reasons I need to display the ServerDrivenComponent in a native view and I need exact height of a ServerDrivenComponent to display my other views correctly and do some animation stuff.

BeagleView(
        """
        {
            "_beagleComponent_": "beagle:container",
            "style": {
                "backgroundColor": "#CD5C5C",
                "size": {
                    "height": {
                        "value": 300,
                        "type": "REAL"
                    }
                }
            }
        }
        """
        ) { _ in }.frame.height

The above code snippet returns me a zero always. And in certain scenarios server might not even be defining height on a container.

dantes-git commented 1 year ago

Hello @TakasurAtWork ,

Sorry for the delay in replying. About your last question, there is no api that returns the size of the BeagleView, but in UIKit it is possible to inspect the frame and get the size of a view in the viewDidLayoutSubviews() function of your viewController. For BeagleView this method will be called twice, once when the component is initialized with zero size and a second time with the updated size after loading the content.

dantes-git commented 1 year ago

About the bug I managed to reproduce but I couldn't find the real cause of the problem. I noticed that it happens when there are three or more BeagleViews inside a ScrollView (could be a VStack too), and that some of them appear with the wrong size. It seems to me something related to the integration with the UIViewRepresentable, because I couldn't reproduce this problem of wrong sizes using only UIKit. An alternative to this problem would be to model your component that will implement the UIViewRepresentable to contain a UIScrollView and all BeagleViews, this way we would have only one UIViewRepresentable view in the hierarchy.

TakasurAtWork commented 1 year ago

Hello @TakasurAtWork ,

In UIKit it is possible to inspect the frame and get the size of a view in the viewDidLayoutSubviews() function of your viewController. For BeagleView this method will be called twice, once when the component is initialized with zero size and a second time with the updated size after loading the content.

Could you please share a small example of how that might work? Would be of great help.

dantes-git commented 1 year ago

In the example below we have a controller with a centralized beagleView of size 300x300. The viewDidLayoutSubviews() method will be called twice, both times with the correct size, but there are scenarios in which the size is initially zeroed.

import UIKit
import Beagle

class TestViewController: UIViewController {

  let beagleView = BeagleView(
    """
    {
      "_beagleComponent_": "beagle:container",
      "style": {
        "backgroundColor": "#CD5C5C",
        "size": {
          "height": {
            "value": 300,
            "type": "REAL"
          },
          "width": {
            "value": 300,
            "type": "REAL"
          }
        }
      }
    }
    """
  ) { _ in }

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .white
    setupViews()
  }

  func setupViews() {
    beagleView.translatesAutoresizingMaskIntoConstraints = false

    view.addSubview(beagleView)
    beagleView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    beagleView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
  }

  override func viewDidLayoutSubviews() {
    // This validation is necessary because when initializing the view, the size may be zero for some cases, such as the screen loaded from an endpoint
    if beagleView.frame.size != .zero {
      print(beagleView.frame.size)
    }
  }
}
Tiagoperes commented 1 year ago

@TakasurAtWork did these tips help you? Can we close this issue?

When Beagle is used with SwiftUI we'll try to find a setup that works, in this case, it would be to use a single BeagleView instead of multiple. We won't consider it to be a bug, because Beagle was never intended to work with SwiftUI. It's cool that we can, most of the times, make it work, but if in some case we can't, it's not a bug.

Having said this, SwiftUI, as well as Compose are very powerful tools for developing mobile apps. Not just powerful, they make the implementation of Server Driven UI much less complicated. For this reason, we are developing a new SDUI Framework with both in mind. We'll probably have a beta version before the end of the year.

TakasurAtWork commented 1 year ago

Sure, let me do it. Thank you for quick response and updates, I will be counting the days for beta release on my finger tips, it's really exciting. Can't wait to contribute to awesomeness.

TakasurAtWork commented 1 year ago

Okay, I know I have closed this one but is it possible to add ServerDriven components inside native UIViewController's view? Because I am having trouble. I definitely don't want to create views using JSON but instead host actual server driven components inside a native screen (UIViewController)

Tiagoperes commented 1 year ago

@TakasurAtWork why do you need to call Beagle's default components inside a native UIViewController? Can you give us a practical example of what you're trying to achieve?

TakasurAtWork commented 1 year ago

I have a specific design need that cannot be satisfied purely with beagle alone so I need to implement a custom widget that has 3 server driven components and I need to show 2 of them based on some maths. if a > 1 { show b } else { keep a } and c is always shown. And there are animations as well that need to go so I need the calculated height and not specified as specified height means server has to calculate the height and that is not optimal solution for all devices.

Also this link has missing reference for the thing I want to accomplish.

TakasurAtWork commented 1 year ago

A concept of what I want, the small design overlaps bigger but is hidden initially and shows when needed. A gif illustration of what I want but totally server driven.

image
Tiagoperes commented 1 year ago

@TakasurAtWork @dantes-git is currently checking this and will get back to you as soon as possible.

dantes-git commented 1 year ago

Hello @TakasurAtWork, I hope you are well.

Unfortunately, it is not possible to build this scenario today using only the Beagle, as it would need some resource in the ListView or GridView that would allow full control through the scrollview delegate. However, it is possible to build a hybrid scenario (native + sdui) where you would have the necessary control to manipulate a headerView or even an animation in the navigation bar based on the scroll offset. In this scenario we would have a UITableView and a UIView representing the header and, the first cell of the tableview would have its server driven content, so we can use the scrollView delegate to control the header according to the offset.

I created an example here with a simple implementation of this idea. In this example, the position of the header and its size can vary according to the offset value of the scroll, but it would not be difficult to include other interactions in the scrollViewDidScroll, such as for example, varying the alpha of the title of the navigationbar to appear as the content scrolls.

Below is a video showing the above example.

https://user-images.githubusercontent.com/6993763/208194799-91436218-41d7-4750-bd06-ae0f50f0dc5f.mp4

Note: This example is using version 2.1.1 of beagle-ios, in this version I fixed a simple bug related to the use of a ListView (through a BeagleView) inside a UITableView or UICollectionView.

TakasurAtWork commented 1 year ago

See, I don't need a navigation bar and I need three server driven components inside a native component and 2 of them show/hide based on scroll amount and for offset I need a way to get precise height of server driven header and inside header anything can go, another custom component as well.

Tiagoperes commented 1 year ago

@TakasurAtWork I'm sorry to read that, I really though this was what you were trying to do.

To be honest, this issue lost its track, I'm not even sure what the problem is right now.

You started by asking how to calculate the height of a component. @dantes-git showed you an example and you closed the issue. Then, you reopened the issue with this example and @dantes-git showed how to do it with a view that is part native, part server-driven. And now it seems like "getting the height of a component" is still a problem.

In this issue there's an example of how to get the height of server driven component. Does it work?

I definitely don't want to create views using JSON but instead host actual server driven components inside a native screen (UIViewController)

There's also this. If you don't want to use a JSON to create a server driven view, then this view is not server driven. Why would you use Beagle to create something that is not server driven?

This issue is very confusing. We really want to help, but I'd appreciate it if you could open another issue with a more clear description of your problem.

Tiagoperes commented 1 year ago

@TakasurAtWork I'm closing this issue due to lack of clarity. If you still have a problem, please open a new issue with a clear description of what you're trying to achieve and why all of our answers didn't help you.