Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwiftUI Support/Example? #281

Open
zntfdr opened this issue Oct 26, 2019 · 19 comments
Open

SwiftUI Support/Example? #281

zntfdr opened this issue Oct 26, 2019 · 19 comments

Comments

@zntfdr
Copy link
Contributor

zntfdr commented Oct 26, 2019

Update May 2021:
Another approach is to use UITableView/UICollectionView and then embed the SwiftUI views as cells, Noah Gilmore has a great article showing how to do so.

Update May 2020:
The code below now works for static content. I assume we shouldn't try to get the hidden UIKit scroll view used within SwiftUI, because that can easily break at any iOS update, therefore for the moment we should use components like VStack but not List or ScrollView.

I'm trying to make the library work with SwiftUI, and was wondering if you have had any success so far.

You can find my attempt here:

Steps:

  1. download FloatingPanel project
  2. open maps example, set target iOS 13
  3. open map ViewController.swift
  4. replace ViewController.swift content with:
Click here to see the source code
import UIKit
import MapKit
import FloatingPanel
import SwiftUI

class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate, FloatingPanelControllerDelegate {
    var fpc: FloatingPanelController!
    var hostViewController: UIScrollViewController<MyList>!

    @IBOutlet weak var mapView: MKMapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // Initialize FloatingPanelController
        fpc = FloatingPanelController()
        fpc.delegate = self

        // Initialize FloatingPanelController and add the view
        fpc.surfaceView.backgroundColor = .white
        fpc.surfaceView.cornerRadius = 9.0
        fpc.surfaceView.shadowHidden = false

        hostViewController = UIScrollViewController(rootView: MyList())

        // Set a content view controller
        fpc.set(contentViewController: hostViewController)
        fpc.track(scrollView: hostViewController.scrollView)

        setupMapView()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //  Add FloatingPanel to a view with animation.
        fpc.addPanel(toParent: self, animated: true)
    }

    func setupMapView() {
        let center = CLLocationCoordinate2D(latitude: 37.623198015869235,
                                            longitude: -122.43066818432008)
        let span = MKCoordinateSpan(latitudeDelta: 0.4425100023575723,
                                    longitudeDelta: 0.28543697435880233)
        let region = MKCoordinateRegion(center: center, span: span)
        mapView.region = region
        mapView.showsCompass = true
        mapView.showsUserLocation = true
        mapView.delegate = self
    }

    // MARK: FloatingPanelControllerDelegate

    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
        switch newCollection.verticalSizeClass {
        case .compact:
            fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
            fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
            return SearchPanelLandscapeLayout()
        default:
            fpc.surfaceView.borderWidth = 0.0
            fpc.surfaceView.borderColor = nil
            return nil
        }
    }
}

public class SearchPanelLandscapeLayout: FloatingPanelLayout {
    public var initialPosition: FloatingPanelPosition {
        .tip
    }

    public var supportedPositions: Set<FloatingPanelPosition> {
        [.full, .tip]
    }

    public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full: return 16.0
        case .tip: return 69.0
        default: return nil
        }
    }

    public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
        [
            surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
            surfaceView.widthAnchor.constraint(equalToConstant: 291),
        ]
    }

    public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
        0
    }
}

struct MyList: View {
    var body: some View {
        VStack {
            ForEach((1...100).reversed(), id: \.self) { number in
                HStack {
                    Text("This is SwiftUI \(number)")
                    Spacer()
                }
                .padding()
            }
        }
    }
}

class UIScrollViewController<Content: View>: UIViewController {
    weak var scrollView: UIScrollView!
    private let hostingController: UIHostingController<Content>

    init(rootView: Content) {
        hostingController = UIHostingController<Content>(rootView: rootView)
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        // Add Scroll View.
        let uiScrollView = UIScrollView()
        self.scrollView = uiScrollView
        view = uiScrollView

        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        uiScrollView.addSubview(hostingController.view)
        let constraints = [
            hostingController.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
            hostingController.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
        ]
        uiScrollView.addConstraints(constraints)

        super.viewDidLoad()
    }
}

Current working example:

Screen Recording 2020-05-01 at 10 44 03 AM 2020-05-01 10_52_29

