Giter VIP home page Giter VIP logo

turbolinks-ios's Introduction

Turbolinks for iOS

⚠️ Note: this repo is no longer being maintained. We recommend migrating to Turbo iOS which supports both Turbo 7 and Turbolinks 5 apps. Follow the migration guide to update from this framework to Turbo. ⚠️


Build high-fidelity hybrid apps with native navigation and a single shared web view. Turbolinks for iOS provides the tooling to wrap your Turbolinks 5-enabled web app in a native iOS shell. It manages a single WKWebView instance across multiple view controllers, giving you native navigation UI with all the client-side performance benefits of Turbolinks.

Features

  • Deliver fast, efficient hybrid apps. Avoid reloading JavaScript and CSS. Save memory by sharing one WKWebView.
  • Reuse mobile web views across platforms. Create your views once, on the server, in HTML. Deploy them to iOS, Android, and mobile browsers simultaneously. Ship new features without waiting on App Store approval.
  • Enhance web views with native UI. Navigate web views using native patterns. Augment web UI with native controls.
  • Produce large apps with small teams. Achieve baseline HTML coverage for free. Upgrade to native views as needed.

Requirements

Turbolinks for iOS is written in Swift 5.0 and requires Xcode 10.2. It should also work with Swift 4.2 as well. It currently supports iOS 10 or higher, but we'll be dropping iOS 10 support in the next version.

Web views are backed by WKWebView for full-speed JavaScript performance.

Note: You should understand how Turbolinks works with web applications in the browser before attempting to use Turbolinks for iOS. See the Turbolinks 5 documentation for details.

Installation

Install Turbolinks manually by building Turbolinks.framework and linking it to your project.

Installing with Carthage

Add the following to your Cartfile:

github "turbolinks/turbolinks-ios" "master"

Installing with CocoaPods

Add the following to your Podfile:

use_frameworks!
pod 'Turbolinks', :git => 'https://github.com/turbolinks/turbolinks-ios.git'

Then run pod install.

Running the Demo

This repository includes a demo application to show off features of the framework. The demo bundles a simple HTTP server that serves a Turbolinks 5 web app on localhost at port 9292.

To run the demo, clone this repository to your computer and change into its directory. Then, start the demo server by running TurbolinksDemo/demo-server from the command line.

Once you’ve started the demo server, explore the demo application in the Simulator by opening turbolinks-ios.xcworkspace and running the TurbolinksDemo target.

Turbolinks for iOS demo application

Getting Started

We recommend playing with the demo app to get familiar with the framework. When you’re ready to start your own application, see our Quick Start Guide for step-by-step instructions to lay the foundation.

Understanding Turbolinks Concepts

The Session class is the central coordinator in a Turbolinks for iOS application. It creates and manages a single WKWebView instance, and lets its delegate—your application—choose how to handle link taps, present view controllers, and deal with network errors.

A Visitable is a UIViewController that can be visited by the Session. Each Visitable view controller provides a VisitableView instance, which acts as a container for the Session’s shared WKWebView. The VisitableView has a pull-to-refresh control and an activity indicator. It also displays a screenshot of its contents when the web view moves to another VisitableView.

When you tap a Turbolinks-enabled link in the web view, the Session asks your application how to handle the link’s URL. Most of the time, your application will visit the URL by creating and presenting a Visitable. But it might also choose to present a native view controller for the URL, or to ignore the URL entirely.

Creating a Session

To create a Session, first create a WKWebViewConfiguration and configure it as needed (see Customizing the Web View Configuration for details). Then pass this configuration to the Session initializer and set the delegate property on the returned instance.

The Session’s delegate must implement the following two methods.

func session(session: Session, didProposeVisitToURL URL: NSURL, withAction action: Action)

Turbolinks for iOS calls the session:didProposeVisitToURL:withAction: method before every application visit, such as when you tap a Turbolinks-enabled link or call Turbolinks.visit(...) in your web application. Implement this method to choose how to handle the specified URL and action.

See Responding to Visit Proposals for more details.

func session(session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError)

Turbolinks calls session:didFailRequestForVisitable:withError: when a visit’s network request fails. Use this method to respond to the error by displaying an appropriate message, or by requesting authentication credentials in the case of an authorization failure.

See Handling Failed Requests for more details.

Working with Visitables

Visitable view controllers must conform to the Visitable protocol by implementing the following three properties:

protocol Visitable {
    weak var visitableDelegate: VisitableDelegate? { get set }
    var visitableView: VisitableView! { get }
    var visitableURL: NSURL! { get }
}

Turbolinks for iOS provides a VisitableViewController class that implements the Visitable protocol for you. This view controller displays the VisitableView as its single subview.

Most applications will want to subclass VisitableViewController to customize its layout or add additional views. For example, the bundled demo application has a DemoViewController subclass that can display a custom error view in place of the VisitableView.

If your application’s design prevents you from subclassing VisitableViewController, you can implement the Visitable protocol yourself. See the VisitableViewController implementation for details.

Note that custom Visitable view controllers must forward their viewWillAppear and viewDidAppear methods to the Visitable delegate’s visitableViewWillAppear and visitableViewDidAppear methods. The Session uses these hooks to know when it should move the WKWebView from one VisitableView to another.

Building Your Turbolinks Application

Initiating a Visit

To visit a URL with Turbolinks, first instantiate a Visitable view controller. Then present the view controller and pass it to the Session’s visit method.

For example, to create, display, and visit Turbolinks’ built-in VisitableViewController in a UINavigationController-based application, you might write:

let visitable = VisitableViewController()
visitable.URL = NSURL(string: "http://localhost:9292/")!

