GithubHelp home page GithubHelp logo

turbo-ios's People

Contributors

bramjetten avatar conormuirhead avatar coppolafab avatar davegudge avatar frederfred avatar ghiculescu avatar indigotechtutorials avatar jackloughran avatar jayohms avatar joemasilotti avatar julianrubisch avatar justinlyman avatar leonvogt avatar olivaresf avatar p8 avatar pfeiffer avatar quaasi avatar railsbob avatar sethherr avatar shpigford avatar solilin avatar sstephenson avatar svara avatar tapster avatar zachwaugh avatar zoejessica avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

turbo-ios's Issues

Is this library going to be maintained?

Given the recent civil war at Basecamp, and the fact that basically nothing has happened in this repo for a couple of months now (when it's new and you'd think would be getting lots of love), should we assume this library is DOA?

Same question goes for the wider Hotwire ecosystem really.

How to load PathConfiguration from Data source?

I'm trying to write a test where I inject some path configuration and then assert that the app provides correctly when it encounters a given rule. I read through the PathConfiguration code and saw that it can be loaded from a Data source, so I tried something like this:

let config = """
{
    "rules": [
        {
            "patterns": [
                "/example"
            ],
            "properties": {
                "myProperty": "myValue"
            }
        }
    ]
}
"""
let configData = config.data(using: .utf32)
let pathConfig = PathConfiguration(sources: [.data(configData)])

When I run the debugger, I see that my resulting configuration source is always marked as invalid. In the code I see that path configuration is invalid if it doesn't have a rules key, but nothing else jumped out at me. I reviewed the docs and saw that there are examples for the other two sources, but not for data.

What am I missing? Thanks!

Revisiting the app gives a blank page

Hey,

love the project, and everything is working flawlessly except that whenever a user return to the app after a few minutes the app is blank. Swiping back will bring it back alive again, any clues on how to fix this?

Cheers,
Chris!

request.referer is always null

I'm trying to set a request.referer on my edit action so I can redirect users on iOS to where they came from (index or show)

def edit
    session[:return_to] = request.referrer
end

def update
    ....
    redirect_to(session[:return_to] || default_path)
end

Yet this is always null? Am I missing something in how I should handle this? I also tried setting session[:return_to] = request.original_url on index & show, and while this works it causes other issues I'd rather avoid.

Modal -> form submission flow is broken on iOS 15

Steps to reproduce

  1. Open Demo app on iOS 15
  2. Tap "Load a webpage modally"
  3. Tap "Submit Form"

Expected behavior: Page loads and renders without issue.

Actual behavior: Loading indicator hangs and page does not render. See screenshot below.

image

Note that following the steps a second time "fixes" the issue and the page is loaded and rendered correctly.

Preload the first page/view during the launchscreen

Hi,

Is there a way, a trick, to preload the first view controller during the launchscreen ?
Right now, launchscreen happen, it's great, but then a white loading screen comes...

Thanks for your recommendations.

Cordialement,

session.reload() may open the page in external browser

My app has 4 Turbo WebView tabs with bottom tab ui,
After the sign-in, I reload all the pages to refresh the auth info.
Then the page is opened in external safari browser.

I think the following method is suspicious. (in Session.swift)
it returns true although the url was not intended to open externally.

        var shouldOpenURLExternally: Bool {
            let type = navigationAction.navigationType
            return type == .linkActivated || (isMainFrameNavigation && type == .other)
        }

Is there a reason the `VisitableViewController` doesn't install the view in the safe area?

Disclaimer: I'm pretty new to Swift, and am not quite sure this even the right thing to do.

I noticed that VisitableViewController #installVisitableView constrains the visitable view to the view's leadingAnchor, trailingAnchor, topAnchor, and bottomAnchor. This works fine most of the time, but when trying to add a tab bar in my app, the tab bar and the visitable view overlap.

Looking over some of Apple's docs on safe area, it looks like safe area is the space not already used. To me then, it seems like the visitable view should be constrained relative to the safe area's anchors, instead of just the view's anchors.

What do think about this? Does this seem valid, or is there a different way this is supposed to be done?

Final location.pathname when accessing GET redirects

In brief: After visiting a GET request that responded with a redirect, the location.pathname of a turbo-ios webview is the requested path rather than the redirect-target path. This is different from how it is handled in Chrome, Safari, and Mobile Safari where the resulting location.pathname is the ultimate redirect-target path.

Please see: https://github.com/johnmaxwell/turbo-native-demo/pull/1/files

This fork and patch to the hotwired/turbo-native-demo demo app adds a /get-redirect GET route that simply redirects (302) to /one and an index link for it.

image
image
☝️Tapping the "Go through a redirect" on a desktop browser (Chrome or Safari) takes the browser to /one as expected, showing both the page content and updating the browser URL.


image
image
image
☝️ In the context of turbo-ios, the browser is shown the content of /one as expected, but the location.pathname remains on /get-redirect


Is this expected behavior or a known issue?

image
☝️In our production Rails app, we have a notification tray that contains a variety of types of notifications, each resolving to a different target URL. In that tray, we link not to the target URL, but to a notification URL -- e.g. /notifications/123 -- where the user's interaction is logged before forwarding them to the final target -- e.g. /final-target

In attempting to package our Rails app in a turbo-ios project, we have noticed that the app location.pathname hangs on /notification/123 . For subsequent link clicks, the referrer is then passed as /notification/123 instead of /final-target.

