motion-turbo-ios 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +48 -0
- data/lib/motion-turbo-ios.rb +15 -0
- data/lib/turbo/logging.rb +25 -0
- data/lib/turbo/path_configuration/path_configuration.rb +55 -0
- data/lib/turbo/path_configuration/path_configuration_decoder.rb +25 -0
- data/lib/turbo/path_configuration/path_configuration_loader.rb +62 -0
- data/lib/turbo/path_configuration/path_rule.rb +34 -0
- data/lib/turbo/session/navigation_delegate_methods.rb +43 -0
- data/lib/turbo/session/session.rb +184 -0
- data/lib/turbo/session/session_delegate_methods.rb +30 -0
- data/lib/turbo/session/visit_delegate_methods.rb +67 -0
- data/lib/turbo/session/visitable_delegate_methods.rb +51 -0
- data/lib/turbo/session/web_view_delegate_methods.rb +46 -0
- data/lib/turbo/turbo_error.rb +30 -0
- data/lib/turbo/visit/cold_boot_visit.rb +109 -0
- data/lib/turbo/visit/javascript_visit.rb +107 -0
- data/lib/turbo/visit/visit.rb +91 -0
- data/lib/turbo/visit/visit_options.rb +36 -0
- data/lib/turbo/visit/visit_proposal.rb +13 -0
- data/lib/turbo/visit/visit_response.rb +29 -0
- data/lib/turbo/visitable/visitable.rb +58 -0
- data/lib/turbo/visitable/visitable_view.rb +20 -0
- data/lib/turbo/visitable/visitable_view_controller.rb +71 -0
- data/lib/turbo/visitable_view/activity_indicator.rb +36 -0
- data/lib/turbo/visitable_view/constraints.rb +14 -0
- data/lib/turbo/visitable_view/refresh_control.rb +61 -0
- data/lib/turbo/visitable_view/screenshots.rb +56 -0
- data/lib/turbo/visitable_view/scroll_view.rb +21 -0
- data/lib/turbo/visitable_view/web_view.rb +28 -0
- data/lib/turbo/web_view/script_message.rb +77 -0
- data/lib/turbo/web_view/script_message_handler.rb +16 -0
- data/lib/turbo/web_view/web_view_bridge.rb +154 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e7a10ca24cbd7e3473c49d8721a90826036bc67af2e9871e2aa2c60ccb57ffcb
|
4
|
+
data.tar.gz: 065b1e687eb6e77bdf1305857a842485523fb2839c8d530f90bb0b236b20329d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9989b9032156906195c0ecb28a1e3db27a532dbb27188fb5e1d3109bccdc1c654bbd04595051a6a846754ef6fc2d4f20749540c86fa968f6ef190e551d597049
|
7
|
+
data.tar.gz: 123dc0c7a486ee91aaa9a3274cafc97938bf945da7feb41697b34d2f11ab66ad8c165ea74a6c6e9b63941398431f784b9611f59ed7b7597f68ea703cc4c44022
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Turbo Native iOS for RubyMotion
|
2
|
+
|
3
|
+
**Build high-fidelity hybrid apps with native navigation and a single shared web view.** Turbo Native iOS for RubyMotion provides the tooling to wrap your [Turbo 7](https://github.com/hotwired/turbo)-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 Turbo.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Deliver fast, efficient hybrid apps.** Avoid reloading JavaScript and CSS. Save memory by sharing one WKWebView.
|
8
|
+
- **Reuse mobile web views across platforms.** Create your views once, on the server, in HTML. Deploy them to iOS, [Android](https://github.com/hotwired/turbo-android), and mobile browsers simultaneously. Ship new features without waiting on App Store approval.
|
9
|
+
- **Enhance web views with native UI.** Navigate web views using native patterns. Augment web UI with native controls.
|
10
|
+
- **Produce large apps with small teams.** Achieve baseline HTML coverage for free. Upgrade to native views as needed.
|
11
|
+
|
12
|
+
### Features of Turbo Native iOS for RubyMotion
|
13
|
+
|
14
|
+
- **Ruby syntax. Native performance.** Build native iOS and Android apps using the Ruby syntax you know and love with the same performance as Swift and Java by using [RubyMotion](http://www.rubymotion.com/).
|
15
|
+
|
16
|
+
## Requirements
|
17
|
+
|
18
|
+
Turbo Native iOS for RubyMotion is compatible with all versions of RubyMotion.
|
19
|
+
|
20
|
+
**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:
|
21
|
+
|
22
|
+
```javascript
|
23
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
24
|
+
window.Turbo = Turbo
|
25
|
+
```
|
26
|
+
|
27
|
+
## Getting Started
|
28
|
+
|
29
|
+
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.xcworkspace` 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.
|
30
|
+
|
31
|
+
## Documentation
|
32
|
+
|
33
|
+
- [Quick Start](docs/QuickStartGuide.md)
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
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).
|
38
|
+
Development is sponsored by [Basecamp](https://basecamp.com/).
|
39
|
+
|
40
|
+
We welcome contributions in the form of bug reports, pull requests, or thoughtful discussions in the [GitHub issue tracker](https://github.com/hotwired/turbo-ios/issues).
|
41
|
+
|
42
|
+
Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). By participating in this project you agree to abide by its terms.
|
43
|
+
|
44
|
+
---
|
45
|
+
|
46
|
+
© 2020 Basecamp, LLC
|
47
|
+
|
48
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
unless defined?(Motion::Project::Config)
|
4
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
5
|
+
end
|
6
|
+
|
7
|
+
lib_dir_path = File.dirname(File.expand_path(__FILE__))
|
8
|
+
Motion::Project::App.setup do |app|
|
9
|
+
app.files.unshift(Dir.glob(File.join(lib_dir_path, "turbo/**/*.rb")))
|
10
|
+
app.resources_dirs.unshift(Dir.glob(File.join(lib_dir_path, "../resources")))
|
11
|
+
app.frameworks += ['WebKit']
|
12
|
+
end
|
13
|
+
|
14
|
+
module Turbo
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class TurboLog
|
2
|
+
def self.debugLoggingEnabled
|
3
|
+
true # TODO NSBundle.mainBundle.objectForInfoDictionaryKey('DEBUG')
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
def debugLog(message)
|
8
|
+
debugLog(message, arguments: {})
|
9
|
+
end
|
10
|
+
|
11
|
+
def debugLog(message, arguments: arguments)
|
12
|
+
timestamp = NSDate.new
|
13
|
+
|
14
|
+
log2("#{timestamp} #{message} #{arguments}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def debugPrint(message)
|
18
|
+
log2(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def log2(message)
|
22
|
+
if TurboLog.debugLoggingEnabled
|
23
|
+
NSLog(message)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Turbo
|
2
|
+
class PathConfiguration
|
3
|
+
|
4
|
+
attr_accessor :sources, :loader, :delegate, :rules, :settings
|
5
|
+
# Multiple sources will be loaded in order
|
6
|
+
# Remote sources should be last since they're loaded async
|
7
|
+
def initWithSources(sources)
|
8
|
+
self.sources = sources
|
9
|
+
self.rules = []
|
10
|
+
load
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a merged hash containing all the properties
|
15
|
+
# that match this url
|
16
|
+
# Note: currently only looks at path, not query, but most likely will
|
17
|
+
# add query support in the future, so it's best to always use this over the path variant
|
18
|
+
# unless you're sure you'll never need to reference other parts of the URL in the future
|
19
|
+
def propertiesForURL(url)
|
20
|
+
propertiesForPath(url.path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a merged hash containing all the properties
|
24
|
+
# that match this path
|
25
|
+
def propertiesForPath(path)
|
26
|
+
#source = NSString.stringWithContentsOfURL(url, encoding: NSUTF8StringEncoding, error: nil)
|
27
|
+
properties = {}
|
28
|
+
|
29
|
+
rules.each do |rule|
|
30
|
+
if rule.match(path)
|
31
|
+
properties.merge!(rule.properties)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
properties
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def load
|
40
|
+
loader = PathConfigurationLoader.alloc.initWithSources(sources)
|
41
|
+
if loader
|
42
|
+
loader.load do |config|
|
43
|
+
self.send(:update, config)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def update(config)
|
49
|
+
# Update our internal state with the config from the loader
|
50
|
+
self.settings = config.settings
|
51
|
+
self.rules = config.rules
|
52
|
+
delegate.pathConfigurationDidUpdate if delegate
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Turbo
|
2
|
+
class PathConfigurationDecoder
|
3
|
+
attr_accessor :settings, :rules
|
4
|
+
|
5
|
+
def initWithSettings(settings, rules: rules)
|
6
|
+
self.settings = settings
|
7
|
+
self.rules = rules
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def initWithJSON(json)
|
12
|
+
# rules must be present, settings are optional
|
13
|
+
#guard let rulesArray = json["rules"] as? [[String: AnyHashable]] else {
|
14
|
+
#throw JSONDecodingError.invalidJSON
|
15
|
+
#}
|
16
|
+
|
17
|
+
rules = json["rules"].map do |rule|
|
18
|
+
PathRule.alloc.initWithRule(rule)
|
19
|
+
end
|
20
|
+
settings = json["settings"]
|
21
|
+
|
22
|
+
initWithSettings(settings, rules: rules)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Turbo
|
2
|
+
class PathConfigurationLoader
|
3
|
+
PathConfigurationLoaderCompletionHandler = Class.new(PathConfigurationDecoder)
|
4
|
+
|
5
|
+
attr_accessor :sources, :completionHandler
|
6
|
+
def initWithSources(sources)
|
7
|
+
self.sources = sources
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(&completionHandler)
|
12
|
+
#completionHandler = PathConfigurationLoaderCompletionHandler.new
|
13
|
+
|
14
|
+
sources.each do |source|
|
15
|
+
#case source
|
16
|
+
#when .data(let data)
|
17
|
+
#data = loadData(source)
|
18
|
+
data = loadFile(source)
|
19
|
+
completionHandler.call(data)
|
20
|
+
|
21
|
+
#when .file(let url):
|
22
|
+
#loadFile(source)
|
23
|
+
#when .server(let url):
|
24
|
+
#download(from: url)
|
25
|
+
#end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def cacheDirectory
|
32
|
+
"Turbo"
|
33
|
+
end
|
34
|
+
|
35
|
+
def configurationCacheFilename
|
36
|
+
"path-configuration.json"
|
37
|
+
end
|
38
|
+
|
39
|
+
# MARK: - File
|
40
|
+
|
41
|
+
def loadFile(url)
|
42
|
+
#precondition(url.isFileURL, "URL provided for file is not a file url")
|
43
|
+
error_ptr = Pointer.new(:object)
|
44
|
+
data = NSData.alloc.initWithContentsOfURL(url, options:NSDataReadingUncached, error:error_ptr)
|
45
|
+
#begin
|
46
|
+
#data = File.read(url) #try Data(contentsOf: url)
|
47
|
+
loadData(data)
|
48
|
+
#end catch {
|
49
|
+
#debugPrint("[path-configuration] *** error loading configuration from file: \(url), error: \(error)")
|
50
|
+
#end
|
51
|
+
#end
|
52
|
+
end
|
53
|
+
|
54
|
+
# MARK: - Data
|
55
|
+
|
56
|
+
def loadData(json) #, cache: cache)
|
57
|
+
error_ptr = Pointer.new(:object)
|
58
|
+
data = NSJSONSerialization.JSONObjectWithData(json, options: 0, error: error_ptr)
|
59
|
+
PathConfigurationDecoder.alloc.initWithJSON(data)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Turbo
|
2
|
+
class PathRule
|
3
|
+
# Array of regular expressions to match against
|
4
|
+
attr_accessor :patterns
|
5
|
+
|
6
|
+
# The properties to apply for matches
|
7
|
+
attr_accessor :properties
|
8
|
+
|
9
|
+
# Convenience method to retrieve a String value for a key
|
10
|
+
# Access `properties` directly to get a different type
|
11
|
+
def subscript(key)
|
12
|
+
properties[key].to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def initWithRule(rule)
|
16
|
+
self.patterns = rule["patterns"]
|
17
|
+
self.properties = rule["properties"]
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns true if any pattern in this rule matches `path`
|
22
|
+
def match(path)
|
23
|
+
patterns.each do |pattern|
|
24
|
+
#guard let regex = try? NSRegularExpression(pattern: pattern) else { continue }
|
25
|
+
regex = %r(#{pattern})
|
26
|
+
|
27
|
+
if path =~ regex
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Session
|
3
|
+
module NavigationDelegateMethods
|
4
|
+
|
5
|
+
attr_accessor :navigationAction
|
6
|
+
|
7
|
+
def webView(webview, decidePolicyForNavigationAction: navigationAction, decisionHandler: decisionHandler)
|
8
|
+
navigationDecision = NavigationDecision(navigationAction: navigationAction)
|
9
|
+
decisionHandler(navigationDecision.policy)
|
10
|
+
|
11
|
+
if url = navigationDecision.externallyOpenableURL
|
12
|
+
openExternalURL(url)
|
13
|
+
elsif navigationDecision.shouldReloadPage
|
14
|
+
reload
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def policy
|
19
|
+
navigationAction.navigationType == WKNavigationTypeLinkActivated || isMainFrameNavigation ? WKNavigationResponsePolicyCancel : WKNavigationResponsePolicyAllow
|
20
|
+
end
|
21
|
+
|
22
|
+
def externallyOpenableURL
|
23
|
+
if url = navigationAction.request.url && shouldOpenURLExternally
|
24
|
+
url
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def shouldOpenURLExternally
|
29
|
+
type = navigationAction.navigationType
|
30
|
+
return type == WKNavigationTypeLinkActivated || (isMainFrameNavigation && type == WKNavigationTypeOther)
|
31
|
+
end
|
32
|
+
|
33
|
+
def shouldReloadPage
|
34
|
+
type = navigationAction.navigationType
|
35
|
+
return isMainFrameNavigation && type == WKNavigationTypeReload
|
36
|
+
end
|
37
|
+
|
38
|
+
def isMainFrameNavigation
|
39
|
+
navigationAction.targetFrame.isMainFrame if navigationAction.targetFrame
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Turbo
|
2
|
+
# A Session represents the main interface for managing
|
3
|
+
# a Turbo app in a web view. Each Session manages a single web view
|
4
|
+
# so you should create multiple sessions to have multiple web views, for example
|
5
|
+
# when using modals or tabs
|
6
|
+
class Session
|
7
|
+
include VisitDelegateMethods
|
8
|
+
include VisitableDelegateMethods
|
9
|
+
include WebViewDelegateMethods
|
10
|
+
include SessionDelegateMethods
|
11
|
+
include NavigationDelegateMethods
|
12
|
+
|
13
|
+
attr_accessor :delegate, :webView, :pathConfiguration, :bridge
|
14
|
+
attr_reader :initialized,
|
15
|
+
:refreshing,
|
16
|
+
:activatedVisitable,
|
17
|
+
:currentVisit,
|
18
|
+
:topmostVisit,
|
19
|
+
:topmostVisitable
|
20
|
+
|
21
|
+
def init
|
22
|
+
initWithConfiguration(WKWebViewConfiguration.alloc.init)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initWithConfiguration(configuration)
|
26
|
+
webViewConfiguration = configuration
|
27
|
+
#webView = WKWebView.alloc.initWithFrame(CGRectZero, configuration: webViewConfiguration)
|
28
|
+
webView = WKWebView.alloc.initWithFrame(CGRectMake(-50,050,450,100), configuration: webViewConfiguration)
|
29
|
+
webView.layer.borderColor = UIColor.blueColor
|
30
|
+
webView.layer.borderWidth = 2
|
31
|
+
|
32
|
+
initWithWebView(webView)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initWithWebView(webView)
|
36
|
+
@webView = webView
|
37
|
+
@initialized = false
|
38
|
+
@refreshing = false
|
39
|
+
setup
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def bridge
|
46
|
+
@bridge ||= WebViewBridge.alloc.initWithWebView(webView)
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup
|
50
|
+
webView.translatesAutoresizingMaskIntoConstraints = false
|
51
|
+
bridge.delegate = self
|
52
|
+
end
|
53
|
+
|
54
|
+
public
|
55
|
+
|
56
|
+
# The topmost visitable is the visitable that has most recently completed a visit
|
57
|
+
def topmostVisitable
|
58
|
+
topmostVisit.visitable if topmostVisit
|
59
|
+
end
|
60
|
+
|
61
|
+
# The active visitable is the visitable that currently owns the web view
|
62
|
+
def activeVisitable
|
63
|
+
activatedVisitable
|
64
|
+
end
|
65
|
+
|
66
|
+
def visitVisitable(visitable)
|
67
|
+
visitVisitable(visitable, options: nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
def visitVisitable(visitable, action: action)
|
71
|
+
visitVisitable(visitable, options: VisitOptions.alloc.initWithAction(action, response: nil))
|
72
|
+
end
|
73
|
+
|
74
|
+
def visitVisitable(visitable, options: options)
|
75
|
+
visitVisitable(visitable, options: options, reload: false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def visitVisitable(visitable, options: options, reload: reload)
|
79
|
+
# TODO raise instead?
|
80
|
+
raise "Visitable must provide a url! #{visitable}" unless visitable.visitableURL
|
81
|
+
|
82
|
+
visitable.visitableDelegate = self
|
83
|
+
|
84
|
+
if reload
|
85
|
+
@initialized = false
|
86
|
+
end
|
87
|
+
|
88
|
+
visit = makeVisit(visitable, options: options || VisitOptions.alloc.initFromHash({}))
|
89
|
+
@currentVisit.cancel if currentVisit
|
90
|
+
@currentVisit = visit
|
91
|
+
|
92
|
+
log("visit", { location: visit.location, options: visit.options, reload: reload })
|
93
|
+
|
94
|
+
visit.delegate = self
|
95
|
+
visit.start
|
96
|
+
end
|
97
|
+
|
98
|
+
def makeVisit(visitable, options: options)
|
99
|
+
if initialized
|
100
|
+
restorationIdentifier = restorationIdentifierForVisitable(visitable)
|
101
|
+
JavaScriptVisit.alloc.initWithVisitable(visitable, options: options, bridge: bridge, restorationIdentifier: restorationIdentifier)
|
102
|
+
else
|
103
|
+
ColdBootVisit.alloc.initWithVisitable(visitable, options: options, bridge: bridge)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def reload
|
108
|
+
return unless visitable = topmostVisitable
|
109
|
+
@initialized = false
|
110
|
+
visitVisitable(visitable)
|
111
|
+
@topmostVisit = currentVisit
|
112
|
+
end
|
113
|
+
|
114
|
+
def clearSnapshotCache
|
115
|
+
bridge.clearSnapshotCache
|
116
|
+
end
|
117
|
+
|
118
|
+
# Visitable activation
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def activateVisitable(visitable)
|
123
|
+
return if isActivatedVisitable(visitable)
|
124
|
+
|
125
|
+
deactivateActivatedVisitable
|
126
|
+
visitable.activateVisitableWebView(webView)
|
127
|
+
@activatedVisitable = visitable
|
128
|
+
end
|
129
|
+
|
130
|
+
def deactivateActivatedVisitable
|
131
|
+
if activatedVisitable
|
132
|
+
deactivateVisitable(activatedVisitable, showScreenshot: true)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def deactivateVisitable(visitable, showScreenshot: showScreenshot)
|
137
|
+
if visitable == activatedVisitable
|
138
|
+
if showScreenshot
|
139
|
+
visitable.updateVisitableScreenshot
|
140
|
+
visitable.showVisitableScreenshot
|
141
|
+
end
|
142
|
+
|
143
|
+
visitable.deactivateVisitableWebView
|
144
|
+
@activatedVisitable = nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def isActivatedVisitable(visitable)
|
149
|
+
visitable == activatedVisitable
|
150
|
+
end
|
151
|
+
|
152
|
+
# Visitable restoration identifiers
|
153
|
+
|
154
|
+
def visitableRestorationIdentifiers
|
155
|
+
@visitableRestorationIdentifiers ||= NSMapTable.weakToStrongObjectsMapTable
|
156
|
+
end
|
157
|
+
|
158
|
+
def restorationIdentifierForVisitable(visitable)
|
159
|
+
visitableRestorationIdentifiers.objectForKey(visitable.visitableViewController)
|
160
|
+
end
|
161
|
+
|
162
|
+
def storeRestorationIdentifier(restorationIdentifier, forVisitable: visitable)
|
163
|
+
visitableRestorationIdentifiers.setObject(restorationIdentifier, forKey: visitable.visitableViewController)
|
164
|
+
end
|
165
|
+
|
166
|
+
# MARK: - Navigation
|
167
|
+
|
168
|
+
def completeNavigationForCurrentVisit
|
169
|
+
if currentVisit
|
170
|
+
@topmostVisit = currentVisit
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def log(name)
|
177
|
+
log(name, {})
|
178
|
+
end
|
179
|
+
|
180
|
+
def log(name, arguments)
|
181
|
+
debugLog("[Session] #{name}", arguments: arguments)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Session
|
3
|
+
module SessionDelegateMethods
|
4
|
+
|
5
|
+
def sessionDidLoadWebView(session)
|
6
|
+
session.webView.navigationDelegate = session
|
7
|
+
end
|
8
|
+
|
9
|
+
def session(session, openExternalURL: url)
|
10
|
+
UIApplication.shared.open(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sessionDidStartRequest(session)
|
14
|
+
end
|
15
|
+
|
16
|
+
def sessionDidFinishRequest(session)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sessionDidStartFormSubmission(session)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sessionDidFinishFormSubmission(session)
|
23
|
+
end
|
24
|
+
|
25
|
+
def session(session, didReceiveAuthenticationChallenge: challenge, &completionHandler)
|
26
|
+
completionHandler.call(NSURLAuthenticationChallengeSender.performDefaultHandlingForAuthenticationChallenge, nil)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Session
|
3
|
+
module VisitDelegateMethods
|
4
|
+
def visitRequestDidStart(visit)
|
5
|
+
delegate.sessionDidStartRequest(self) if delegate
|
6
|
+
end
|
7
|
+
|
8
|
+
def visitRequestDidFinish(visit)
|
9
|
+
delegate.sessionDidFinishRequest(self) if delegate
|
10
|
+
end
|
11
|
+
|
12
|
+
def visit(visit, requestDidFailWithError: error)
|
13
|
+
delegate.session(self, didFailRequestForVisitable: visit.visitable, withError: error) if delegate
|
14
|
+
end
|
15
|
+
|
16
|
+
def visitDidInitializeWebView(visit)
|
17
|
+
@initialized = true
|
18
|
+
delegate.sessionDidLoadWebView(self) if delegate
|
19
|
+
end
|
20
|
+
|
21
|
+
def visitWillStart(visit)
|
22
|
+
visit.visitable.showVisitableScreenshot
|
23
|
+
activateVisitable(visit.visitable)
|
24
|
+
end
|
25
|
+
|
26
|
+
def visitDidStart(visit)
|
27
|
+
unless visit.hasCachedSnapshot
|
28
|
+
visit.visitable.showVisitableActivityIndicator
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def visitWillLoadResponse(visit)
|
33
|
+
visit.visitable.updateVisitableScreenshot
|
34
|
+
visit.visitable.showVisitableScreenshot
|
35
|
+
end
|
36
|
+
|
37
|
+
def visitDidRender(visit)
|
38
|
+
visit.visitable.hideVisitableScreenshot
|
39
|
+
visit.visitable.hideVisitableActivityIndicator
|
40
|
+
visit.visitable.visitableDidRender
|
41
|
+
end
|
42
|
+
|
43
|
+
def visitDidComplete(visit)
|
44
|
+
if restorationIdentifier = visit.restorationIdentifier
|
45
|
+
storeRestorationIdentifier(restorationIdentifier, forVisitable: visit.visitable)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def visitDidFail(visit)
|
50
|
+
visit.visitable.clearVisitableScreenshot
|
51
|
+
visit.visitable.showVisitableScreenshot
|
52
|
+
visit.visitable.hideVisitableActivityIndicator
|
53
|
+
end
|
54
|
+
|
55
|
+
def visitDidFinish(visit)
|
56
|
+
if refreshing
|
57
|
+
@refreshing = false
|
58
|
+
visit.visitable.visitableDidRefresh
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def visit(visit, didReceiveAuthenticationChallenge: challenge, &completionHandler)
|
63
|
+
delegate?.session(self, didReceiveAuthenticationChallenge: challenge, completionHandler: completionHandler)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Session
|
3
|
+
module VisitableDelegateMethods
|
4
|
+
def visitableViewWillAppear(visitable)
|
5
|
+
return unless topmostVisit && currentVisit
|
6
|
+
|
7
|
+
if visitable == topmostVisit.visitable && visitable.visitableViewController.isMovingToParentViewController
|
8
|
+
# Back swipe gesture canceled
|
9
|
+
if topmostVisit.state.to_sym == :completed
|
10
|
+
currentVisit.cancel
|
11
|
+
else
|
12
|
+
visitVisitable(visitable, action: :advance)
|
13
|
+
end
|
14
|
+
elsif visitable == currentVisit.visitable && currentVisit.state.to_sym == :started
|
15
|
+
# Navigating forward - complete navigation early
|
16
|
+
completeNavigationForCurrentVisit
|
17
|
+
elsif visitable != topmostVisit.visitable
|
18
|
+
# Navigating backward
|
19
|
+
visitVisitable(visitable, action: :restore)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def visitableViewDidAppear(visitable)
|
24
|
+
if currentVisit && visitable == currentVisit.visitable
|
25
|
+
# Appearing after successful navigation
|
26
|
+
completeNavigationForCurrentVisit
|
27
|
+
if currentVisit.state.to_sym != :failed
|
28
|
+
activateVisitable(visitable)
|
29
|
+
end
|
30
|
+
elsif topmostVisit && visitable == topmostVisit.visitable && topmostVisit.state == :completed
|
31
|
+
# Reappearing after canceled navigation
|
32
|
+
visitVisitable(visitable, action: :restore)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def visitableDidRequestReload(visitable)
|
37
|
+
if visitable == topmostVisitable
|
38
|
+
reload
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def visitableDidRequestRefresh(visitable)
|
43
|
+
if visitable == topmostVisitable
|
44
|
+
@refreshing = true
|
45
|
+
visitable.visitableWillRefresh
|
46
|
+
reload
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|