navigationController.pushViewController(visitable, animated: true)
session.visit(visitable)

Responding to Visit Proposals

When you tap a Turbolinks-enabled link, the link’s URL and action make their way from the web view to the Session as a proposed visit. Your Session’s delegate must implement the session:didProposeVisitToURL:withAction: method to choose how to act on each proposal.

Normally you’ll respond to a visit proposal by simply initiating a visit and loading the URL with Turbolinks. See Initiating a Visit for more details.

You can also choose to intercept the proposed visit and display a native view controller instead. This lets you transparently upgrade pages to native views on a per-URL basis. See the demo application for an example.

Implementing Visit Actions

Each proposed visit has an Action, which tells you how you should present the Visitable.

The default Action is .Advance. In most cases you’ll respond to an advance visit by pushing a Visitable view controller for the URL onto the navigation stack.

When you follow a link annotated with data-turbolinks-action="replace", the proposed Action will be .Replace. Usually you’ll want to handle a replace visit by popping the topmost view controller from the navigation stack and pushing a new Visitable for the proposed URL without animation.

Handling Form Submission

By default, Turbolinks for iOS prevents standard HTML form submissions. This is because a form submission often results in redirection to a different URL, which means the Visitable view controller’s URL would change in place.

Instead, we recommend submitting forms with JavaScript using XMLHttpRequest, and using the response to tell Turbolinks where to navigate afterwards. See Redirecting After a Form Submission in the Turbolinks documentation for more details.

Handling Failed Requests

Turbolinks for iOS calls the session:didFailRequestForVisitable:withError: method when a visit request fails. This might be because of a network error, or because the server returned an HTTP 4xx or 5xx status code.

The NSError object provides details about the error. Access its code property to see why the request failed.

An error code of .HTTPFailure indicates that the server returned an HTTP error. You can access the HTTP status code in the error object's userInfo dictionary under the key "statusCode".

An error code of .NetworkFailure indicates a problem with the network connection: the connection may be offline, the server may be unavailable, or the request may have timed out without receiving a response.

func session(session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
    guard let errorCode = ErrorCode(rawValue: error.code) else { return }

    switch errorCode {
    case .HTTPFailure:
        let statusCode = error.userInfo["statusCode"] as! Int
        // Display or handle the HTTP error code
    case .NetworkFailure:
        // Display the network failure or retry the visit
    }
}

HTTP error codes are a good way for the server to communicate specific requirements to your Turbolinks application. For example, you might use a 401 Unauthorized response as a signal to prompt the user for authentication.

See the demo app’s ApplicationController for a detailed example of how to present error messages and perform authorization.

Setting Visitable Titles

By default, Turbolinks for iOS sets your Visitable view controller’s title property to the page’s <title>.

If you want to customize the title or pull it from another element on the page, you can implement the visitableDidRender method on your Visitable:

func visitableDidRender() {
    title = formatTitle(visitableView.webView?.title)
}

func formatTitle(title: String) -> String {
    // ...
}

Starting and Stopping the Global Network Activity Indicator

Implement the optional sessionDidStartRequest: and sessionDidFinishRequest: methods in your application’s Session delegate to show the global network activity indicator in the status bar while Turbolinks issues network requests.

func sessionDidStartRequest(_ session: Session) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
}

func sessionDidFinishRequest(_ session: Session) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}

Note that the network activity indicator is a shared resource, so your application will need to perform its own reference counting if other background operations update the indicator state.

Changing How Turbolinks Opens External URLs

By default, Turbolinks for iOS opens external URLs in Safari. You can change this behavior by implementing the Session delegate’s optional session:openExternalURL: method.

For example, to open external URLs in an in-app SFSafariViewController, you might write:

import SafariServices

// ...

func session(session: Session, openExternalURL URL: NSURL) {
    let safariViewController = SFSafariViewController(URL: URL)
    presentViewController(safariViewController, animated: true, completion: nil)
}

Becoming the Web View’s Navigation Delegate

Your application may require precise control over the web view’s navigation policy. If so, you can assign yourself as the WKWebView’s navigationDelegate and implement the webView:decidePolicyForNavigationAction:decisionHandler: method.

To assign the web view’s navigationDelegate property, implement the Session delegate’s optional sessionDidLoadWebView: method. Turbolinks calls this method after every “cold boot,” such as on the initial page load and after pulling to refresh the page.

func sessionDidLoadWebView(_ session: Session) {
    session.webView.navigationDelegate = self
}

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> ()) {
    decisionHandler(WKNavigationActionPolicy.Cancel)
    // ...
}

Once you assign your own navigation delegate, Turbolinks will no longer invoke the Session delegate’s session:openExternalURL: method.

Note that your application must call the navigation delegate’s decisionHandler with WKNavigationActionPolicy.Cancel for main-frame navigation to prevent external URLs from loading in the Turbolinks-managed web view.

Customizing the Web View Configuration

Turbolinks allows your application to provide a WKWebViewConfiguration when you instantiate a Session. Use this configuration to set a custom user agent, share cookies with other web views, or install custom JavaScript message handlers.

let configuration = WKWebViewConfiguration()
let session = Session(webViewConfiguration: configuration)

Note that changing this configuration after creating the Session has no effect.

Setting a Custom User Agent

Set the applicationNameForUserAgent property to include a custom string in the User-Agent header. You can check for this string on the server and use it to send specialized markup or assets to your application.

configuration.applicationNameForUserAgent = "MyApplication"

Sharing Cookies with Other Web Views