Issue with step 3

In trying to replace the SceneDelegate contents I run into an issue.

When I build the app, it succeeds, but I then get the following issue error which doesn't allow the app to load.

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

For the line window!.rootViewController = navigationController

Any idea around the issue?

invalidJSON when server returns path configuration with settings but no rules

The app throws *** error decoding path configuration: invalidJSON when the server endpoint returns a path configuration with only settings but no rules. However, the opposite is not true: no error message is thrown (and the bundle-defined settings are loaded fine) when the server path configuration only returns rules and no settings. This is what I observed while trying out the Jumpstart iOS template which relies on Turbo and where custom tabs are loaded from settings.

Based on the fact that it's OK for the server to only return rules without settings but not the other way around, this looks like a bug? I guess ideally the server should be able to flexibly return either of them, or both. However alternately it would help to make the docs and/or error message to make debugging easier.

Handling delete routes

Excuse me if I'm missing something obvious here, but this seems like a pretty common use case and I'm not sure how to handle it.

If I use the default Rails way of deleting a record...

<%= button_to event_path(@event), method: :delete, data: { confirm: 'Delete this event?' }, class: 'button button--link' do %>
      Delete event
<% end%>

This defaults to .advance so you end up pushing the redirected view (in this case the index list view of events) onto the stack and having a back button to go back to a view that no longer exists. How do you handle delete routes? Currently I'm having to write some custom regex in my Turbo coordinator to see if the activeVisitable URL was the show view and the next url is the index view and act as though that is deleting.

I tried data-turbo-action="replace" based on https://github.com/hotwired/turbo/pull/231/files but that doesn't seem to be working on any forms. This always defaults to .advance and is never picked up by the proposal.

<%= form_for(@event, url: events_path, html: { data: { "turbo-action": "replace" }}) do |f| %>
<%= button_to event_path(@event), method: :delete, data: { confirm: 'Delete this event?' }, form: { 'data-turbo-action': 'replace' } do %>

pageLoadFailure on successful request

I have a strange issue I'm trying to resolve, or at the very least find its source.

I have a controller action that renders successfully on web, but fails on iOS with a pageLoadFailure. When I monitor the request cycle, I see that the request completes successfully without issue, but then my SessionDelegate receives didFailRequestForVisitable with the error.

I have some error handling that pops a native error view controller on error, but when I disable it I see that the underlying webview pretty much looks as it should, with the exception of the navigation bar - it's missing title and back navigation.

I'm trying to find the source of this error, but everything besides the SessionDelegate indicates success so I don't have much to go on. In what situations does Turbo trigger a pageLoadFailure? I'd expect some accompanying error from the server, turbo js or the browser but see nothing indicating a problem.

Thanks!

Elaborating on `webView.loadRequest` flow in Authentication Doc

Hi all,

Assume one is to adopt the strategy listed here https://github.com/hotwired/turbo-ios/blob/main/Docs/Authentication.md#basecamp-3-and-hey: The key to this strategy is to create a URLRequest using the OAuth header, and use that to load the web view calling webView.loadRequest(request) for the authentication request.

Based on the setup in https://github.com/hotwired/turbo-ios/blob/main/Demo/SceneController.swift, where can I run webView.loadRequest(request) with additional Authorization header containing the OAuth token?

extension SceneController: WKNavigationDelegate is only executed on navigation, not on initial page load:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

Can I set headers on initial session.visit using something like VisitOptions?

session.visit(visitable)

Thanks for the help in advance!

Customizing the loading indicator

Making an issue to see if there's appetite for a PR here.

We wanted to make the activity indicator work with dark mode and be a different size. We ended up with this extension:

class MyVisitableView: VisitableView {
    private var _activityIndicatorView: UIActivityIndicatorView?
    override var activityIndicatorView: UIActivityIndicatorView {
        get {
            if _activityIndicatorView != nil {
                return _activityIndicatorView!
            } else {
                let frame = UIScreen.main.bounds
                // based on Turbo's VisitableView, changes are:
                // - added frame
                // - changed colours
                // - removed translatesAutoresizingMaskIntoConstraints
                // - removed pre iOS 13 code
                let view = UIActivityIndicatorView(frame: frame)

                view.style = .medium
                view.backgroundColor = UIColor.systemBackground
                view.color = UIColor.systemGray
                view.hidesWhenStopped = true

                _activityIndicatorView = view
                return _activityIndicatorView!
            }
        }
        set {
            _activityIndicatorView = newValue
        }
    }
}

class MyVisitableViewController: VisitableViewController {
    // for this pattern https://stackoverflow.com/a/49648445
    private var _visitableView: VisitableView?
    override var visitableView: VisitableView! {
        if _visitableView != nil {
            return _visitableView!
        } else {
            let view = MyVisitableView(frame: CGRect.zero)
            view.translatesAutoresizingMaskIntoConstraints = false
            _visitableView = view
            return _visitableView
        }
    }
}

Then instead of this we instead use a MyVisitableViewController.

I'm not a Swift expert, and maybe there's a better way to do this, but I copied most of the code from