Thank you in advance!
Federico

@scenee
Copy link
Owner

scenee commented Oct 27, 2019

Thank you for your attempt for Swift UI!
I'm sorry I haven't yet tried the lib with SwiftUI because I'm working recently for v2 which beta I'm going to release next month.(v2 will resolve many issues in GitHub). But I would like to try your example and take a look at scroll tracking problem later.

@zntfdr
Copy link
Contributor Author

zntfdr commented Oct 27, 2019

Hi @scenee,
thank you very much 😊

@sipersso
Copy link

@zntfdr Where you able to resolve this? I am facing the same issue here. @scenee is v2 still in pogress?

@scenee
Copy link
Owner

scenee commented Feb 10, 2020

@sipersso Yes it is. But now I’m focusing to fix some issue on v1.x and I would like to release v2 on March.

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 11, 2020

@sipersso I've tried multiple ways, such as embedding SwiftUI views into auto-resizing UITableViewCells, so far no result was acceptable.

If you really want to use SwiftUI today, my suggestion is to implement this floating panel yourself (I haven't done it): with a geometry reader and a gesture recogniser you should be able to accomplish everything you need.

Thank you @scenee for all your support ❤️

@sipersso
Copy link

sipersso commented Feb 11, 2020

I agree with, it is a great library @scenee.

@zntfdr I did get scroll tracking to work with SwiftUI. The trick, which is missing from your example is to set the content size of the scrollView to the size of the hostingController view.

let width = UIScreen.main.bounds.width
let size = hostingController.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
hostingController.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
scrollView.addSubview(hostingController.view)
scrollView.contentSize = CGSize(width: width, height: size.height)

The downside is that if you the swiftui view is resized you would have to reset the contentsize and I am not sure how to get notified to do that. Other than that it seems to work pretty great and it does allow for interaction in SwiftUI as well as track scrolling.

I also did try to recreate the Panel in SwiftUI, but none of my attempts where nearly as good as the component that @scenee has built. Using the hybrid approach, UIKit panel, but SwiftUI content, makes it really easy to use from a UIKit UIViewController ;)

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 12, 2020

@sipersso

@zntfdr I did get scroll tracking to work with SwiftUI. The trick, which is missing from your example is to set the content size of the scrollView to the size of the hostingController view.

let width = UIScreen.main.bounds.width
let size = hostingController.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
hostingController.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
scrollView.addSubview(hostingController.view)
scrollView.contentSize = CGSize(width: width, height: size.height)

The downside is that if you the swiftui view is resized you would have to reset the contentsize and I am not sure how to get notified to do that. Other than that it seems to work pretty great and it does allow for interaction in SwiftUI as well as track scrolling.

Very cool! Many thanks @sipersso, I will experiment some more as soon as possible 😃

I also did try to recreate the Panel in SwiftUI, but none of my attempts where nearly as good as the component that @scenee has built. Using the hybrid approach, UIKit panel, but SwiftUI content, makes it really easy to use from a UIKit UIViewController ;)

That's why I didn't try to reimplement the whole thing myself: I want to keep the feeling of a normal scroll view (with the iOS default pull elasticity, etc) 👍

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 17, 2020

@sipersso I haven't managed to get the scroll tracking to work with SwiftUI, can you please share your code?

@sipersso
Copy link

I got ideas from this
https://gist.github.com/timothycosta/0d8f64afeca0b6cc29665d87de0d94d2

But had to adapt it a little by setting the content size of the scrollview.

class  UIScrollViewViewController: UIViewController {

    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        return v
    }()

    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(Text("Test")))

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.addSubview(self.scrollView)
        self.pinEdges(of: self.scrollView, to: self.view)
        self.hostingController.view.removeFromSuperview()
        self.hostingController.willMove(toParent: self)
        let width = UIScreen.main.bounds.width
        let size = hostingController.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
        hostingController.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
        scrollView.addSubview(hostingController.view)
        scrollView.contentSize = CGSize(width: width, height: size.height)
        self.hostingController.didMove(toParent: self)
    }

    func pinEdges(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
            viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
            viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
            viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
            viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
        ])
    }

}

