Slide 1

Slide 1 text

(PJOH/BUJWF 5VSCP/BUJWF4USBEB 3VCZ.FFUVQ'SBOLGVSU

Slide 2

Slide 2 text

• "QQ*OUSP • 5VSCP/BUJWF • 4USBEB

Slide 3

Slide 3 text

"QQ*OUSP

Slide 4

Slide 4 text

"QQ*OUSP %FWFMPQFS)BQQJOFTT 3FTQPOTJWFOFTT CPEZ SFQMBDFNFOU BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH 5VSCP 'SBNFT 5VSCP 4USFBN BDUJPOT 5VSCP

Slide 5

Slide 5 text

"QQ*OUSP # view (e.g., show) <%= turbo_stream_from @model %> # model.rb broadcasts_refreshes # child.rb belongs_to :model, touch: true broadcasts_refreshes # application.html.erb <%= turbo_refreshes_with method: :morph %> <%= turbo_refresh_scroll_tag :preserve %> # Gemfile gem 'turbo-rails', '~> 2.0.0.pre.beta' # use Turbo8

Slide 6

Slide 6 text

5VSCP/BUJWF

Slide 7

Slide 7 text

• 0OF"QQUP$POUSPM5IFN"MM • 8FC • "OESPJE • J04 • 7FSTJPODIBOHFBQQTUPSFBDDFQUBODFQSPDFTT • 4QFDJGJDBQQWJFXT • &WFOOPOOBUJWFDPEFSTDBOEPJU • 0VUPGUIFCPYXJUIUVSCPSBJMTHFN • UVSCPJPTQBDLBHF 5VSCP/BUJWF

Slide 8

Slide 8 text

5VSCP/BUJWF BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH

Slide 9

Slide 9 text

5VSCP/BUJWF BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH 5VSCP/BUJWF

Slide 10

Slide 10 text

%PXOMPBE9DPEF J04EFWFMPQNFOU *OTUBMMUVSCPJPTQBDLBHF IUUQTHJUIVCDPNIPUXJSFEUVSCPJPT %0/& 5VSCP/BUJWF $SFBUFJ04"QQ %PXOMPBE9DPEF J04EFWFMPQNFOU *OTUBMMUVSCPJPTQBDLBHF IUUQTHJUIVCDPNIPUXJSFEUVSCPJPT .BLF"EBQUJPOT

Slide 11

Slide 11 text

5VSCP/BUJWF $SFBUFJ04"QQ # SceneDelegate.swift # DELETE EVERYTHING!

Slide 12

Slide 12 text

5VSCP/BUJWF $SFBUFJ04"QQ // SceneDelegate.swift import UIKit import Turbo import WebKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { }

Slide 13

Slide 13 text

5VSCP/BUJWF $SFBUFJ04"QQ "EEJOHCBTJDWBSJBCMFT // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? private let navigationController = UINavigationController() }

Slide 14

Slide 14 text

5VSCP/BUJWF $SFBUFJ04"QQ "EEJOHBTFTTJPO 5IFPCKFDUUIBUWJTJUTTDSFFOT QPQTUIFNPOUPUIFIJFSBSDIZ BEBQUFSUIBUIPPLTJOUPUIF+4GPS5VSCPKT // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables private lazy var session: Session = { let configuration = WKWebViewConfiguration() let session = Session(webViewConfiguration: configuration) session.delegate = self return session }() }

Slide 15

Slide 15 text

5VSCP/BUJWF $SFBUFJ04"QQ -BVODIJOHUIFBQQ // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables // Our session variable 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() } }

Slide 16

Slide 16 text

5VSCP/BUJWF $SFBUFJ04"QQ &OIBODFUIF7JTJUBCMF7JFX$POUSPMMFS 7JTJUUIFXFCBQQPOJ04BQQMBVODI // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables // Our session variable // scene function – to launch the app private func visit() { let url = URL(string: "http://localhost:3000")! let controller = VisitableViewController(url: url) session.visit(controller, action: .advance) // push contoller (aka screen) onto the stack navigationController.pushViewController(controller, animated: true) } }

Slide 17

Slide 17 text

5VSCP/BUJWF $SFBUFJ04"QQ 4DFOF%FMFHBUFDPOGPSNTUPUIF4FTTJPO%FMFHBUFQSPUPDPM -FUVTLOPXXIFOBMJOLJTDMJDLFE // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Some code } extension SceneDelegate: SessionDelegate { func session(_ session: Session, didProposeVisit proposal: VisitProposal) { let controller = VisitableViewController(url: proposal.url) session.visit(controller, options: proposal.options navigationController.pushViewController(controller, animated: true) } }

Slide 18

Slide 18 text

5VSCP/BUJWF $SFBUFJ04"QQ 4DFOF%FMFHBUFDPOGPSNTUPUIF4FTTJPO%FMFHBUFQSPUPDPM -FUVTLOPXXIFOBMJOLJTDMJDLFE // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Some code } extension SceneDelegate: SessionDelegate { // Our code from 5 seconds ago func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) { // Some code } func sessionWebViewProcessDidTerminate(_ session: Session) { // Some code } }

Slide 19

Slide 19 text

5VSCP/BUJWF $SFBUFJ04"QQ 5IF4DFOF%FMFHBUF // SceneDelegate.swift // imports // class SceneDelegate // Our variables // Our session variable // scene function – to launch the app // visit function – to connect native to the web // extension SceneDelegate: SessionDelegate // session function to visit links // session function for failed requests // sessionWebViewProcessDidTerminate function OBUJWFBQQà

Slide 20

Slide 20 text

5VSCP/BUJWF $SFBUFJ04"QQ 8FCWT/BUJWF"QQ

Slide 21

Slide 21 text

5VSCP/BUJWF $SFBUFJ04"QQ J04"QQ // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables private lazy var session: Session = { let configuration = WKWebViewConfiguration() let session = Session(webViewConfiguration: configuration) session.delegate = self return session }() }

Slide 22

Slide 22 text

5VSCP/BUJWF $SFBUFJ04"QQ J04"QQ // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables private lazy var session: Session = { let configuration = WKWebViewConfiguration() configuration.applicationNameForUserAgent = "Turbo Native iOS" let session = Session(webViewConfiguration: configuration) session.delegate = self return session }() }

Slide 23

Slide 23 text

5VSCP/BUJWF $SFBUFJ04"QQ 3BJMT"QQ # application.css .turbo-native-hidden { display: block !important; } .turbo-native-shown { display: none !important; }

Slide 24

Slide 24 text

5VSCP/BUJWF $SFBUFJ04"QQ 3BJMT"QQ # native.css .turbo-native-hidden { display: none !important; } .turbo-native-shown { display: block !important; } # application.html.erb <%= stylesheet_link_tag 'application', media: 'all', 'data-turbo- track': 'reload’ %> <%= stylesheet_link_tag 'native', media: 'all', 'data-turbo-track': 'reload' if turbo_native_app? %>

Slide 25

Slide 25 text

5VSCP/BUJWF $SFBUFJ04"QQ 3BJMT"QQ # shared/_navbar.html.erb
<%= render 'shared/web_navbar' %>
<%= render 'shared/native_navbar' %>
OBUJWFXFCBQQà

Slide 26

Slide 26 text

5VSCP/BUJWF $SFBUFJ04"QQ 8FCWT"QQ 'PS'SFF OBUJWFXFCBQQà

Slide 27

Slide 27 text

4USBEB

Slide 28

Slide 28 text

• .PSFOBUJWFFMFNFOUT • TUBEBJPTQBDLBHF • 6TJOHTUJNVMVT • lDMJDLTVOEFSUIFIPPEz • /PUBTTUSBJHIUGPSXBSE 4USBEB

Slide 29

Slide 29 text

4USBEB BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH 5VSCP/BUJWF

Slide 30

Slide 30 text

4USBEB BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH 5VSCP /BUJWF 4USBEB

Slide 31

Slide 31 text

"EBQU3BJMTBQQ QJOTUSBEBQBDLBHF 8SJUFTUJNVMVTDPOUSPMMFS $POOFDUTUJNVMVTDPOUSPMMFSIJEFUIFFMFNFOU "EBQUJ04BQQ *OTUBMMTUSBEBJPT IUUQTHJUIVCDPNIPUXJSFETUSBEBJPT "EBQU8,8FC7JFX$POGJHVSBUJPO $SFBUF5VSCP8FC7JFX$POUSPMMFS "EBQU4DFOF%FMFHBUF $SFBUF$PNQPOFOU 4USBEB OBUJWFBQQà

Slide 32

Slide 32 text

4USBEB 3BJMT"QQ 1JOTUSBEBQBDLBHF # config/importmap.rb pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2 pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@hotwired/strada", to: "@hotwired--strada.js" # @1.0.0 # by running `./bin/importmap pin @hotwired/stimulus @hotwired/strada

Slide 33

Slide 33 text

4USBEB 3BJMT"QQ 8SJUFTUJNVMVTDPOUSPMMFS # javascript/controllers/bridge/form_controller.js import { BridgeComponent, BridgeElement } from "@hotwired/strada" export default class extends BridgeComponent { static component = "form" static targets = ["submit"] connect() { super.connect() this.#notifyBridgeOfConnect() } #notifyBridgeOfConnect() { const submitButton = new BridgeElement(this.submitTarget) const submitTitle = submitButton.title this.send("connect", {submitTitle}, () => { this.submitTarget.click() }) } }

Slide 34

Slide 34 text

4USBEB 3BJMT"QQ $POOFDUTUJNVMVTDPOUSPMMFS

Slide 35

Slide 35 text

4USBEB J04"QQ "EBQU8,8FC7JFX$POGJHVSBUJPO // WKWebViewConfiguration+App.swift import Foundation import WebKit import Strada enum WebViewPool { static var shared = WKProcessPool() } // More code soon

Slide 36

Slide 36 text

4USBEB J04"QQ "EBQU8,8FC7JFX$POGJHVSBUJPO // WKWebViewConfiguration+App.swift // Code from 5 seconds ago extension WKWebViewConfiguration { static var appConfiguration: WKWebViewConfiguration { let stradaComponents = [FormComponent.self] let stradaSubstring = Strada.userAgentSubstring(for: stradaComponents) let userAgent = "Turbo Native iOS \(stradaSubstring)" let configuration = WKWebViewConfiguration() configuration.processPool = WebViewPool.shared configuration.applicationNameForUserAgent = userAgent configuration.defaultWebpagePreferences?.preferredContentMode = .mobile return configuration } }

Slide 37

Slide 37 text

4USBEB J04"QQ $SFBUF5VSCP8FC7JFX$POUSPMMFS // 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: [FormComponent.self]) }() // Lots of more code for lifecycle & visitable }

Slide 38

Slide 38 text

4USBEB J04"QQ "EBQU4DFOF%FMFHBUF // SceneDelegate.swift import UIKit import Turbo import Strada import WebKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { // some variables // use the new config! let webView = WKWebView(frame: .zero, configuration: .appConfiguration) // More changes coming up

Slide 39

Slide 39 text

4USBEB J04"QQ "EBQU4DFOF%FMFHBUF // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables (incl. webView) private lazy var session: Session = { let configuration = WKWebViewConfiguration() let session = Session(webViewConfiguration: configuration) session.delegate = self return session }() }

Slide 40

Slide 40 text

4USBEB J04"QQ "EBQU4DFOF%FMFHBUF // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables (incl. webView) private lazy var session: Session = { let session = Session(webView: webView) Bridge.initialize(webView) session.delegate = self return session }() }

Slide 41

Slide 41 text

4USBEB J04"QQ "EBQU4DFOF%FMFHBUF // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables (incl. WebView) // Our new session variable // scene function – to launch the app private func visit() { let url = URL(string: "http://localhost:3000")! let controller = VisitableViewController(url: url) session.visit(controller, action: .advance) navigationController.pushViewController(controller, animated: true) } }

Slide 42

Slide 42 text

4USBEB J04"QQ "EBQU4DFOF%FMFHBUF // SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Our variables (incl. WebView) // Our new session variable // scene function – to launch the app private func visit() { let url = URL(string: "http://localhost:3000")! let controller = TurboWebViewController(url: url) session.visit(controller, action: .advance) navigationController.pushViewController(controller, animated: true) } }

Slide 43

Slide 43 text

4USBEB J04"QQ $SFBUF$PNQPOFOU // Components/FormComponent.swift import Foundation import Strada import UIKit final class FormComponent: BridgeComponent { // name of the component as in the Stimulus controller override class var name: String { "form" } private weak var button: UIBarButtonItem? // who is in charge of rendering this screen right now private var viewController: UIViewController? { delegate.destination as? UIViewController } }

Slide 44

Slide 44 text

4USBEB J04"QQ $SFBUF$PNQPOFOU // Components/FormComponent.swift final class FormComponent: BridgeComponent { // our variables override func onReceive(message: Message) { guard let viewController = viewController else { return } guard let data: MessageData = message.data() else { return } // click on the element that this component was wired up to let action = UIAction(title: data.submitTitle) { [unowned self] _ in self.reply(to: "connect") } // define native element let button = UIBarButtonItem(primaryAction: action) // use native button viewController.navigationItem.rightBarButtonItem = button self.button = button } }

Slide 45

Slide 45 text

4USBEB J04"QQ $SFBUF$PNQPOFOU // Components/FormComponent.swift final class FormComponent: BridgeComponent { // our variables // func onReceive } // data that matches to what we send as data within stimulus controller private extension FormComponent { struct MessageData: Decodable { let submitTitle: String }

Slide 46

Slide 46 text

4USBEB 8FCWT/BUJWF"QQ Web Button Response clicks

Slide 47

Slide 47 text

4USBEB 8FCWT/BUJWF"QQ Web Button Response Web App Native App Native Button Native Button Web Button Response clicks instructs actually clicks shows user clicks

Slide 48

Slide 48 text

4VNNBSZ

Slide 49

Slide 49 text

4VNNBSZ BEBQUFEGSPNIUUQTEFWTJHOBMTDPNBIBQQJFSIBQQZQBUIJOUVSCPXJUINPSQIJOH 5VSCP /BUJWF 4USBEB • (PJOHOBUJWFJOXFFLT OPUNPOUIT • 5VSCP/BUJWFCBTJDOBUJWFBQQ • 4USBEBGBODJGZOBUJWFBQQT • 0OFEFWDBOEPJUBMM BMTPOBUJWF

Slide 50

Slide 50 text

4VNNBSZ IUUQTHJUIVCDPNLFWLFWQFSTPOBM@LOPXMFEHF@CBTF