open private(set) lazy var visitableView: VisitableView! = {
and
open lazy var activityIndicatorView: UIActivityIndicatorView = {

It's a bit of a clunky solution, so my question is if there's appetite for a simpler API for a custom loading indicator. Or if there's other ways that people have solved this.

I think ideally you could define a function in a VisitableViewController a subclass that returns a UIActivityIndicatorView, and the framework would do the rest.

[Question] How to send cookies with OAuth response?

The end of the Authorization doc mentions getting cookies alongside an OAuth response.

For HEY, we have a slightly different approach. We get the cookies back along with our initial OAuth request and set those cookies directly to the web view and global cookie stores.

First, can you elaborate a bit more on how that's possible with Rails? This request is coming from a new client, so it shouldn't have any existing or shared session/cookies. Are they created fresh? If so, how?

Second, am I wrong to assume that the access token request should be hitting an API-like controller? This will be a request without a CSRF token, so no protection can be added there. But if I work with, for example, ActionController::API, then I don't even have access to the cookies.

I've spent a while on this and feel like I'm missing something obvious. But I'm kind of lost as to how this all lines up. Any help would be greatly appreciated! Thanks in advance.

Demo doesn't work against a Turbo/Rails app with importmap-rails 0.9.1

The latest importmap-rails (included in Rails 7 rc1) includes es-module-shims.js version 1.3.5. This latter library creates blob URLs that the Turbo Demo app tries to visit. This causes errors with http status code 0.

One current workaround is to fork importmap-rails and downgrade es-module-shims.js to version 1.2.0 so the backend is serving this downgraded shim to the Demo app.

User's session is not persisent

Hi there! I am working on an iOS application using the turbo library. I made the initial setup you mention in the repo, and based also on the structure of the demo project. I am rendering an existent RoR application in the iOS application. I am able to sign up and log in, but once I close the application, I need to log in again; the session cookie is not being stored. This is not happening in Android, though.

  • Does it have to be with something regarding storage permission?
  • Is this an issue with this version?

As I mentioned, I set up the project based on your demo so the WKProcessPool() for sharing the cookie is being instantiated.
I don't know what else to do. In your demo, the session is being saved, so I assume I am missing something rather than it be an error of the library.

Render error when use Turbo.visit on iOS 14

In iOS 14, when clicking a button, and the Button triggered Turbo.visit(url), the screen keeps spinning, don't know what's wrong, but it works fine in iOS 13.

2021-08-04 05:29:56 +0000 - [Bridge] ← visitProposed
"visit ----------> https://localhost/en/trans/60300?ledger_id=1, lastUrl: Optional(https://localhost/en/ledgers) isModal: false cookies: " Turbo.VisitAction.advance ["uri": AnyHashable("turbo://fragment/web"), "context": AnyHashable("default"), "fallback_uri": AnyHashable("turbo://fragment/web"), "pull_to_refresh_enabled": AnyHashable(true), "presentation": AnyHashable("push")]
2021-08-04 05:29:56 +0000 - [JavaScriptVisit] startVisit()
2021-08-04 05:29:56 +0000 - [Bridge] → window.turboNative.visitLocationWithOptionsAndRestorationIdentifier
2021-08-04 05:29:56 +0000 - [Bridge] ← visitStarted
2021-08-04 05:29:56 +0000 - [JavaScriptVisit] webView(_:didStartVisitWithIdentifier:hasCachedSnapshot:)
2021-08-04 05:29:56 +0000 - [Bridge] ← visitRequestStarted
2021-08-04 05:29:56 +0000 - [JavaScriptVisit] webView(_:didStartRequestForVisitWithIdentifier:date:)
2021-08-04 05:29:56 +0000 - [Bridge] = window.turboNative.visitLocationWithOptionsAndRestorationIdentifier evaluation complete
2021-08-04 13:29:56.983084+0800 Flextab-debug[82290:898901] [Accessibility] WKContentView[@] set up: @ pid: @ MACH_PORT 713441792
2021-08-04 05:29:57 +0000 - [Bridge] ← visitRequestFinished
2021-08-04 05:29:57 +0000 - [JavaScriptVisit] webView(_:didFinishRequestForVisitWithIdentifier:date:)
2021-08-04 05:29:57 +0000 - [Bridge] ← visitRequestCompleted
2021-08-04 05:29:57 +0000 - [JavaScriptVisit] webView(_:didCompleteRequestForVisitWithIdentifier:)
2021-08-04 13:29:57.198272+0800 Flextab-debug[82290:898901] [IPC] Received an invalid message 'WebPageProxy_ElementDidFocus' from the WebContent process.
2021-08-04 13:29:57.207857+0800 Flextab-debug[82290:898901] [assertion] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=3 "Target is not running or required target entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" name:"Background" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Target is not running or required target entitlement is missing}>
2021-08-04 13:29:57.207947+0800 Flextab-debug[82290:898901] [ProcessSuspension] 0x1052c2a00 - ProcessAssertion: Failed to acquire RBS Background assertion 'ConnectionTerminationWatchdog' for process with PID 82295, error: Error Domain=RBSAssertionErrorDomain Code=3 "Target is not running or required target entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" name:"Background" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Target is not running or required target entitlement is missing}
2021-08-04 13:29:57.208062+0800 Flextab-debug[82290:898901] [Process] 0x12b035418 - [pageProxyID=6, webPageID=7, PID=82295] WebPageProxy::processDidTerminate: (pid 82295), reason 3
2021-08-04 13:29:57.208785+0800 Flextab-debug[82290:898901] [assertion] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}>
2021-08-04 13:29:57.208852+0800 Flextab-debug[82290:898901] [ProcessSuspension] 0x1052c2a40 - ProcessAssertion: Failed to acquire RBS Background assertion 'WebProcess Background Assertion' for process with PID 82295, error: Error Domain=RBSAssertionErrorDomain Code=2 "Specified target process does not exist" UserInfo={NSLocalizedFailureReason=Specified target process does not exist}
2021-08-04 13:29:57.213657+0800 Flextab-debug[82290:898901] [Loading] 0x12b035418 - [pageProxyID=6, webPageID=7, PID=82295] WebPageProxy::dispatchProcessDidTerminate: reason = 3
2021-08-04 05:29:57 +0000 - [Bridge] ← visitStarted