In my floatingpanelcontroller subclass I create an instance of my UIScrollViewViewController and then use this in ViewDidLoad

scrollViewViewController.hostingController = UIHostingController(rootView: AnyView(mySwiftuiView))
set(contentViewController: scrollViewViewController)
track(scrollView: scrollViewViewController.scrollView)

Note that this won't work if your swiftui view changes height. You would have to ajust the contentsize of the scrollview every time this happens.

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 18, 2020

Thank you @sipersso, I still can't get the scroll tracking even with your code snippets:
but it might be me just getting frustrated and giving up too early 😝

Might try again in a few days 👍

A full working example similar to the one I posted on the first post would be greatly appreciated 🚀

@sipersso
Copy link

Hi @zntfdr! Unfortunately I can't share much more than I already have. The rest of my code is non-generic and would be quite an effort for me to provide a more detailed/standalone example

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 18, 2020

No worries @sipersso, many thanks for the support! 🤗

@ramunasjurgilas
Copy link

@zntfdr Here is working example:


import UIKit
import MapKit
import FloatingPanel
import SwiftUI

class MyFpc: FloatingPanelController {
    var scrollViewViewController = UIScrollViewViewController()

    override func viewDidLoad() {
        super.viewDidLoad()
        scrollViewViewController.hostingController = UIHostingController(rootView: vvv)
        set(contentViewController: scrollViewViewController)
        track(scrollView: scrollViewViewController.scrollView)
    }

    var vvv: AnyView {
        let result1 = VStack {
            Text("flk")
            Text("flk")
            Text("flk")
            Text("flk")
            List {
                Text("flk")
                Text("flk")
                Text("flk")
                Text("flk")
            }
        }
        let result = List {
          ForEach((1...100).reversed(), id: \.self) {
              Text("\($0)")
          }
        }
        .frame(width: 100, height: 800, alignment: .center)
        return AnyView(result)
    }
}

class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate, FloatingPanelControllerDelegate {
    var fpc: MyFpc!
    var hostViewController: UIScrollViewController<MyList>!

    @IBOutlet weak var mapView: MKMapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // Initialize FloatingPanelController
        fpc = MyFpc()
        fpc.delegate = self

        // Initialize FloatingPanelController and add the view
        fpc.surfaceView.backgroundColor = .clear
        if #available(iOS 11, *) {
            fpc.surfaceView.cornerRadius = 9.0
        } else {
            fpc.surfaceView.cornerRadius = 0.0
        }
        fpc.surfaceView.shadowHidden = false

        hostViewController = UIScrollViewController(rootView: MyList())

        // Set a content view controller
        fpc.set(contentViewController: hostViewController)
        fpc.track(scrollView: hostViewController.scrollView)

        setupMapView()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //  Add FloatingPanel to a view with animation.
        fpc.addPanel(toParent: self, animated: true)
    }

    func setupMapView() {
        let center = CLLocationCoordinate2D(latitude: 37.623198015869235,
                                            longitude: -122.43066818432008)
        let span = MKCoordinateSpan(latitudeDelta: 0.4425100023575723,
                                    longitudeDelta: 0.28543697435880233)
        let region = MKCoordinateRegion(center: center, span: span)
        mapView.region = region
        mapView.showsCompass = true
        mapView.showsUserLocation = true
        mapView.delegate = self
    }

    // MARK: FloatingPanelControllerDelegate

    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
        switch newCollection.verticalSizeClass {
        case .compact:
            fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
            fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
            return SearchPanelLandscapeLayout()
        default:
            fpc.surfaceView.borderWidth = 0.0
            fpc.surfaceView.borderColor = nil
            return nil
        }
    }
}

public class SearchPanelLandscapeLayout: FloatingPanelLayout {
    public var initialPosition: FloatingPanelPosition {
        return .tip
    }

    public var supportedPositions: Set<FloatingPanelPosition> {
        return [.full, .tip]
    }

    public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full: return 16.0
        case .tip: return 69.0
        default: return nil
        }
    }

    public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
        if #available(iOS 11.0, *) {
            return [
                surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
                surfaceView.widthAnchor.constraint(equalToConstant: 291),
            ]
        } else {
            return [
                surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
                surfaceView.widthAnchor.constraint(equalToConstant: 291),
            ]
        }
    }

    public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
        return 0.0
    }
}