If you’re using a separate web view for authentication purposes, or if your application has more than one Turbolinks Session, you can use a single WKProcessPool to share cookies across all web views.

Create and retain a reference to a process pool in your application. Then configure your Turbolinks Session and any other web views you create to use this process pool.

let processPool = WKProcessPool()
// ...
configuration.processPool = processPool

Passing Messages from JavaScript to Your Application

You can register a WKScriptMessageHandler on the configuration’s user content controller to send messages from JavaScript to your iOS application.

class ScriptMessageHandler: WKScriptMessageHandler {
    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
        // ...
    }
}

let scriptMessageHandler = ScriptMessageHandler()
configuration.userContentController.addScriptMessageHandler(scriptMessageHandler, name: "myApplication")
document.addEventListener("click", function() {
    webkit.messageHandlers.myApplication.postMessage("Hello!")
})

Contributing to Turbolinks

Turbolinks for iOS is open-source software, freely distributable under the terms of an MIT-style license. The source code is hosted on GitHub. Development is sponsored by Basecamp.

We welcome contributions in the form of bug reports, pull requests, or thoughtful discussions in the GitHub issue tracker.

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.


© 2017 Basecamp, LLC

turbolinks-ios's People

Contributors

chris-teague avatar crododile avatar danieldimitrov avatar dginsburg avatar joemasilotti avatar jun1st avatar lucascaton avatar packagethief avatar sstephenson avatar zachwaugh 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

turbolinks-ios's Issues

Can't trigger UJS link from iOS

I have a button in my iOS app that calls a javascript function. That javascript function calls $("#sign-out").click(). That element looks like <a id="sign-out" href="/users/logout" data-method="delete" rel="nofollow">Sign out</a>

Calling the .click() from Firebug works fine, but in my turbolinks-ios app, nothing happens. I can debug to ensure that the element I'm calling it on exists, but the link doesn't get followed.

Similarly, for a regular <a id="sign-in" href="/users/signin">Sign in</a> link, calling $("#sign-in").click() has no effect, yet $("sign-in").get(0).click() DOES work, but of course that doesn't help in the first case because that would skip the UJS.

Any ideas of what I'm missing or what I need to do here?

session:openExternalURL not firing on first load?

First of all, great job with Turbolinks and the ios adapter. I'm a big fan, and very appreciative.

I might be doing something wrong, but I'm having a problem using the 'session:openExternalURL:' method. I'm trying to handle a mailto link, and, for illustration purposes, just doing something basic:

func session(session: Session, openExternalURL URL: NSURL) {
        print("opening an external url: " + URL.absoluteString)
        let url = URL.absoluteString
        let url_elements = url.componentsSeparatedByString(":")
        if(url_elements[0] == "mailto"){
            print("we need to mail! to this address: " + url_elements[1])
        }
    }

For some reason, this is not firing when the page first loads. It only fires when I pull-to-refresh. Anyone have an idea as to what might be going on here?

Note, I found and originally posted in this thread: #34 (comment)

Thanks, all!

MobileAssetError:29 Unable to copy asset information...

Running the demo with absolutely no changes, on latest xcode, and each time I focus on a form input (the password field in the demo), the keyboard fails to show and I get this error in the log...

2016-10-05 18:37:56.424457 TurbolinksDemo[25583:1766486] 0x61800014a3a0 Copy matching assets reply: XPC_TYPE_DICTIONARY  <dictionary: 0x61800014a3a0> { count = 1, transaction: 0, voucher = 0x0, contents =
    "Result" => <int64: 0x610000230100>: 29
}
2016-10-05 18:37:56.425750 TurbolinksDemo[25583:1766486] 0x618000149320 Daemon configuration query reply: XPC_TYPE_DICTIONARY  <dictionary: 0x618000149320> { count = 2, transaction: 0, voucher = 0x0, contents =
    "Dictionary" => <dictionary: 0x61800014a710> { count = 1, transaction: 0, voucher = 0x0, contents =
        "ServerURL" => <dictionary: 0x61800014a5b0> { count = 3, transaction: 0, voucher = 0x0, contents =
            "com.apple.CFURL.magic" => <uuid: 0x61800005f440> C3853DCC-9776-4114-B6C1-FD9F51944A6D
            "com.apple.CFURL.string" => <string: 0x6180002513d0> { length = 30, contents = "https://mesu.apple.com/assets/" }
            "com.apple.CFURL.base" => <null: 0x10d224f20>: null-object
        }
    }
    "Result" => <int64: 0x61800022b580>: 0
}
2016-10-05 18:37:56.426218 TurbolinksDemo[25583:1766486] [MobileAssetError:29] Unable to copy asset information from https://mesu.apple.com/assets/ for asset type com.apple.MobileAsset.TextInput.SpellChecker

I get the same error on a fresh Turbolinks based iOS project too using swift 2.3 and 3.

Any ideas?

"Undefined symbols for architecture x86_64" when migrating to swift 3

I've just tried a migration to swift 3 (using XCode's assistant and, after specifying the swift-3.0 branch in the Cartfile, running carthage update).

When compiling the project, I now get:

Undefined symbols for architecture x86_64:
  "direct field offset for Turbolinks.VisitableViewController.(visitableView.storage in _DF3372EDB6DA8B9C7F2112D8D315974E) : Turbolinks.VisitableView!?", referenced from:
      Turbolinks.VisitableViewController.visitableView.setter : Turbolinks.VisitableView! in VisitableViewController.o
      Turbolinks.VisitableViewController.(visitableView.materializeForSet : Turbolinks.VisitableView!).(closure #1) in VisitableViewController.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Has anyone seen something similar?

WebView page height is set too tall when the page doesn't fill the height

If the height of the html is shorter than the height of the webView, then the page acts as if it's taller than you'd expect. Basically, the height is 100% the height of the device, ignoring the nav bar and status bar. As soon as you try to scroll down, you'll see the "top" of the page slide under the nav bar and align to the top of the device.

You can reproduce this easily by going to server/lib/turbolinks_demo/views/one.erb and delete half the paragraphs. You can also reproduce this, by replacing layout.erb with https://gist.github.com/glennfu/ba776039e1377699eb81f75952865606. Using this you can see an example of a height: 100% page, with flex box stretching content to fill the page. The top looks like it starts in the right place, but the bottom extends beyond the bottom of the device.

Note that on turbolinks-android, everything behaves just fine in both scenarios. In addition, for better or worse (I think I like it), on turbolinks-android there's no scrolling enabled in this case.

I've been wrestling around with this myself for a while. Hopefully someone else here can help come up with a clean fix!

Oh one more thing: You CAN fix the problem by adding this to DemoViewController:

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        self.edgesForExtendedLayout = .None
    }

However then you have to be satisfied with the opaque nav bar, which is a bummer. Maybe that's a clue that helps point someone in the right direction for a better fix.

ActionCable

Doesn't appear to play nicely with ActionCable. Any chance this will work in the future?

Restore scroll position on session reload

After reloading a Turbolinks session, the scroll position is reset to the top. Is there a way to maintain this scroll position without manually setting it on turbolinks:load?

invalidating visit when popping navigation stack

Sorry if I missed where this was documented. Seems like a fairly common thing so I will pose it here.

I push a native view controller onto my navigation stack. When that view controller is popped, I know that the last visitable view controller I am going back to is going to need to be reloaded. Is there an easy way to do this?

Something is wrong with history stack restoring the wrong cache

So first, let me say I've implemented my flow much like the Demo in the sense of the AuthenticationController and the 401 response and all that.

Today I noticed a bug in my app, where I could look at the home screen while logged in, and see a login-specific element on the top of the HTML. I'd click a button to log out, which would perform Turbolinks.clearCache(); Turbolinks.visit... via the Rails redirect. So it would reload the page in the now logged out state. That login-specific element was properly removed. Now, I'd click on a page that required authorization, then click Cancel. The code for that Cancel button is:

popViewControllerAnimated(false)
dismissViewControllerAnimated(true, completion: nil)

Which triggers Session.swift's visitableViewWillAppear to call visitVisitable(visitable, action: .Restore).

The resulting behavior is that the page would "restore" to the way it looked when I was logged in. If I clicked that same link which required authorization, then Cancel again, it would "restore" to the way it looked when logged out. I can keep doing this to toggle infinitely.

My solution was to change that line in Session.swift to read visitVisitable(visitable, action: .Back) and enable this PR: turbolinks/turbolinks#76

This solves my problem completely. I also tested this multiple times without using that PR, and couldn't find a solution. I tried to reproduce this behavior in the Demo app but it would require implementing way too much and I'm not experienced enough with Sinatra to do all that so unfortunately I don't have a good repo for you to pull to see this yourself.

On a similar note, and I don't know if this is related bug/issue or what, but I noticed that window.history.length kept growing. I tested this in the turbolinks-ios Demo app and saw it there as well. Load up the Demo, and click the link "Open a new view controller" and then click "< Demo" and repeat. If you inspect in Safari, you can monitor and observe that window.history.length grows every time you do this. I'm pretty sure this shouldn't grow if you're just going back and forth to the same page, right?

DOM Exception 18 on Session.reload()

Calling reload() on a Session instance is raising the following error.

Error evaluating JavaScript function webView.changeHistoryForVisitWithIdentifier: Error: SecurityError: DOM Exception 18

I'm assuming this is an issue on my end, but I can't seem to track it down. Any pointers or ideas on where to start debugging?

install via cocoa pods in swift/x-code 7.3 throws error in turbolinks framework

I've just started a new project to test Turbolinks cocoa pod and it throws an error before even importing the project. here are some screenshots:

Inital project configs:
captura de tela 2016-09-19 as 16 11 29

Error in Turbolinks lib: (VisitableView.swift)
captura de tela 2016-09-19 as 16 11 36

I tried in 3 different projects with cocoa pods and without core data as the inital config.

I can't even import turbolinks to a file.....

cannot submit form or make post request?

I have a login form, and an app build with iOS adapter.

The form cannot be submitted when its inside a VisitableViewController,

what I need to do to make it submittable?

I've tried with a clean scaffold rails project, with rails 5 beta3, and a model Book. Below is the screenshot. The button is disabled, but nothing happens, and no post request is sent to server.

screen shot 2016-04-06 at 7 12 09 pm

Passing data to and from the native client

Turbolinks is an interesting hybrid platform because IIUC, it allows you to start quickly with web views and incrementally adopt native components on an as needed basis.

I have been running into some problems accomplishing this but it may just be a lack of understanding on my part or not reading the proper documentation. I feel I am a reasonably experienced Swift developer but have a fragile understanding of WebKit and Javascript.

After a user authenticates, I want to be able to grab a header in the token that comes back from the POST request so that I can issue my own native REST requests. I am not sure what the proper way to accomplish this is. Am I supposed do something in the visitDidComplete callback and possibly inject some Javascript into the WebView so I can serialize the token out?

It would be nice if there were documentation or something in the sample app that showed how to do this. I find the current sample app lacking on details here. Perhaps there currently is not a mechanism for doing this kind of data transfer. If so, would there be interest to add it? I am willing to work on it if I can get some help pointing me in the right direction about what approach would be acceptable.

Also, if this is not the appropriate place to discuss this, please let me know. Thank you!!

Toggling Navigation Bar's visibility

Hi,

On our Turbolinks-only app, we want to show the navigation bar on pages other than the welcome page. To do that, we've set the visitableDelegate of our VisitableViewController object to itself, and toggled the navigation bar using
self.navigationController.navigationBarHidden = true
in delegate's visitableViewWillAppear method. However, after transitioning to another controller, second page doesn't load, and when we go back to the first page, buttons stop working.

Is this the right place and method to do this?

How to login with omniauth-google-oauth2 or omniauth-facebook

I'm a newbie to swift and ios. I'm converting the rails app into turbolinks. Everything is fine, but when it comes to integrate login with facebook or google. It's just doesn't work. I understand that link is NOT turbolink url, therefore it doesn't work. It's under different UIViewController, AuthenticationController.

Is there any clue?

custom WKNavigationDelegate doesn't work

I'd like get some data from response headers, I think it's inside WKNavigation object. But my custom delegate not get called.

I've set my view controller as the delegate

func sessionDidLoadWebView(session: Session) {
        session.webView.navigationDelegate = self
}

here is the summary of my delegate

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
    if let _ = navigationAction.request.URL?.absoluteString.rangeOfString("itunes.apple.com") {
        UIApplication.sharedApplication().openURL(navigationAction.request.URL!)
        decisionHandler(WKNavigationActionPolicy.Cancel)
        return
    }
    decisionHandler(WKNavigationActionPolicy.Allow)
}

func webView(webView: WKWebView, didCommitNavigation navigation: WKNavigation!) {
    print("loading...")
}

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
    print("cool...")
}

the decidePolicyForNavigationAction method did get called, but not the other two,

Did I miss anything?

visitableDidRender is not getting called

This is probably a silly question since I am a just starting out on iOS development with Swift, but I cannot seem to define a visitableDidRender and actually have it ran. I am playing around with the demo application and added the following to DemoViewController, but visitableDidRender never seems to be called...

class DemoViewController: Turbolinks.VisitableViewController {
    // Snip...
    func visitableDidRender() {
        print("running visitableDidRender() in \(self)")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("running viewDidLoad() in \(self)")
    }
}

It looks like it should be called from Session. I modified the Session: VisitDelegate extension with the following and it makes me feel like I am putting visitableDidRender in the right place, on DemoViewController:

extension Session: VisitDelegate {
    // Snip...
    func visitDidInitializeWebView(visit: Visit) {
        initialized = true
        delegate?.sessionDidLoadWebView(self)
        print("calling visitableDidRender() from visitDidInitializeWebView() on \(visit.visitable)")
        visit.visitable.visitableDidRender()
    }

    func visitDidRender(visit: Visit) {
        visit.visitable.hideVisitableScreenshot()
        visit.visitable.hideVisitableActivityIndicator()
        print("calling visitableDidRender() from visitDidRender() on \(visit.visitable)")
        visit.visitable.visitableDidRender()
    }
}

This gives the following output when running the app:

running viewDidLoad() in <TurbolinksDemo.DemoViewController: 0x7ff9b941f540>
calling visitableDidRender() from visitDidInitializeWebView() on <TurbolinksDemo.DemoViewController: 0x7ff9b941f540>
calling visitableDidRender() from visitDidRender() on <TurbolinksDemo.DemoViewController: 0x7ff9b941f540>

I would expect to also see the output running visitableDidRender() in <TurbolinksDemo.DemoViewController: 0x7ff9b941f540>, but it seems that visitableDidRender is never actually ran on DemoViewController

I feel that I must be missing something either about how the lifecycle of a Turbolinks.VisitableViewController or Swift in general. Any thoughts? I'd be happy to contribute some extra documentation if I am misunderstanding the README

You can look at my fork to see the full context of what I have added: https://github.com/phil-monroe/turbolinks-ios

Cannot Build Turbolinks on Xcode 8.1

Xcode 8.1 throws the following error when trying to build Turbolinks via CocoaPods installation:

#{SRC_ROOT}/Pods/Turbolinks/Turbolinks/ScriptMessage.swift:1:8: Module file was created by an older version of the compiler; rebuild 'WebKit' and try again:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos/arm64/WebKit.swiftmodule

This can be reproduced by opening the swift-3.0 branch and trying to build the Turbolinks framework in Xcode 8.1.

Design advice: structuring an app to make modally presented view controllers work with the session

We’re getting started with a serious Turbolinks-iOS app. In our early work, it’s looking really great (thank you for sharing your code! 😄), but we’ve come across a hitch:

Our app has an initial UINavigationController subclass, very much like your demo app.

By default, it displays a “dashboard” screen with links to the various sections of our app.

Tapping those links will push in different UIViewController subclasses, which are custom to each section.

(The per-URL view controllers are built by a very simple “Router” object, whose job it is to take a URL and return an appropriate view controller instance for that URL).

In one of the app’s sections (“Tasks”), we have a “+” button in the navigation bar to create a new task. Since this will show a form, we take a common iOS approach: present a new modal UINavigationController with a TaskComposerViewController (a Turbolinks.VisitableViewController subclass) as its root VC:

func didTapAddTask(sender: UIBarButtonItem) {
        let composerVC = TaskComposerViewController()
        let navVC = UINavigationController(rootViewController: composerVC)
        presentViewController(navVC, animated: true, completion: nil)
}

This TaskComposer view controller sets its own visitableURL in its initializer. However, it can never end up displaying its web content, because it is now one step removed from the app’s initial UINavigationController, which is the delegate for the Turbolinks Session object. This means the task composer view controller (or its navVC) has no way to get a session.visit(visitable) to happen.

Do you have any suggestions for how to make a structure like this workable with turbolinks-ios? In essence, we need:

  1. To use custom view controllers to show the various pages in our app, so they can add extra items to the navigation bar (this one seems easy enough to do).
  2. To allow view controllers created and displayed outside of the original navigation controller’s stack to still somehow work with the Turbolinks session (this is where I’m a bit stuck).

I’ve been mulling over some potential arrangements to fix this:

  • Manually pass the session from VC to VC as they are created, reassigning its delegate as we go (but this feels like it might need a lot of code to be repeated across multiple VCs)
  • Make a “router” (or something similar) hold the Turbolinks session (and the other webkit things it needs), and have that router maintain a stack of pushed/presented VCs, which it can then set/unset as the session’s delegate when each VC is added or removed to/from the stack.

Both of these seem like they could get pretty finicky, which is why I thought I might run this query past you here first. In regular iOS app development, the “modally present a new navigation controller” pattern is pretty common, so I thought it might’ve been something you’ve already tackled in the Basecamp app or any other turbolinks-ios experiments of your own.

Is there any chance you could offer some pointers about what a good structure might be for using turoblinks-ios with the arrangement I described above?

Thank you for your help! 🙏 And if you’d like me to take a query like this elsewhere, I’d be happy to do that.

Unable to load page

I am trying to add Turbolinks-ios to an existing ios project. I have created a new view controller that is embedded in a navigation controller. I have added the code below and it does load the web display and begin spinning, but it does not load the contents of the webpage. I have confirmed that going to Safari in the simulator does load the page without issue.

import UIKit
import Turbolinks

class SignInView: UIViewController {
    var window: UIWindow?
    var session = Session()

    override func viewDidAppear(animated: Bool) {
        session.delegate = self
        visit(NSURL(string: "http://192.168.1.8:3000")!)
    }

    func visit(URL: NSURL) {
        let visitableViewController = VisitableViewController(URL: URL)
        navigationController!.pushViewController(visitableViewController, animated: true)
        session.visit(visitableViewController)
    }
}

extension SignInView: SessionDelegate {
    func session(session: Session, didProposeVisitToURL URL: NSURL, withAction action: Action) {
        visit(URL)
    }

    func session(session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
        let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .Alert)
        alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
        navigationController!.presentViewController(alert, animated: true, completion: nil)
    }
}

The page being loaded is just a simple page with a few assets. I also tried to test this against https://google.com and it just spins the same way.

JavaScript error: RangeError: Maximum call stack size exceeded. (undefined:7:22)

Any idea why I get this error every now and again when sending a JavaScript message back into the iOS app? The message still sends but the error randomly happens in the Xcode console. I'm sending it the URL to a PDF file on Amazon S3.

webkit.messageHandlers.printZPLFromURL.postMessage("<%= @order.label_url %>")

Sample @order.label_url:

https://mysite-staging.s3.amazonaws.com/store/aedeaddc74a888d57f0a6ffbd705c964.zpl?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=AKIAI23YKHM4O4BTX7GQ%2F20160906%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20160906T164220Z&amp;X-Amz-Expires=900&amp;X-Amz-SignedHeaders=host&amp;X-Amz-Signature=9b7c5bf2ab1a3335ae8bc924fesr3a23697a5bf8b6ff253773b83fa65a69b901

Also, any idea why it converts & to &amp;?

Websocket with ActionCable

Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)

Using turbolinks-ios in objective-c

Hi,

Has anyone managed to use turbolink-ios in their objective-c project?

Or if anyone has at least attempted to do this, could you please share your finding?

I couldn't figure out how to invoke session.visit(visitable) in my objective-c project.

Thanks

[Question] Is it possible to fire Turbolinks JavaScript events on a ColdBootVisit?

The first call to visit() on the Session creates a ColdBootVisit. In turn, this makes a standard network request via loadRequest() on WKWebView, without Turbolinks.

Because of this the turbolinks:request-start event is not fired. This makes it impossible to modify the XHR for the first request of any Turbolinks session. (I am trying to set custom HTTP headers for each XHR that is made through Turbolinks.)

Is there a workaround to achieve what I am trying to accomplish?

Unable to load Google Maps

Hello,

Has anyone successfully be able to get a turbolinks-ios app to display a google maps?

The map displays fine in the web browser, but when I fire it up in the iOS app the map doesn't display. It is just empty space. I have removed all javascript from the web pages except for the https://maps.googleapis.com/maps/api/js, jquery and turbolinks.

I just have the basic google maps js code to create the map.
var map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}

Again this works in the web browser but not the iOS app.

Thanks,

Freeze after dismissing a custom transition

Hi! Sorry I don't have a good test case for this yet. I am noticing that if I try to create a custom navigation transition and use:

viewController.modalPresentationStyle = .OverCurrentContext

After the modal VisitableViewController controller is dismissed, the VisitableViewController on the top of the navigation stack is frozen. If I inspect the view hierarchy after the dismissal I see:

screen shot 2016-10-14 at 3 15 01 pm

At this point I am dead in the water.

On the other hand. If I do not set the modalPresentationStyle everything works (except instead of the previous view controller, the background is totally black as expected)

screen shot 2016-10-14 at 3 22 36 pm

If I go outside of Turbolinks, I don't see any problems. I have run out of things to try. If anyone has any insights on what I might try to do restore the web view properly, I am all ears. Thanks.

Display PDF attachments in VisitableView

When a turbolinks page links to a pdf or other file that's not handled by turbolinks, currently the link is handled as external link and will open in safari rather than the embedded VisitableView.

Scenario

Consider a documents index. The document links refer to the actual pdfs.

# app/views/documents/index.html.haml
# ...
  = link_to document.title, document.file_url

The user clicks on such a link.

Observed behavior

The linked pdf is opened in safari rather than an embedded VisitableView. Unfortunately, safari asks for authentication, again. Furthermore, the user leaves the application.

Desired behavior

The pdf is displayed within the app, preferably within a VisitableView of turbolinks-ios.

Is this currently possible? Is this something, you would consider to implement?

Workaraounds?

  1. The README describes how to open the external link in a SFSafariViewController. Is this the way to go? Does this work without showing the url or other safari ui elements?

  2. I could have the link send a message to the mobile app via javascript as shown in the README: webkit.messageHandlers.show_pdf.postMessage(pdf_url). But how would I tell the VisitableView to just open that url?

    If I use presentVisitableForSession(session, URL: pdf_url, action: .Advance) as shown in the demo application, it takes a while, and then the pdf is shown as raw:

    bildschirmfoto 2016-08-17 um 13 06 01

  3. I've tried to use an iframe within a documents#show page rendered by turbolinks. But unfortunately, the iframe just shows the first page of the pdf. I suspect this might be a scrolling issue.

Thanks very much in advance for any hint you might have!

Removal of field navigation bar over keybaord

Is it possible to remove the field navigation bar (the arrows and the "done") when you focus a field in a webview? If not, I think that would be a lovely feature :)

Maybe a way to tie closing the keyboard to a native element so the bar is no longer me essay?

image

HTTP Basic Authentication and ColdBootVisits WKNavigationDelegate

My iOS application needs to load web views which require HTTP basic authentication. In WKWebView basic authentication is implemented in the WKNavigationDelegate, which according to the Turbolinks documentation can be set from within the sessionDidLoadWebView method of the SessionDelegate.

Upon investigation of the source code I found out however, that this takes place only after the first page load where the ColdBootVisit will always function as WKNavigationDelegate. Furthermore the ColdBootVisit is not open, so I cannot extend this class.

The respective WKNavigationDelegate method to be implemented is:
optional func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

What would be the best way in your opinion to add HTTP basic authentication capabilities to the Turbolinks-ios project? Is there a possible way which would work without modifying the turbolinks framework itself? Right now I have just manually modified the Visit implementation, but this is of course not a long term solution

Using with a Non-Turbolinks Web App

I am looking at adopting Turbolinks for future projects. I have a couple of existing apps, however, that are not yet updated to use Turbolinks. I notice that at present WebView.js relies on Turbolinks.controller. I managed to get a page of a non-turbolinks web-app to load by stubbing out Turbolinks.

var Turbolinks = Turbolinks || {controller:{restorationIdentifier:location.href} };

Does it make sense to try and get turbolinks-ios working with a non-turbolinks web app or is this going to painful?

form Post not working

Hello guys,

I'm using turbolinks-ios on my rails app.
My Rails app is using devise to manage user auth.

the problem is, if I just run visit(URL: URL(string: "http://localhost:3000")!) if works on the devise form, I put email and password, submit, and it redirects to root_path, but the links on the app, doesn't works.

But, when I use session.delegate = self to manage sessions, the devise form POST stop working, and links on the apps, start working.

Any Idea?

edit: When using session.delegate = self, and click on button, the server hasn't receiving nothing.

target="_blank" on href elements has no effect

Description
We have standard href link which is meant to take the user out of the app and to http://facebook.com/shared/sharer.php.

<a class="Share-link m-facebook" href="http://www.facebook.com/sharer/sharer.php?u=ourdomain&amp;title=blah blah" target="_blank"></a>

As we are using the same on our web app, we want the link to take the user to a new window, rather than reloading the current one (hence target="_blank"). This is working completely fine. With this on the tag, the iOS app receives no callback of any kind that a link has been clicked. Nothing from the WKWebView delegate methods is triggered. openExternalURL in the Session is never called.

If however, we remove target="_blank" then the link will open in an external safari window as expected. Our work around has been to pass a flag when the page is loaded from a native webview that will not apply the target tag to the element.

Is this expected functionality?

Turbolinks Demo - Build failed

I'm getting some kinds of errors when I'm trying to run/build the Turbolinks Demo app.
If someone can help me I would appreciate very much.

screen shot 2016-08-29 at 09 34 38

Best
Andrew

Setting custom headers

I was wondering if it is possible to set a custom header value? (For example X-DeviceToken).

Question: Offline capabilities

Hi,

thanks for the great work!

I will develop an app for a new project and the first page/view will fetch an image from an API and display it. It would be important that this image will be shown even if there is no internet connection (like loading it once when there is a connection available, and it will be displayed even when there is no internet connection every time afterwards). Would that be somehow possible using turbolinks?

Thanks!!

Disable bounce?

Is there a sanctioned way to disable the "bounce" within VisitableView?

I've disabled "pull to refresh", but the view still gets "pulled down" farther than it should, unlike a native app.

I see that VisitableView has a hiddenScrollView, but unfortunately it is private. Perhaps disabling this functionality should be supported?

Blank webview after backgrounding

I am seeing behavior that Turbolinks webviews get cleared after backgrounding the app or not using it for a while. The navbar still shows, but the webview is just blank. Pull to refresh still works and after reloading via pull to refresh, the web content is visible again.