[Question] How to "catch" sign out requests?

The Turbo apps I'm working on follow the recommended Native & Web authentication approach outlined in the docs. They make a native HTTP request which returns the cookies and an auth token. This auth token is then stored in the secure keychain.

When signing out I would like to delete said token. However, DELETE requests are transparent to Turbo Native (as are all non-GET requests). All I'm informed of is that the request was redirected to / after the request and redirect complete.

I've reverted to wrapping all "sign out" links in a Stimulus controller that cancels the click and performs a custom JavaScript call via the bridge. Then the client can perform the sign out via a HTTP request and handle errors and delete the token on its own.

This feels like quite the workaround and I'm wondering if I'm missing something obvious. Ideally, we could route /logout (or similar) via the path configuration and remove the need for the Stimulus controller.

Any advice on a better way to "catch" sign out requests or is this Stimulus controller the right way to go?

WebView crashes on iOS 15

I don't think this relates to Turbolinks but it likely affects all apps which run it (or some of them at leats).

When our app is in background for some time and one opens a bunch of other apps so that the phone is low on memory, our app will get to a weird state the next time you open it. No content is shown in the webview and there is a bunch of messages in the XCode console. My understanding is that WebView runs a service which gets killed by the OS and is vital for WebView to run properly.

I do have a workaround which requires a slight change in the Turbo library. I changed the method webViewWebContentProcessDidTerminate() in Session.swift to post a notification when the webview process terminates:

public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
        debugLog("[Session] webViewWebContentProcessDidTerminate")
        NotificationCenter.default.post(name: Notification.Name("webViewWebContentProcessDidTerminate"), object: nil)
    }

Then I basically restart our App with a fresh WebView. It's not ideal as I loose the app state but it's the best I could come up with.

I'd like to ask two things:

  • do you have the same issue with other Turbo apps or is it just us?
  • would it be possible to add my change to the official Turbo code? I think it might be useful for other developers as well.

I've found this forum post which is likely exactly the issue we have:

https://developer.apple.com/forums/thread/684843

Please note that this is not happening on iOS 14.

Thanks a lot

Tony

On Advanced Topics, can you please put a more thourough example of WKScriptMessageHandler message passing?

From the quick start guide https://github.com/hotwired/turbo-ios/blob/main/Docs/QuickStartGuide.md, it is very easy to get up and running, and even link up a local dev project after allowing arbitrary loads.

What I would like to understand is what does a naive two-way call look like in code, and where to add that code to get it functioning.

I know the following code does not work, but what I'm looking for to be added to advanced topics would be similar to the following:

// from your rails application
let messagePasser = Turbolinks.messagePasser()
messagePasser.send({msg: 'Hello World'})

let messageResponder = Turbolinks.messagePasser()
messageResponder.onMessageRecieved(msg => {
  console.log(msg)
})

Replace ViewController.swift with the following:

import UIKit
import WebKit

class ViewController: UIViewController, WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print(message)
        userContentController.session.webView.evaluateJavaScript("messageResponder.onMessageRecieved('hello')") //does not work
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

@zachwaugh am I on the right track?

Demo crashes when rendering native numbers view in modal

We noticed this issue because we'd like to access the TurboNavigationController inside of a native modal in order to invoke some navigation from there.

To reproduce the issue you simply need to add "presentation": "modal", in line 21 of path-configuration.json:

"view-controller": "numbers"

After that, when you run the demo, you can click on "intercept with a native view" and click on one of the rows. Now the app will crash.

It seems as if the navigationController in this case is simply an instance of UINavigationController and not of TurboNavigationController, so the forced cast lets the app crash. I think this is caused by this line here:

let modalNavController = UINavigationController(rootViewController: viewController)

So the questions are:

  1. Is this a bug, or intended behavior?
  2. If it's intended, how do we achieve navigation from within a native modal instead?

N. b. I tested the same thing with the android demo where it doesn't crash, so I suspect this is indeed a bug.

Accessing Camera / GPS

Does turbo-ios have inbuilt APIs to request access to Camera or GPS? Or is there an expectation that users of turbo-ios will have to write additional swift code to handle such requests?

Turbo not loaded

Hi,

I'm having trouble with turbo-ios not accepting the page because Turbo is not loaded. When I visit the page in browser, window.Turbo is set correctly. So I assume window.Turbo is set too late for the app to register it?

Right now I have removed turbo from my Importmap and added the following to my application.html.erb for it to work.
<script type="module"> import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo'; </script>

Am I missing something obvious?

Update previous views

Hi,

  1. I have a page with links
  2. I click and "advance" on a link's page
  3. I click and "advance" on the edit page
  4. Using turbo_stream, I update the page without advancing or replacing the page