struct MyList: View {
  var body: some View {
    List {
      ForEach((1...100).reversed(), id: \.self) {
          Text("\($0)")
      }
    }
  }
}

class UIScrollViewController<Content: View>: UIViewController {
  weak var scrollView: UIScrollView!
  private let hostingController: UIHostingController<Content>

  init(rootView: Content) {
    hostingController = UIHostingController<Content>(rootView: rootView)
    super.init(nibName: nil, bundle: nil)
  }

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    // Add Scroll View.
    let scrollView = UIScrollView()
    self.scrollView = scrollView
    view = scrollView

    scrollView.addSubview(hostingController.view)
    hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    // If we un-comment the following line, fpc will correctly track the
    // `scrollView`, but no iteraction would possible with SwiftUI's `View`.
    // hostingController.view.isUserInteractionEnabled = false

    super.viewDidLoad()
  }
}

class  UIScrollViewViewController: UIViewController {

    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        return v
    }()

    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(Text("Test")))

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addSubview(self.scrollView)
        self.pinEdges(of: self.scrollView, to: self.view)
        self.hostingController.view.removeFromSuperview()
        self.hostingController.willMove(toParent: self)
        let width = UIScreen.main.bounds.width
        let size = hostingController.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
        hostingController.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
        scrollView.addSubview(hostingController.view)
        scrollView.contentSize = CGSize(width: width, height: size.height)
        self.hostingController.didMove(toParent: self)
    }

    func pinEdges(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
            viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
            viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
            viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
            viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
        ])
    }

}


@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 21, 2020

Many thanks @ramunasjurgilas 😃
Will check it out over the weekend! 🚀

@scenee
Copy link
Owner

scenee commented Feb 22, 2020

Thank you so much for your work, @zntfdr, @sipersso and @ramunasjurgila 👍🚀 Unfortunately now I don't have enough time to take care of this, but I would do it after releasing v2 😌

@zntfdr
Copy link
Contributor Author

zntfdr commented Feb 22, 2020

@ramunasjurgilas I'm afraid that your example doesn't work.

As you can see from the video below, scrolling is tracked only when the scroll is outside of the SwiftUI view.

Screen Recording 2020-02-22 at 3 09 23 PM 2020-02-22 15_11_02

Thank you for trying! 😊

@sipersso
Copy link

@zntfdr just checked the example you provided. You are using List and this has it's own scrollview, which explains why the scrolltracking isn't working. Try using a VStack instead in SwiftUI.

@zntfdr
Copy link
Contributor Author

zntfdr commented Jul 25, 2021

Update: I've added a SwiftUI implementation in #481.

While the example shows a mix of UIKit and SwiftUI, if you use something like SwiftUI-Introspect, you can build your panel content entirely in SwiftUI, e.g.:

ContentView()
  .floatingPanel { proxy in
    ScrollView {
      // .. your content here.
    }
    .introspectScrollView { scrollView in
      proxy.track(scrollView: scrollView)
    }
  }

If you'd like to try it out in your app, copy the FloatingPanel group from the example into your project, and you'll be able to use it as if it was part of the FloatingPanel library:

Screen Shot 2021-07-25 at 17 32 51

Please try it out and let me know how it goes 😃

scenee pushed a commit that referenced this issue Sep 25, 2021
Partially solves #281. This adds a new example app which mimics the Maps.app, written in SwiftUI. The code works for iOS 13+, however:
* the project has been created with Xcode 13
* the project uses the SwiftUI lifecycle (iOS 14+)

The source code in Examples/Maps-SwiftUI/Maps/FloatingPanel is ready to move into the library, but there is an issue on SwiftUI’s environment propagation into FloatingPanel. SwiftUI’s environment is propagated to all subviews. However FloatingPanel is not a subview, but a new view controller in the screen (and not a child view controller).

It’s possible to lead behaviors unexpected by SwiftUI users so that this is merged as a sample code until it will be resolved.
@leonboe1
Copy link

For me, this does not work if the content height is dynamic. Anyone else has the same problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants