Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into preserve-visit-option…
Browse files Browse the repository at this point in the history
…s-response

* origin/main:
  Fix when snapshot cache is cleared (#185)
  Stale content (#184)
  Note feature parity in CONTRIBUTING doc
  Provide backwards compatibility support for Turbo 7.x and Turbolinks 5
  Support visit.isPageRefresh and avoid displaying the activity indicator and screenshots when the page is refreshing
  Support Turbo 8 same-page refreshes and include debug logging
  rename loadrRequest to load (#141)
  Add CONTRIBUTING.md (#90)
  Update README window.Turbo reference (#68)

# Conflicts:
#	Source/Session/Session.swift
  • Loading branch information
olivaresf committed Mar 1, 2024
2 parents da5fcec + 979b1a9 commit 7647a48
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 16 deletions.
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Contributing to turbo-ios

To set up your development environment:

1. Clone the repo
1. Open the directory in Xcode to install Swift packages

To run the test suite:

1. Open the directory in Xcode
1. Click Product → Test or <kbd>⌘</kbd>+<kbd>U</kbd>

## Feature parity with Android

New features will not be merged until also added to [turbo-android](https://github.com/hotwired/turbo-android).

This does not apply to bugs that only appear on iOS.
2 changes: 1 addition & 1 deletion Docs/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ If you need to make authenticated network requests from both the web and native
### Basecamp 3 and HEY
For Basecamp 3 and HEY, we perform authentication completely natively, and get an OAuth token back from our API. We then securely persist this token in the user's Keychain. This OAuth token is used for all network requests by setting a header in `URLSession`. This OAuth token is used for all native screens and extensions (share, widgets, today, watch, etc).

For Basecamp 3, when you load a web view for the first time in our app, we get a 401 from the server. We handle that response by making a special request in a hidden `WKWebView` to an endpoint on our server using our OAuth token which sets the appropriate cookies for the web view. When that request finishes successfully, we know the cookies are set and Turbo is ready to go. This only happens the first launch, as the web view cookies will be persisted as mentioned above. 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. If the web view is different from the Turbo web view, you'll need to also ensure they're using the same `WKProcessPool` so the cookies are shared.
For Basecamp 3, when you load a web view for the first time in our app, we get a 401 from the server. We handle that response by making a special request in a hidden `WKWebView` to an endpoint on our server using our OAuth token which sets the appropriate cookies for the web view. When that request finishes successfully, we know the cookies are set and Turbo is ready to go. This only happens the first launch, as the web view cookies will be persisted as mentioned above. The key to this strategy is to create a `URLRequest` using the OAuth header, and use that to load the web view calling `webView.load(request)` for the authentication request. If the web view is different from the Turbo web view, you'll need to also ensure they're using the same `WKProcessPool` so the cookies are shared.

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.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@

Turbo iOS is written in Swift 5.3 and requires iOS 14 or higher. It supports web apps using either Turbo 7 or Turbolinks 5. The Turbo iOS framework has no dependencies.

**Note:** You should understand how Turbo works with web applications in the browser before attempting to use Turbo iOS. See the [Turbo 7 documentation](https://github.com/hotwired/turbo) for details. Ensure that your web app sets the `window.Turbo` global variable as it's required by the native apps:
**Note:** You should understand how Turbo works with web applications in the browser before attempting to use Turbo iOS. See the [Turbo 7 documentation](https://github.com/hotwired/turbo) for details.

```javascript
import { Turbo } from "@hotwired/turbo-rails"
window.Turbo = Turbo
```

Make sure your web app sets the `window.Turbo` global variable as it's required by the native apps (set automatically by [turbo-rails](https://github.com/hotwired/turbo-rails)).

## Getting Started

The best way to get started with Turbo iOS to try out the demo app first to get familiar with the framework. The demo app walks you through all the basic Turbo flows as well as some advanced features. To run the demo, clone this repo and open `Demo/Demo.xcodeproj` in Xcode and run the Demo target. See [Demo/README.md](Demo/README.md) for more details about the demo. When you’re ready to start your own application, read through the rest of the documentation.
Expand All @@ -45,6 +46,8 @@ You can also integrate the framework manually if your prefer, such as by adding

## Contributing

Read the [contributing guide](/CONTRIBUTING.md) to learn how to set up your development environment.

Turbo iOS is open-source software, freely distributable under the terms of an [MIT-style license](LICENSE). The [source code is hosted on GitHub](https://github.com/hotwired/turbo-ios).
Development is sponsored by [37signals](https://37signals.com/).

Expand Down
29 changes: 28 additions & 1 deletion Source/Session/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class Session: NSObject {
/// Options behave differently if a response is provided.
private let visitOptionsHandler = VisitOptionsHandler()

private var isShowingStaleContent = false
private var isSnapshotCacheStale = false

/// Automatically creates a web view with the passed-in configuration
public convenience init(webViewConfiguration: WKWebViewConfiguration? = nil) {
self.init(webView: WKWebView(frame: .zero, configuration: webViewConfiguration ?? WKWebViewConfiguration()))
Expand Down Expand Up @@ -95,6 +98,18 @@ public class Session: NSObject {
bridge.clearSnapshotCache()
}

// MARK: Caching

/// Clear the snapshot cache the next time the visitable view appears.
public func markSnapshotCacheAsStale() {
isSnapshotCacheStale = true
}

/// Reload the `Session` the next time the visitable view appears.
public func markContentAsStale() {
isShowingStaleContent = true
}

// MARK: Visitable activation

private var activatedVisitable: Visitable?
Expand Down Expand Up @@ -168,12 +183,16 @@ extension Session: VisitDelegate {
}

func visitWillStart(_ visit: Visit) {
guard !visit.isPageRefresh else { return }

visit.visitable.showVisitableScreenshot()
activateVisitable(visit.visitable)
}

func visitDidStart(_ visit: Visit) {
guard !visit.hasCachedSnapshot else { return }
guard !visit.isPageRefresh else { return }

visit.visitable.showVisitableActivityIndicator()
}

Expand Down Expand Up @@ -215,7 +234,15 @@ extension Session: VisitableDelegate {
public func visitableViewWillAppear(_ visitable: Visitable) {
guard let topmostVisit = self.topmostVisit, let currentVisit = self.currentVisit else { return }

if visitable === topmostVisit.visitable && visitable.visitableViewController.isMovingToParent {
if isSnapshotCacheStale {
clearSnapshotCache()
isSnapshotCacheStale = false
}

if isShowingStaleContent {
reload()
isShowingStaleContent = false
} else if visitable === topmostVisit.visitable && visitable.visitableViewController.isMovingToParent {
// Back swipe gesture canceled
if topmostVisit.state == .completed {
currentVisit.cancel()
Expand Down
5 changes: 3 additions & 2 deletions Source/Visit/JavaScriptVisit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ final class JavaScriptVisit: Visit {
}

extension JavaScriptVisit: WebViewVisitDelegate {
func webView(_ webView: WebViewBridge, didStartVisitWithIdentifier identifier: String, hasCachedSnapshot: Bool) {
log("didStartVisitWithIdentifier", ["identifier": identifier, "hasCachedSnapshot": hasCachedSnapshot])
func webView(_ webView: WebViewBridge, didStartVisitWithIdentifier identifier: String, hasCachedSnapshot: Bool, isPageRefresh: Bool) {
log("didStartVisitWithIdentifier", ["identifier": identifier, "hasCachedSnapshot": hasCachedSnapshot, "isPageRefresh": isPageRefresh])
self.identifier = identifier
self.hasCachedSnapshot = hasCachedSnapshot
self.isPageRefresh = isPageRefresh

delegate?.visitDidStart(self)
}
Expand Down
1 change: 1 addition & 0 deletions Source/Visit/Visit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Visit: NSObject {
let location: URL

var hasCachedSnapshot: Bool = false
var isPageRefresh: Bool = false
private(set) var state: VisitState

init(visitable: Visitable, options: VisitOptions, bridge: WebViewBridge) {
Expand Down
2 changes: 2 additions & 0 deletions Source/WebView/ScriptMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ extension ScriptMessage {
case pageLoadFailed
case errorRaised
case visitProposed
case visitProposalScrollingToAnchor
case visitProposalRefreshingPage
case visitStarted
case visitRequestStarted
case visitRequestCompleted
Expand Down
8 changes: 6 additions & 2 deletions Source/WebView/WebViewBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protocol WebViewPageLoadDelegate: AnyObject {
}

protocol WebViewVisitDelegate: AnyObject {
func webView(_ webView: WebViewBridge, didStartVisitWithIdentifier identifier: String, hasCachedSnapshot: Bool)
func webView(_ webView: WebViewBridge, didStartVisitWithIdentifier identifier: String, hasCachedSnapshot: Bool, isPageRefresh: Bool)
func webView(_ webView: WebViewBridge, didStartRequestForVisitWithIdentifier identifier: String, date: Date)
func webView(_ webView: WebViewBridge, didCompleteRequestForVisitWithIdentifier identifier: String)
func webView(_ webView: WebViewBridge, didFailRequestForVisitWithIdentifier identifier: String, statusCode: Int)
Expand Down Expand Up @@ -125,8 +125,12 @@ extension WebViewBridge: ScriptMessageHandlerDelegate {
delegate?.webViewDidInvalidatePage(self)
case .visitProposed:
delegate?.webView(self, didProposeVisitToLocation: message.location!, options: message.options!)
case .visitProposalScrollingToAnchor:
break
case .visitProposalRefreshingPage:
break
case .visitStarted:
visitDelegate?.webView(self, didStartVisitWithIdentifier: message.identifier!, hasCachedSnapshot: message.data["hasCachedSnapshot"] as! Bool)
visitDelegate?.webView(self, didStartVisitWithIdentifier: message.identifier!, hasCachedSnapshot: message.data["hasCachedSnapshot"] as! Bool, isPageRefresh: message.data["isPageRefresh"] as! Bool)
case .visitRequestStarted:
visitDelegate?.webView(self, didStartRequestForVisitWithIdentifier: message.identifier!, date: message.date)
case .visitRequestCompleted:
Expand Down
20 changes: 12 additions & 8 deletions Source/WebView/turbo.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,18 @@
// Adapter interface

visitProposedToLocation(location, options) {
if (window.Turbo && typeof Turbo.navigator.locationWithActionIsSamePage === "function") {
if (Turbo.navigator.locationWithActionIsSamePage(location, options.action)) {
Turbo.navigator.view.scrollToAnchorFromLocation(location)
return
}
if (window.Turbo && Turbo.navigator.locationWithActionIsSamePage(location, options.action)) {
// Scroll to the anchor on the page
this.postMessage("visitProposalScrollingToAnchor", { location: location.toString(), options: options })
Turbo.navigator.view.scrollToAnchorFromLocation(location)
} else if (window.Turbo && Turbo.navigator.location?.href === location.href) {
// Refresh the page without native proposal
this.postMessage("visitProposalRefreshingPage", { location: location.toString(), options: options })
this.visitLocationWithOptionsAndRestorationIdentifier(location, options, Turbo.navigator.restorationIdentifier)
} else {
// Propose the visit
this.postMessage("visitProposed", { location: location.toString(), options: options })
}

this.postMessage("visitProposed", { location: location.toString(), options: options })
}

// Turbolinks 5
Expand All @@ -116,7 +120,7 @@

visitStarted(visit) {
this.currentVisit = visit
this.postMessage("visitStarted", { identifier: visit.identifier, hasCachedSnapshot: visit.hasCachedSnapshot() })
this.postMessage("visitStarted", { identifier: visit.identifier, hasCachedSnapshot: visit.hasCachedSnapshot(), isPageRefresh: visit.isPageRefresh || false })
this.issueRequestForVisitWithIdentifier(visit.identifier)
this.changeHistoryForVisitWithIdentifier(visit.identifier)
this.loadCachedSnapshotForVisitWithIdentifier(visit.identifier)
Expand Down

0 comments on commit 7647a48

Please sign in to comment.