How do i use strada-ios to convert my hamburger menu in my rails app to a native ios menu?

// Rails part
This is my _header.html..erb

<div class="flex justify-between items-center p-5"
     data-controller="header bridge--header"
     data-header-menu-outlet=".menu-class">
  <h1>Lego</h1>
  <div class="flex flex-col gap-1 cursor-pointer lg:hidden"
       data-header-target="hamburgerMenu"
       data-bridge--header-target="hamMenu"
       data-bridge-title="Menu"
       data-action="click->header#handleHamburgerMenu">
    <div class="h-[2px] w-[30px] bg-black"></div>
    <div class="h-[2px] w-[30px] bg-black"></div>
    <div class="h-[2px] w-[30px] bg-black"></div>
  </div>
</div>

This is my _menu.html.erb

<div class="p-5 mx-5 bg-gray-200 hidden menu-class"
     data-controller="menu" data-menu-target="menuContainer">
  <div class="flex flex-col gap-2 items-center justify-center">
    <%= link_to "Blogs", blog_posts_path %>
    <%= link_to "About" %>
    <%= link_to "Contact" %>
  </div>
</div>

this is my header bridge component

import { BridgeComponent, BridgeElement } from "@hotwired/strada";

export default class extends BridgeComponent {
  static component = "header";
  static targets = ["hamMenu"];

  connect() {
    console.log("connected header bridge");
  }

  hamburgerMenuTargetConnected(target) {
    const menu = new BridgeElement(target);
    const menuTitle = menu.title;

    this.send("connect", { menuTitle }, () => {
      console.log("target clicked", target);
      target.click();
    });
  }
}

// iOS part

This is my HeaderComponent.swift

import Foundation
import Strada
import UIKit

final class HeaderComponent: BridgeComponent {
    override class var name: String { "header" }
    
    override func onReceive(message: Message) {
        print("executed")
        switch message.event {
            case "connect":
                handleConnectEvent(message: message)
        default:
            break
        }
    }
    
    private func handleConnectEvent(message: Message) {
        guard let data: MessageData = message.data() else { return }
        configureBarButton(with: data.menuTitle)
    }
    
    private func configureBarButton(with title: String) {
      guard let viewController = delegate.destination as? UIViewController else { return }
      let item = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(performAction))
      viewController.navigationItem.leftBarButtonItem = item
        print("item", item)
    }

    // Reply to the originally received "connect" event message (without any new data).
    @objc func performAction() {
        reply(to: "connect")
    }
}


private extension HeaderComponent {
    struct MessageData: Decodable {
        let menuTitle: String
    }
}

This is my TurboWebViewController.swift

import UIKit
import Turbo
import Strada
import WebKit

final class TurboWebViewController: VisitableViewController, BridgeDestination {

    private lazy var bridgeDelegate: BridgeDelegate = {
        BridgeDelegate(location: visitableURL.absoluteString,
                       destination: self,
                       componentTypes: BridgeComponent.allTypes)
    }()

    // MARK: View lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        bridgeDelegate.onViewDidLoad()
        print("viewDidLoad")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        bridgeDelegate.onViewWillAppear()
        print("viewWillAppear")
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        bridgeDelegate.onViewDidAppear()
        print("viewDidAppear")
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        bridgeDelegate.onViewWillDisappear()
        print("viewWillDisappear")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        bridgeDelegate.onViewDidDisappear()
        print("viewDidDisappear")
    }

    // MARK: Visitable

    override func visitableDidActivateWebView(_ webView: WKWebView) {
        bridgeDelegate.webViewDidBecomeActive(webView)
        print("vistabaleDidActivateWebView")
    }

    override func visitableDidDeactivateWebView() {
        bridgeDelegate.webViewDidBecomeDeactivated()
        print("vistabaleDidDeaxctvate")
    }
}

This is my SceneDelegate.swift

import UIKit
import Turbo
import WebKit
import Strada

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    private lazy var navigationController = UINavigationController()
    private lazy var session: Session = {
        let config = WKWebViewConfiguration()
        let stradaSubstring = Strada.userAgentSubstring(for: BridgeComponent.allTypes)
        let userAgent = "Turbo Native iOS \(stradaSubstring)"
        config.applicationNameForUserAgent = userAgent

        let webView = WKWebView(frame: .zero, configuration: config)
        // Initialize Strada bridge.
        Bridge.initialize(webView)

        let session = Session(webViewConfiguration: config)
        session.delegate = self
        return session
    }()


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        self.window = UIWindow(windowScene: windowScene)
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
        
        visit()
    }
    
    private func visit() {
        let url = URL(string: "http://localhost:3001")!
        let controller = VisitableViewController(url: url)
        session.visit(controller, action: .advance)
        navigationController.pushViewController(controller, animated: true)
    }
}

extension SceneDelegate: SessionDelegate {
    
    func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
        let controller = TurboWebViewController(url: proposal.url)
        print("controller", controller)
        session.visit(controller, options: proposal.options)
        navigationController.pushViewController(controller, animated: true)
    }
    
    func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) {
        //TODO: Handle Errors
    }

    
    func sessionWebViewProcessDidTerminate(_ session: Session) {
        //TODO: Handle dead web view
    }
    
    
}

So basically my expectation is to see a native menu with the links , i am not sure if i am doing it correct because i have no prior experience with swift before.

It would be great , if anyone helps me out here :pray:

1 Like

Hey your code looks great and everything should be setup to create a functioning native experience using Strada. The reason why your tab bar button is not showing up in your IOS app is because inside of your HeaderComponent.swift you are setting the navbar item correctly on the viewController but you never defined viewController in that class so there’s no reference of it

You can fix this by adding this to the top of the class

 private var viewController: UIViewController? {
        delegate.destination as? UIViewController
    }

Then you will be able to access viewController from anywhere inside of your HeaderComponent

Thank you @yunggindigo for replying, let me try this out , will reach out here if i need more help.

Edit: I tried as you suggested , and my HeaderComponent.swift looks like this now

import Foundation
import Strada
import UIKit


final class HeaderComponent: BridgeComponent {

    override class var name: String { "header" }
    
    override func onReceive(message: Message) {
        print("executed")
        switch message.event {
            case "connect":
                handleConnectEvent(message: message)
        default:
            break
        }
    }
    
   // Added here
    private var viewController: UIViewController? {
        delegate.destination as? UIViewController
    }
    
    private func handleConnectEvent(message: Message) {
        guard let data: MessageData = message.data() else { return }
        configureBarButton(with: data.menuTitle)
    }
    
    private func configureBarButton(with title: String) {
      let item = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(performAction))
      // using it here
      viewController?.navigationItem.leftBarButtonItem = item
      print("item", item)
    }

    // Reply to the originally received "connect" event message (without any new data).
    @objc func performAction() {
        reply(to: "connect")
    }
}


private extension HeaderComponent {
    struct MessageData: Decodable {
        let menuTitle: String
    }
}

1 Like