Is this some issue with memory management? Any way to prevent this or reload webviews after they are "cleared"?

Demo app fails to show pages if main URL is changed to common websites

Hi there Turbolinkers – I really love the look of this framework and I'm hoping to adopt it for an app we're building. However, I'm having some trouble getting a basic app started, because our subclasses of VisitableViewController (even with zero customisations yet) are never showing the webpages we're trying to load. They do end up replacing the navigation controller's title with the web page's title, but the loading spinner never goes away and the webview never appears.

I was wondering if it was something I was doing wrong with the basic setup of turbolinks-ios in my own project, but I discovered that if you take the TurbolinksDemo app and change ApplicationController's URL to something like "http://google.com/" or "https://basecamp.com/", then the very same thing happens in the demo app too. You end up just looking at this:

screen shot 2016-04-11 at 11 30 25 pm

I'm still trying to dig around the library to see why this might be the case, but I thought I might file an issue in case it's an obvious fix to any of you as the authors. Thanks for your help!

relative URLs treated like external

Relative URLs open in native Safari unless you implement

func session(session: Session, openExternalURL URL: NSURL) { visit(URL) }

Consequently, real external URL clicks do not seem to hit this method.

double-call of visitableDidRender

When the app is first loaded, and any time a pull-to-refresh is triggered, visitableDidRender gets called twice. Is this intentional? This is easy to reproduce in the Demo. Just open DemoViewController and add:

    override func visitableDidRender() {
        super.visitableDidRender()
        print("visitableDidRender")
    }

You'll see it get called twice at the times I mentioned. When clicking a link, or navigating back, it's only called once as expected.

Following the callback chain it looks like ColdBootVisit didFinishNavigation gets called, triggering it, and then ColdBootVisit didLoadPageWithRestorationIdentifier gets called, triggering it again.

If this is intentional, is there a way to determine the difference in paths so that I can apply my changes in visitableDidRender only once?

Error Running Turbolinks iOS Demo

Hi Everybody!

I saw the talk from @sstephenson about Turbolinks 5. Now, I want to try some exercises before implementing this feature in my work. I want to be clear that I never have used X-Code, I clone this repository https://github.com/turbolinks/turbolinks-ios/ and I was trying to run the demo. I started the server with the Turbolinks 5 app but when I want to run the simulator X-Code is raising the next error and I don't know how to fix it. You can see the error on the next screenshot:

image

Can you help me please?

Would it be interesting to separate turbolinks 5-enabled web app/server requirement from the rest of it?

Given that I barely scratched the surface on this; I would like to know your comments about the work required to separate the native navigation and a single shared web view feature from the Turbolinks 5-enabled web app feature.

Reasoning: I like the idea of laying out my views using web views, but then I don’t always have a fully functional web[site/app]; instead sometimes all you need/have is a (JSON) API that returns some data.

This probably would defeat the purpose of turbolinks, yet you might avoid reloading JavaScript and CSS and definitely save memory by sharing one WKWebView. So the native navigation and a single shared web view feature is seriously 🆒 .

Not sure where I am headed this at this point, but I would appreciate to hear your thoughts and if could I maybe help getting there if there is enough interest to it.

🍻

Form submit causes view to advance.

Hello, I'm fairly new to Turbolinks but i've managed to create a simple application that logs a user in and shows a reserved area. It works fine on the web. I'm now trying to run the app with Turbolinks-iOS and whenever the login form validation fails, a new VisitableViewController is pushed onto the navigation stack because after a failed login attempt i redirect via turbolinks the user to the form with the validation errors on it. I've tried using the "replace" action instead of the "advance" but still a new VisitableViewController is pushed to the navigation stack.

Any ideas on how I could solve this? I don't want to push a new VisitableViewController onto the stack unless the action is set to "advance"

`history.back()` JS doesn't pop from navigation stack?

I have a setup like this:

Main page -> Form entry

Upon successful submission of the form, I return a JS response with history.back(); in there.

This seems to perform a turbolinks visit to the previous page instead of popping the stack. Should I be doing something other than history.back(); to get the desired effect, or is this a bug?

Turbolinks caching is off.

Saving session

Has any thought/effort happened so far toward saving sessions? It would be great if loading the app later didn't log you out. I tried implementing this myself but it seems that with WKWebView, managing cookies is a lot trickier than with the old UIWebView. This is the only online reference I found to anyone saying they were able to do it successfully: http://stackoverflow.com/a/32845148/1366620

Is saving session in the scope of the turbolinks-ios project? If so, is this kind of solution the right approach for making this work, or is something cleaner possible? I know I'm jumping on board the train before it's left the station so you might all have plans in place for this already, especially since I know it works in Basecamp 3, but I thought I'd share what I've found and ask!

Hiding the activity indicator

Here's what's currently in Session.swift:

func visitDidRender(visit: Visit) {
        visit.visitable.hideVisitableScreenshot()
        visit.visitable.hideVisitableActivityIndicator()
        visit.visitable.visitableDidRender()
    }

With this, the page is displayed before visitableDidRender() gets called. As I understand it, this is the only place for a bridge to hook into changing any native components on the screen for a page. I have a cast in my app where in response to something on the page, I construct a new native element near the top of the page. In doing so, the web view has to shift down. This causes a quick bounce in the visuals which is no good. Swapping the order of visitableDidRender and hideVisitableActivityIndicator fixes this. Now I get to control exactly when I'm ready for the page to be displayed.

Is this a good change? Do you see any problems with it? From my perspective, you can basically get the same original behavior if you really want it just by making sure your visitableDidRender is asynchronous.

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.