But when I go back, using the native navigation arrow, got back to the link's page, the content is not updated.
My question, sorry to be like a nut, is: how can I update previous visited page with the updated content ?

Thanks for your time and your help.

Cordialement,

Is window.Turbo required and is it documented?

It took me quite a while to realize that it setting window.Turbo must be a requirement for the iOS part to work properly?

registerAdapter() {
if (window.Turbo) {
Turbo.registerAdapter(this)
} else if (window.Turbolinks) {
Turbolinks.controller.adapter = this
} else {
this.pageLoadFailed()
}
}

I haven't seen this mentioned anywhere, and was following the npm package install instructions here (just an import and part of my webpack build):
image

It was extra confusing because I could tell that Turbo was working in the browser, but was not seeing the didProposeVisit code firing at all on iOS (though there was a pretty vague [Bridge] ← pageLoadFailed log line)... After setting window.Turbo = Turbo in my JS, everything seems to be working now.

Anyway, just wondering if this interpretation is correct and could/should be documented somewhere? Or is it already and I missed it? Thanks!

Do not dismiss modal and navigate to another view

Hi,

How can I allow to navigate in a modal ?

For example, I click on link, it open a modal (after settings conf json file with the correct url), then in this modal, there is 2 links and if I click a link, it advance to a new view but without dismissing the modal.

Thanks for the help.

Notification handeling documentation

Hey,

The "issue" seems to be that I have no clue how it works, which might be an issue with the documentation.
Whenever one of my users opens a push notification I would like to open the app in a parameter. I found out how to receive the "user clicks on notification" and how to get the custom URL but could not found anything how to visit the URL.

Maybe it's super easy but I can't figure it out
image

Any help would be a massive help!

Cheers

SwiftUI Component

Greetings!

I'm working on a SwiftUI component for Turbo. Here's what the current implementation looks like:

//
//  TurboView.swift
//
//  Created by Brad Gessler on 12/29/20.
//

import UIKit
import SwiftUI
import Turbo

struct TurboView: UIViewControllerRepresentable {
    let session: Session
    let url: String
    let navigate: ((VisitProposal) -> Void)

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, SessionDelegate {
        var parent: TurboView

        init(_ parent: TurboView) {
            self.parent = parent
        }

        func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
            parent.navigate(proposal)
        }

        func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) {
            NSLog("didFailRequestForVisitable: \(error)")
        }
    }

    func makeUIViewController(context: Context) -> VisitableViewController {
        let viewController = VisitableViewController(url: URL(string: url)!)
        session.delegate = context.coordinator
        session.visit(viewController)
        return viewController
    }

    func updateUIViewController(_ visitableViewController: VisitableViewController, context: Context) {
    }
}

struct TurboView_Previews: PreviewProvider {
    static var previews: some View {
        TurboView(session: Session(), url: "https://www.turbo-example-url.com/", navigate: { proposal in
            NSLog(proposal.url.absoluteString)
        })
    }
}

Using it in a NavigationView component looks like:

//
//  ContentView.swift
//
//  Created by Brad Gessler on 12/21/20.
//

import SwiftUI
import Turbo

struct TurboNavigationView: View {
    let session: Session
    let url: String
    @State private var destinationUrl : String = ""
    @State private var isDestinationVisible : Bool = false
    
    var body: some View {
        VStack {
            NavigationLink(destination: TurboNavigationView(session: session, url: destinationUrl), isActive: $isDestinationVisible) { EmptyView() }
            TurboView(session: session, url: url, navigate: { proposal in
                NSLog("Heading to \(proposal.url.absoluteString)")
                self.destinationUrl = proposal.url.absoluteString
                self.isDestinationVisible = true
            })
        }
        .navigationTitle("<document.title>")
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView{
            TurboNavigationView(session: Session(), url: "https://www.turbo-example-url.com/")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

There's still a lot of issues with how this works because I'm new to writing Swift code and still learning the UIKit, SwiftUI, and WKWebView APIs. Right now I'm trying to workout the lifecycle of view objects & bindings in UIKit and SwiftUI.

Is this something that this project would eventually like to integrate into this distribution? If so, I'd like to pair with somebody more familiar with everything I listed out above.

If a form submission redirects, the target page is rendered twice

This gif demonstrates the bug:

form

To replicate, using https://github.com/hotwired/turbo-native-demo

// index.ejs

  <form method="post" action="/redirect">
    <button type="submit">Submit Form and redirect to another page</button>
  </form>
// server.js

app.post("/redirect", (request, response) => {
  response.redirect("/one")
})

Or click here to see the changes: hotwired/turbo-native-demo@main...ghiculescu:duplicate-page-replica (if you'd like redirect behavior in the demo, I can tidy up the UI and make a PR - but that's not the issue here :))

When you click on the button:

  • The POST request is made and returns a 302
  • Turbo.js catches the 302 and makes a GET to /one
  • Turbo-native's didProposeVisit is called with /one as the desired URL. session.visit(/one) gets called. And another GET to /one is made.

You can see this in the demo server logs:

Sat Jan 09 2021 17:35:23 GMT-0700 (Mountain Standard Time) -- POST /redirect
Sat Jan 09 2021 17:35:23 GMT-0700 (Mountain Standard Time) -- GET /one
Sat Jan 09 2021 17:35:23 GMT-0700 (Mountain Standard Time) -- GET /one

As well as the doubled up requests in Safari's inspector. I would expect only a single GET /one.

This isn't too bad in this case, but where it gets really problematic is if the POST request sets a flash (in Rails). For example:

def create
  redirect_to some_path, alert: "something happened"
end

In this case the flash will be consumed by the first GET request, and will not appear in the second one. So from the app's perspective no flash message is shown (but if you do exactly the same behavior through a web browser, you do see a flash).

ColdBoot redirect doesn't trigger Turbo actions

turbo-ios uses ColdBootVisit for the first request to the server. The problem is that it doesn't notify Turbo about any changes in the loading URL. If I make a request to server path "/root" and it redirects to "/new" Turbo still thinks it's a "/root" request.

How to reproduce:

On Demo application add print(url) statement at the start of the route(url: URL, options: VisitOptions, properties: PathProperties) method.

diff --git Demo/Navigation/TurboNavigationController.swift Demo/Navigation/TurboNavigationController.swift
index ce16d5c..04a3789 100644
--- Demo/Navigation/TurboNavigationController.swift
+++ Demo/Navigation/TurboNavigationController.swift
@@ -22,6 +22,8 @@ class TurboNavigationController : UINavigationController {
     }

     func route(url: URL, options: VisitOptions, properties: PathProperties) {
+        print(url)
+

Load application and click on "Follow a redirect". Two statements have been logged:

https://turbo-native-demo.glitch.me/follow
https://turbo-native-demo.glitch.me/redirected

demo_hot

Now, change the rootURL to a link with redirection

diff --git Demo/SceneController.swift Demo/SceneController.swift
index d1d78aa..69306c0 100644
--- Demo/SceneController.swift
+++ Demo/SceneController.swift
@@ -7,7 +7,7 @@ final class SceneController: UIResponder {
     private static var sharedProcessPool = WKProcessPool()

     var window: UIWindow?
-    private let rootURL = Demo.current
+    private let rootURL = URL.init(string: (Demo.current.absoluteString +  "/follow"))!

And run the application. Only one statement has been logged:

https://turbo-native-demo.glitch.me/follow

demo_cold

Hover state is triggered on "next" page

Tapping a link seems to trigger the hover state for the next page. The element under the initial tap seems to be effected.

I can reproduce this by making the following diff to the Demo app. Note that even if the navigated to page was different the hover state would still show. I'm using the same page in this example to keep the diff small.

Turbo.hover.mov
diff --git a/public/native.css b/public/native.css
index a3b888f..00933c9 100644
--- a/public/native.css
+++ b/public/native.css
@@ -10,7 +10,7 @@ input[type=text]:focus {
 }
 
 .actions__item {
-  text-decoration: none;
+  /* text-decoration: none; */
 }
 
 .button\@native,
@@ -23,7 +23,7 @@ input[type=text]:focus {
   font-size: var(--type-m);
   color: var(--color-black);
   text-align: center;
-  text-decoration: none;
+  /* text-decoration: none; */
   line-height: 1.25;
   border: 0;
   border-radius: 0.5em;
diff --git a/views/one.ejs b/views/one.ejs
index d17292c..8bfc0b4 100644
--- a/views/one.ejs
+++ b/views/one.ejs
@@ -2,6 +2,6 @@
 <p>When you tap the link to this page, Turbo proposes a visit to the app to decide how to handle the URL. In this case, it chooses to present a Turbo-based web screen.</p>
 <p>Go back and try <em>Intercept with a native view</em> to see how to load a native screen instead.</p>
 <p>This link will be presented using the same advance presentation:</p>
-<p><a href="/two" class="button@native space --bottom-flush">Advance to another webpage</a></p>
+<p><a href="/one" class="button@native space --bottom-flush hoverable">Advance to another webpage</a></p>
 <p>And this next one does a <em>replace</em> instead:</p>
 <p><a href="/two?action=replace" class="button@native space --buttom-flush" data-turbo-action="replace" data-turbolinks-action="replace">Replace with another webpage</a></p>

Is this expected behavior? Are developers responsible for disabling hover state for native apps? I'd love to hear your thoughts on this!

[Question] Performance compared to Turbolinks in Browser

Hello!

I checked out turbo-ios for a project and noticed (correct me if I'm wrong) that the WebView makes a new request, when before it was a remote request. I think the browser adapter is replaced with the iOS adapter, right? So my conclusion would be that turbo-ios has no performance advantage compared to a website with full page reloads? That would mean that it is slower compared to the turbo powered website..

I was thinking if it would be possible to have one shared webview like now, and then just use Turbo for browser in it. Before navigation a screenshot is taken and added to the last view, and the webview is copied to the new UIView? It is just an idea.. Did you try such a thing or am I on the wrong track e.g. getting it wrong?

Initialize path configurations from file synchronously

I'm curious why PathConfigurationLoader callbacks asynchronously:

private func updateHandler(with config: PathConfigurationDecoder) {
DispatchQueue.main.async {
self.completionHandler?(config)
}
}

If I bundle a file (or they have been cached before) with path configurations then they should be available immediately before an initial request:

override func viewDidLoad() {
  super.viewDidLoad();
                             
  // Properties are null on initial loading because of DispatchQueue.main.async
  let properties = (session.pathConfiguration?.properties(for: visitableURL) ?? [:]) 
        
  route(url: initialURL, options: VisitOptions(action: .replace), properties: properties)
}

Got nil error on page redirect after upgraded turbo to v7.0.0-rc.3

#116  case .visitProposed:
#117             delegate?.webView(self, didProposeVisitToLocation: message.location!, options: message.options!)

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

The problem is message.location is nil, may be caused by requestSucceededWithResponse

 async requestSucceededWithResponse(request: FetchRequest, response: FetchResponse) {
    const responseHTML = await response.responseHTML
    if (responseHTML == undefined) {
      this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch })
    } else {
      this.redirectedToLocation = response.redirected ? response.location : undefined
      this.recordResponse({ statusCode: response.statusCode, responseHTML })
    }
  }
 followRedirect() {
    if (this.redirectedToLocation && !this.followedRedirect) {
      this.adapter.visitProposedToLocation(this.redirectedToLocation, {
        action: 'replace',
        response: this.response
      })
      this.followedRedirect = true
    }
  }

This may be related to:

turbo-ios unable to page load Rails application

I was able to follow the QuickStartGuide.md to load https://turbo-native-demo.glitch.me but unable to load a Rails 6 application that I've created locally.

Things I've tried:

  1. Ensured that Turbo JS is imported with Webpacker (in app/javascript/packs/application.js):
import { Turbo, cable } from "@hotwired/turbo-rails"
  1. Made sure my iOS project's info.plist is configured as so:
<key>NSAppTransportSecurity</key>
<dict>
	<key>NSAllowsArbitraryLoads</key>
	<true/>
</dict>
  1. Some other things that I've tried based on turbo-native-demo
    a. add a index class to <body/>
    b. wrap the content in <main />

The page just loads indefinitely.
Screen Shot 2020-12-28 at 9 52 58 PM

  1. What constitutes a Rails View that can be loaded by the iOS app?
  2. How can I better debug in such situation?

Let me thanks the team again for the amazing work!

Form submissions with response HTML don't work

There's a potential bug in Turbo-iOS when it comes to submitting forms and rendering the response HTML.

I notice the following flow when submitting a form in Turbo iOS:

  1. The form is submitted.
  2. The server issues a redirect upon successful form response.
  3. The redirect is followed, and the response is returned.
  4. The WebView bridge is alerted to a "Visit Proposed" event containing the response HTML.
  5. You're free to route the proposal as desired, usually ended with calling the "window.turboNative.visitLocationWithOptionsAndRestorationIdentifier" with the response HTML.

The problem is, calling "window.turboNative.visitLocationWithOptionsAndRestorationIdentifier" containing response HTML returns the exact same page, completely ignoring the response HTML.

If you try this in the demo app, it will work the first time and the first time only. Subsequent form submissions will continue to return the same page over and over again. This is because of a "Back Navigation" bug, where submitting the form the first time, the app thinks you hit the back button, and issues another get request.

Walking through step by step in the Demo app, using Proxyman to see requests/responses to/from the server:

  1. Run the app
  2. Tap "Load a web page modally"
  3. Submit the form. Proxyman shows two get requests, the first from the redirect, the second from the above mentioned "Back Navigation" bug: The rendered screen is "correct" only because of the second get request.

Screen Shot 2022-04-08 at 5 57 56 PM

  1. Hit the back button.

  2. Tap "Load a web page modally" again.

  3. Submit the form. Proxyman now shows one get request, as expected. However, the response HTML was ignored. This can be seen by comparing the time stamps of the response HTML with the time stamp rendered on screen:

Screen Shot 2022-04-08 at 6 34 43 PM

Walking through the "Back Navigation" bug:

  1. Run the app
  2. Tap "Load a web page modally"
  3. Place breakpoints on line 207 of "Session.swift" and line 47 of "Visit.swift".
  4. Submit the form.
  5. Breakpoint: The first time and first time only, a "visit did complete" message comes from the ScriptMessageHandler, setting the state of the Visit to complete. I have no idea why this event is sent, but it's the reason for the Back Navigation bug. Hit continue.
  6. Breakpoint Session's visitableViewWillAppear activates, and ends up in the "Navigating Backward" path, issuing another visit (GET request 2). It is this second GET request that gives the appearance of "correct" behavior when submitting the form the first time.

Screen Shot 2022-04-08 at 6 09 48 PM

Subsequent form submissions skip step 5, so on visitableViewWillAppear, it ends up in the "Navigating Forward" path, issuing only one GET request as it should. But because the response HTML is ignored by Turbo, it re-renders the same screen again and again. Refreshing the page renders the correct response.

I believe this is bug in turbo.js, but I can't say for sure because I can't set breakpoints in the javascript code in Xcode. 😢 I need to do more research into how Turbo itself works.

My current workaround is to set the response HTML to nil before passing the proposal to "window.turboNative.visitLocationWithOptionsAndRestorationIdentifier". This seems to trick Turbo into not knowing that the request came a redirected form submission, so it issues another GET request. Submitting two GET requests per form submission is not ideal, though.

Screen Shot 2022-04-08 at 6 31 15 PM

Delays in loading ESM prevent turbo.js from detecting the server-side installation

Using https://github.com/hotwired/turbo-rails with the assets pipeline, all the turbo-related code is loaded using es-module-shims.
I've added the window.Turbo declaration as such:

<script src="/assets/libraries/turbo_native.js" type="module-shim"></script>

with /assets/libraries/turbo_native.js declaring window.Turbo

import {Turbo} from "turbo"
window.Turbo = Turbo

But with this setup, there is a delay between the time https://github.com/hotwired/turbo-ios/blob/956484e3524eaf203d12807313e7119427a5771d/Source/WebView/turbo.js loads and the server side window.Turbo gets assigned, leading to a call to this.pageLoadFailed()

I was able to get around it by tweaking the registerAdapter method to add retries waiting for window.Turbo to load as such:

    registerAdapter(retries=1) {
      if (window.Turbo) {
        Turbo.registerAdapter(this)
      } else if (window.Turbolinks) {
        Turbolinks.controller.adapter = this
      } else if (retries > 3) {
        this.pageLoadFailed()
      } else {
        setTimeout(() => this.registerAdapter(retries + 1), 2000)
      }      
    }
  • Is there a way to ensure registerAdapter will run after window.Turbo gets defined when using es-module-shim ? (i.e. should I load my code differently)
  • If not, should the registerAdapter method be changed as above to be more resilient to timing issues ? the downside is that there can be a significant delay in the error case (it will take 6 seconds to realize that window.Turbo is not defined)

Stock UI components

I am wondering if there is a list of stock UI components this wrapper provides? I came across this blog of HelloWeather app https://helloweather.com/blog/two-platforms in it item #2 says

We use 100% stock components for our native UI. Whatever Apple or Google provides out of the box, that’s our jam

Not sure if its referring to the turbo-ios and turbo-android wrappers ?

Links opening in Safari, instead of app

I've encountered this problem recently and I can't figure out how to fix it. Basically, the internal, relative paths are opening on Safari instead of inside the iOS app. Here's some context, I'm running rails 6.1.5 with turbo-rails 1.0.1. I followed the upgrading guide "Compatibility with Rails UJS"

On the iOS side, I'm running a slightly modified version of @dalezak's "Turbo-iOS base project", you can see the repo here. The package dependency point to turbo-ios 7.0.0-rc.6.

I'm not sure if I'm 100% sure on this, but it seems like it's treating all those links as external.

extension TurboController: SessionDelegate {
    func session(_ session: Session, openExternalURL url: URL) {
        let safariViewController = SFSafariViewController(url: url)
        present(safariViewController, animated: true, completion: nil)
    }
}

When I print() the url, I can see it as an absolute path like http://localhost:3000/some-page-url, same behavior happens on a test env. But on HTML code it's relative.

I'm a bit lost, to say the least, I'm not sure if the error is on the rails side or the swift side. Any help would be very much appreciated. And please let me know if I need to share more stats or code. Thanks!

[Question] Offline/cache support?

This is a question, not a bug report or issue. Maybe we can enable GitHub Discussions for these types of conversations?

Are they any recommended approaches to caching data for offline access? I understand that the library won't do anything out of the box – I don't expect it to. But if folks have experience doing this I'd love some pointers.

For example:

  • What iOS APIs to use to download the payload?
  • Where to store/persist the cache?
  • How to invalidate it?
  • How to render it later when the internet connection is down?
  • Any other tips or gotchas.

I know HEY renders email content without a connections, so I know this is possible.

Any help, tips, or ideas would be greatly appreciated!

Ongoing maintenance of this library

What are the ongoing maintenance plans for this library? It looks like the latest release and PR merge was back in January.

There are currently 17 open issues and 11 open pull requests. The demo included with the app doesn't even build (PR #72 fixes this).

I understand that 37signals/Basecamp/HEY has a lot going on. But I work with a bunch of people that rely on this library to power their iOS apps. And it makes for a hard case trying to adopt a library that looks like it might be abandoned soon.

@jayohms, I'd love to help out. If there's any possibility of getting commit access, helping review PRs, or anything else, please let me know! If it helps, I can provide links to blog articles I've written on Turbo Native, past PRs and contributions, my Turbo Native code template, or testimonials from past clients.

Swift compiler warning

This is super minor, but I thought it'd be worth noting.

image

% xcrun swift -version
swift-driver version: 1.26.21 Apple Swift version 5.5.2 (swiftlang-1300.0.47.5 clang-1300.0.29.30)
Target: x86_64-apple-macosx11.0

Missing title when using content_for with Rails 7

Hola 👋🏼

I can get the title to show not when it's hard-coded by using content_for It only works for the desktop browser but not on the iPhone:

<!-- application.html.erb -->
<title><%= yield(:title) %></title>

Then on other page:

<%= content_for(:title, "Books") %>

Having the above, navigation title, iOS, is empty:

print(session.webView.title) // or
print(visitableView.webView?.title)

What works? By setting an instance variable in the controller/action then:

<!-- application.html.erb -->
<title><%= @title %></title>

Allowing custom actions

I'm not sure how possible this is and my knowledge of enums is pretty limited, but could you allow a custom action to be passed through? For example we have some form redirections that we'd like to advance but replace the previous view, as replace on it's own can be quite abrupt. Then we could do something like...

<%= form_for(@event, url: events_path, html: { data: { "turbo-action": "advanceReplace" }}) do |f| %>
if action == .custom("advanceReplace") {
            navigationController.viewControllers = Array(navigationController.viewControllers.dropLast())
            navigationController.pushViewController(viewController, animated: true)
            session.visit(viewController)
        }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.