motion-turbo-ios 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1,46 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Session
|
3
|
+
module WebViewDelegateMethods
|
4
|
+
def webView(webView, didProposeVisitToLocation: location, withOptions: options)
|
5
|
+
properties = pathConfiguration ? pathConfiguration.propertiesForURL(location) : {}
|
6
|
+
proposal = VisitProposal.alloc.initWithURL(location, options: options, properties: properties)
|
7
|
+
delegate.session(self, didProposeVisitProsal: proposal) if delegate
|
8
|
+
end
|
9
|
+
|
10
|
+
def webView(webView, didStartFormSubmissionToLocation: location)
|
11
|
+
delegate.sessionDidStartFormSubmission(self) if delegate
|
12
|
+
end
|
13
|
+
|
14
|
+
def webView(webView, didFinishFormSubmissionToLocation: location)
|
15
|
+
delegate.sessionDidFinishFormSubmission(self) if delegate
|
16
|
+
end
|
17
|
+
|
18
|
+
def webViewDidInvalidatePage(webView)
|
19
|
+
if topmostVisitable
|
20
|
+
topmostVisitable.updateVisitableScreenshot
|
21
|
+
topmostVisitable.showVisitableScreenshot
|
22
|
+
topmostVisitable.showVisitableActivityIndicator
|
23
|
+
reload
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initial page load failed, this will happen when we couldn't find Turbo JS on the page
|
28
|
+
def webView(webView, didFailInitialPageLoadWithError: error)
|
29
|
+
return unless currentVisit = self.currentVisit && !initialized
|
30
|
+
|
31
|
+
@initialized = false
|
32
|
+
currentVisit.cancel
|
33
|
+
visitDidFail(currentVisit)
|
34
|
+
visit(currentVisit, requestDidFailWithError: error)
|
35
|
+
end
|
36
|
+
|
37
|
+
def webView(webView, didFailJavaScriptEvaluationWithError: error)
|
38
|
+
return unless currentVisit = self.currentVisit && initialized
|
39
|
+
|
40
|
+
@initialized = false
|
41
|
+
currentVisit.cancel
|
42
|
+
visit(currentVisit.visitable)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Turbo
|
2
|
+
class TurboError < NSError
|
3
|
+
ERROR_DOMAIN = "com.basecamp.Turbolinks"
|
4
|
+
ERROR_CODES = {
|
5
|
+
http_failure: 0,
|
6
|
+
network_failure: -1,
|
7
|
+
content_type_mismatch: -2
|
8
|
+
}
|
9
|
+
attr_accessor :statusCode
|
10
|
+
|
11
|
+
def self.pageLoadFailure
|
12
|
+
localizedDescription = "The page could not be loaded due to a configuration error."
|
13
|
+
errorWithDomain(ERROR_DOMAIN, code: 123, userInfo: { NSLocalizedDescriptionKey => localizedDescription })
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.errorWithCode(code, localizedDescription: localizedDescription)
|
17
|
+
errorWithDomain(ERROR_DOMAIN, code: ERROR_CODES[code], userInfo: { NSLocalizedDescriptionKey => localizedDescription })
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.errorWithCode(code, statusCode: statusCode)
|
21
|
+
error = errorWithDomain(ERROR_DOMAIN, code: ERROR_CODES[code], userInfo: { "statusCode" => statusCode, NSLocalizedDescriptionKey => "HTTP Error: #{statusCode}" })
|
22
|
+
error.statusCode = statusCode
|
23
|
+
error
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.errorWithCode(code, error: error)
|
27
|
+
errorWithDomain(ERROR_DOMAIN, code: ERROR_CODES[code], userInfo: { "error" => error, NSLocalizedDescriptionKey => error.localizedDescription })
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Turbo
|
2
|
+
# A "Cold Boot" visit is the initial visit to load the page, including all resources
|
3
|
+
# Subsequent visits go through Turbo and use `JavaScriptVisit`
|
4
|
+
class ColdBootVisit < Visit
|
5
|
+
attr_reader :navigation
|
6
|
+
|
7
|
+
def startVisit
|
8
|
+
log("startVisit")
|
9
|
+
webView.navigationDelegate = self
|
10
|
+
bridge.pageLoadDelegate = self
|
11
|
+
|
12
|
+
request = NSURLRequest.alloc.initWithURL(location)
|
13
|
+
@navigation = webView.loadRequest(request)
|
14
|
+
|
15
|
+
delegate.visitDidStart(self) if delegate
|
16
|
+
startRequest
|
17
|
+
end
|
18
|
+
|
19
|
+
def cancelVisit
|
20
|
+
log("cancelVisit")
|
21
|
+
removeNavigationDelegate
|
22
|
+
webView.stopLoading
|
23
|
+
finishRequest
|
24
|
+
end
|
25
|
+
|
26
|
+
def completeVisit
|
27
|
+
log("completeVisit")
|
28
|
+
removeNavigationDelegate
|
29
|
+
delegate.visitDidInitializeWebView(self) if delegate
|
30
|
+
end
|
31
|
+
|
32
|
+
def failVisit
|
33
|
+
log("cancelVisit")
|
34
|
+
removeNavigationDelegate
|
35
|
+
finishRequest
|
36
|
+
end
|
37
|
+
|
38
|
+
def removeNavigationDelegate
|
39
|
+
if webView.navigationDelegate == self
|
40
|
+
webView.navigationDelegate = nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def log(name)
|
47
|
+
debugLog("[ColdBootVisit] #{name} #{location.absoluteString}")
|
48
|
+
end
|
49
|
+
|
50
|
+
# WKNavigationDelegate methods
|
51
|
+
|
52
|
+
public
|
53
|
+
|
54
|
+
def webView(webView, didFinishNavigation: navigation)
|
55
|
+
if navigation == self.navigation
|
56
|
+
finishRequest
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def webView(webView, decidePolicyForNavigationAction: navigationAction, decisionHandler: decisionHandler)
|
61
|
+
@navigationActionDecisionHandler = decisionHandler
|
62
|
+
# Ignore any clicked links before the cold boot finishes navigation
|
63
|
+
if navigationAction.navigationType == WKNavigationTypeLinkActivated
|
64
|
+
@navigationActionDecisionHandler.call(WKNavigationActionPolicyCancel)
|
65
|
+
if url = navigationAction.request.URL
|
66
|
+
UIApplication.sharedApplication.openURL(url)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
@navigationActionDecisionHandler.call(WKNavigationActionPolicyAllow)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def webView(webView, decidePolicyForNavigationResponse: navigationResponse, decisionHandler: decisionHandler)
|
74
|
+
if httpResponse = navigationResponse.response
|
75
|
+
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
|
76
|
+
decisionHandler.call(WKNavigationResponsePolicyAllow)
|
77
|
+
else
|
78
|
+
decisionHandler.call(WKNavigationResponsePolicyCancel)
|
79
|
+
fail(TurboError.errorWithCode(:http_failure, statusCode: httpResponse.statusCode))
|
80
|
+
end
|
81
|
+
else
|
82
|
+
decisionHandler.call(WKNavigationResponsePolicyCancel)
|
83
|
+
fail(TurboError.errorWithCode(:network_failure, localizedDescription: "An unknown error occurred"))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def webView(webView, didFailProvisionalNavigation: navigation, withError: originalError)
|
88
|
+
if navigation == self.navigation
|
89
|
+
fail(TurboError.errorWithCode(:network_failure, error: originalError))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def webView(webView, didFailNavigation: navigation, withError: originalError)
|
94
|
+
if navigation === self.navigation
|
95
|
+
fail(TurboError.errorWithCode(:network_failure, error: originalError))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# WebViewPageLoadDelegate
|
100
|
+
|
101
|
+
def webView(webView, didLoadPageWithRestorationIdentifier: restorationIdentifier)
|
102
|
+
@restorationIdentifier = restorationIdentifier
|
103
|
+
delegate.visitDidRender(self) if delegate
|
104
|
+
complete
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Turbo
|
2
|
+
# A JavaScript managed visit through the Turbo library
|
3
|
+
# All visits are JavaScriptVisits except the initial ColdBootVisit
|
4
|
+
# or if a reload() is issued
|
5
|
+
class JavaScriptVisit < Visit
|
6
|
+
attr_writer :identifier, :hasCachedSnapshot
|
7
|
+
def identifier
|
8
|
+
@identifier ||= "(pending)"
|
9
|
+
end
|
10
|
+
|
11
|
+
def initWithVisitable(visitable, options: options, bridge: bridge, restorationIdentifier: restorationIdentifier)
|
12
|
+
initWithVisitable(visitable, options: options, bridge: bridge)
|
13
|
+
self.restorationIdentifier = restorationIdentifier
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def description
|
18
|
+
"<#{dynamicType} #{identifier}: state=#{state} location=#{location}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
def startVisit
|
22
|
+
log("startVisit")
|
23
|
+
bridge.visitDelegate = self
|
24
|
+
bridge.visitLocation(location, withOptions: options, restorationIdentifier: restorationIdentifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
def cancelVisit
|
28
|
+
log("cancelVisit")
|
29
|
+
bridge.cancelVisitWithIdentifier(identifier)
|
30
|
+
finishRequest
|
31
|
+
end
|
32
|
+
|
33
|
+
def failVisit
|
34
|
+
log("failVisit")
|
35
|
+
finishRequest
|
36
|
+
end
|
37
|
+
|
38
|
+
# WebViewVisitDelegate
|
39
|
+
|
40
|
+
def webView(webView, didStartVisitWithIdentifier: identifier, hasCachedSnapshot: hasCachedSnapshot)
|
41
|
+
log("didStartVisitWithIdentifier", arguments: { identifier: identifier, hasCachedSnapshot: hasCachedSnapshot })
|
42
|
+
self.identifier = identifier
|
43
|
+
self.hasCachedSnapshot = hasCachedSnapshot
|
44
|
+
|
45
|
+
delegate.visitDidStart(self) if delegate
|
46
|
+
end
|
47
|
+
|
48
|
+
def webView(webView, didStartRequestForVisitWithIdentifier: identifier, date: date)
|
49
|
+
log("didStartRequestForVisitWithIdentifier", arguments: { identifier: identifier, date: date })
|
50
|
+
if identifier == self.identifier
|
51
|
+
startRequest
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def webView(webView, didCompleteRequestForVisitWithIdentifier: identifier)
|
56
|
+
log("didCompleteRequestForVisitWithIdentifier", arguments: { identifier: identifier })
|
57
|
+
if identifier == self.identifier
|
58
|
+
delegate.visitWillLoadResponse(self) if hasCachedSnapshot && delegate
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def webView(webView, didFailRequestForVisitWithIdentifier: identifier, statusCode: statusCode)
|
63
|
+
log("didCompleteRequestForVisitWithIdentifier", arguments: { identifier: identifier, statusCode: statusCode })
|
64
|
+
if identifier == self.identifier
|
65
|
+
# TODO implemented differently
|
66
|
+
if statusCode == 0
|
67
|
+
error = TurboError.errorWithCode(:network_failure, localizedDescription: "A network error occurred.")
|
68
|
+
else
|
69
|
+
error = TurboError.errorWithCode(:http_failure, statusCode: statusCode)
|
70
|
+
end
|
71
|
+
fail(error)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def webView(webView, didFinishRequestForVisitWithIdentifier: identifier, date: date)
|
76
|
+
log("didFinishRequestForVisitWithIdentifier", arguments: { identifier: identifier, date: date })
|
77
|
+
if identifier == self.identifier
|
78
|
+
finishRequest
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def webView(webView, didRenderForVisitWithIdentifier: identifier)
|
83
|
+
log("didRenderForVisitWithIdentifier", arguments: { identifier: identifier })
|
84
|
+
if identifier == self.identifier
|
85
|
+
delegate.visitDidRender(self) if delegate
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def webView(webView, didCompleteVisitWithIdentifier: identifier, restorationIdentifier: restorationIdentifier)
|
90
|
+
log("didCompleteVisitWithIdentifier", arguments: { identifier: identifier, restorationIdentifier: restorationIdentifier })
|
91
|
+
if identifier == self.identifier
|
92
|
+
@restorationIdentifier = restorationIdentifier
|
93
|
+
complete
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def log(name)
|
100
|
+
log(name, arguments: nil)
|
101
|
+
end
|
102
|
+
|
103
|
+
def log(name, arguments: arguments)
|
104
|
+
debugLog("[JavaScriptVisit] #{name} #{location.absoluteString}", arguments: arguments)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Turbo
|
2
|
+
class Visit
|
3
|
+
attr_accessor :delegate, :restorationIdentifier
|
4
|
+
attr_reader :visitable,
|
5
|
+
:webView,
|
6
|
+
:bridge,
|
7
|
+
:options,
|
8
|
+
:state,
|
9
|
+
:location,
|
10
|
+
:hasCachedSnapshot
|
11
|
+
|
12
|
+
def initWithVisitable(visitable, options: options, bridge: bridge)
|
13
|
+
raise visitable.visitableURL.inspect if visitable.visitableURL.is_a?(Hash)# TODO
|
14
|
+
raise options.inspect unless options.is_a?(VisitOptions)# TODO
|
15
|
+
@visitable = visitable
|
16
|
+
@location = visitable.visitableURL
|
17
|
+
@options = options
|
18
|
+
@bridge = bridge
|
19
|
+
@webView = @bridge.webView
|
20
|
+
@state = :initialized
|
21
|
+
@hasCachedSnapshot = false
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
if state == :initialized
|
27
|
+
delegate.visitWillStart(self) if delegate
|
28
|
+
@state = :started
|
29
|
+
startVisit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cancel
|
34
|
+
if state == :started
|
35
|
+
@state = :canceled
|
36
|
+
cancelVisit
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def complete
|
41
|
+
if state == :started
|
42
|
+
@state = :completed
|
43
|
+
completeVisit
|
44
|
+
delegate.visitDidComplete(self) if delegate
|
45
|
+
delegate.visitDidFinish(self) if delegate
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def fail(error)
|
50
|
+
if state == :started
|
51
|
+
@state = :failed
|
52
|
+
delegate.visit(self, requestDidFailWithError: error) if delegate
|
53
|
+
failVisit
|
54
|
+
delegate.visitDidFail(self) if delegate
|
55
|
+
delegate.visitDidFinish(self) if delegate
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Hooks for subclasses
|
60
|
+
def startVisit; end
|
61
|
+
def cancelVisit; end
|
62
|
+
def completeVisit; end
|
63
|
+
def failVisit; end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Request state
|
68
|
+
|
69
|
+
def requestStarted
|
70
|
+
@requestStarted ||= false
|
71
|
+
end
|
72
|
+
|
73
|
+
def requestFinished
|
74
|
+
@requestFinished ||= false
|
75
|
+
end
|
76
|
+
|
77
|
+
def startRequest
|
78
|
+
unless requestStarted
|
79
|
+
@requestStarted = true
|
80
|
+
delegate.visitRequestDidStart(self) if delegate
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def finishRequest
|
85
|
+
if requestStarted && !requestFinished
|
86
|
+
@requestFinished = true
|
87
|
+
delegate.visitRequestDidFinish(self) if delegate
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitOptions
|
3
|
+
attr_reader :action, :response
|
4
|
+
|
5
|
+
# TODO not used for now
|
6
|
+
def initWithAction(action, response: response)
|
7
|
+
@action = action
|
8
|
+
if response
|
9
|
+
@response = VisitResponse.alloc.initWithStatusCode(response["statusCode"], responseHTML: response["innerHTML"])
|
10
|
+
end
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def initFromHash(options)
|
15
|
+
raise "options is not a Hash" unless options.is_a?(Hash) # TODO remove?
|
16
|
+
@action = options["action"] || 'advance'
|
17
|
+
if response = options["response"]
|
18
|
+
@response = VisitResponse.alloc.initWithStatusCode(response["statusCode"], responseHTML: response["responseHTML"])
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO Codable, JSONCodable?
|
24
|
+
def encode
|
25
|
+
if response
|
26
|
+
{ action: @action, response: response.encode }
|
27
|
+
else
|
28
|
+
{ action: @action }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"#<VisitOptions action=#{action} response=#{response}>"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitProposal
|
3
|
+
attr_reader :url, :options, :properties
|
4
|
+
|
5
|
+
def initWithURL(url, options: options, properties: properties)
|
6
|
+
@url = url
|
7
|
+
raise unless options.is_a?(VisitOptions)
|
8
|
+
@options = options
|
9
|
+
@properties = properties || {}
|
10
|
+
self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitResponse
|
3
|
+
attr_reader :statusCode, :responseHTML
|
4
|
+
|
5
|
+
def initWithStatusCode(statusCode, responseHTML: responseHTML)
|
6
|
+
@statusCode = statusCode
|
7
|
+
@responseHTML = responseHTML
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO Codable, JSONCodable?
|
12
|
+
def encode
|
13
|
+
{ statusCode: statusCode, responseHTML: responseHTML }
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"#<VisitResponse statusCode=#{statusCode} responseHTML=#{responseHTML.inspect}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
#TODO public var isSuccessful: Bool {
|
21
|
+
#switch statusCode {
|
22
|
+
#case 200...299:
|
23
|
+
#return true
|
24
|
+
#default:
|
25
|
+
#return false
|
26
|
+
#}
|
27
|
+
#}
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Turbo
|
2
|
+
module Visitable
|
3
|
+
attr_accessor :visitableDelegate
|
4
|
+
attr_reader :visitableView, :visitableURL
|
5
|
+
|
6
|
+
def reloadVisitable
|
7
|
+
visitableDelegate.visitableDidRequestReload(self) if visitableDelegate
|
8
|
+
end
|
9
|
+
|
10
|
+
def showVisitableActivityIndicator
|
11
|
+
visitableView.showActivityIndicator
|
12
|
+
end
|
13
|
+
|
14
|
+
def hideVisitableActivityIndicator
|
15
|
+
visitableView.hideActivityIndicator
|
16
|
+
end
|
17
|
+
|
18
|
+
def activateVisitableWebView(webView)
|
19
|
+
visitableView.activateWebView(webView, forVisitable: self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def deactivateVisitableWebView
|
23
|
+
visitableView.deactivateWebView
|
24
|
+
end
|
25
|
+
|
26
|
+
def updateVisitableScreenshot
|
27
|
+
visitableView.updateScreenshot
|
28
|
+
end
|
29
|
+
|
30
|
+
def showVisitableScreenshot
|
31
|
+
visitableView.showScreenshot
|
32
|
+
end
|
33
|
+
|
34
|
+
def hideVisitableScreenshot
|
35
|
+
visitableView.hideScreenshot
|
36
|
+
end
|
37
|
+
|
38
|
+
def clearVisitableScreenshot
|
39
|
+
visitableView.clearScreenshot
|
40
|
+
end
|
41
|
+
|
42
|
+
def visitableWillRefresh
|
43
|
+
visitableView.refreshControl.beginRefreshing
|
44
|
+
end
|
45
|
+
|
46
|
+
def visitableDidRefresh
|
47
|
+
visitableView.refreshControl.endRefreshing
|
48
|
+
end
|
49
|
+
|
50
|
+
def visitableViewDidRequestRefresh
|
51
|
+
visitableDelegate.visitableDidRequestRefresh(self) if visitableDelegate
|
52
|
+
end
|
53
|
+
|
54
|
+
def visitableViewController
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitableView < UIView
|
3
|
+
include WebView
|
4
|
+
include Screenshots
|
5
|
+
include RefreshControl
|
6
|
+
include ActivityIndicator
|
7
|
+
include Constraints
|
8
|
+
include ScrollView
|
9
|
+
|
10
|
+
def initWithFrame(frame)
|
11
|
+
super
|
12
|
+
setup
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup
|
17
|
+
installActivityIndicatorView
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitableViewController < UIViewController
|
3
|
+
include Visitable
|
4
|
+
|
5
|
+
attr_accessor :visitableDelegate
|
6
|
+
attr_reader :visitableURL
|
7
|
+
|
8
|
+
def initWithURL(url)
|
9
|
+
if url.is_a? String
|
10
|
+
url = NSURL.alloc.initWithString(url)
|
11
|
+
end
|
12
|
+
@visitableURL = url
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# View Lifecycle methods
|
17
|
+
|
18
|
+
def viewDidLoad
|
19
|
+
super
|
20
|
+
installVisitableView
|
21
|
+
end
|
22
|
+
|
23
|
+
def viewWillAppear(animated)
|
24
|
+
super
|
25
|
+
visitableDelegate.visitableViewWillAppear(self) if visitableDelegate
|
26
|
+
end
|
27
|
+
|
28
|
+
def viewDidAppear(animated)
|
29
|
+
super
|
30
|
+
visitableDelegate.visitableViewDidAppear(self) if visitableDelegate
|
31
|
+
end
|
32
|
+
|
33
|
+
# Visitable
|
34
|
+
|
35
|
+
def visitableDidRender
|
36
|
+
self.title = visitableView.webView.title if visitableView.webView
|
37
|
+
end
|
38
|
+
|
39
|
+
def showVisitableActivityIndicator
|
40
|
+
visitableView.showActivityIndicator
|
41
|
+
end
|
42
|
+
|
43
|
+
def hideVisitableActivityIndicator
|
44
|
+
visitableView.hideActivityIndicator
|
45
|
+
end
|
46
|
+
|
47
|
+
# Visitable View
|
48
|
+
|
49
|
+
def visitableView
|
50
|
+
@visitableView ||= begin
|
51
|
+
view = VisitableView.alloc.initWithFrame(CGRectZero)
|
52
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
53
|
+
view
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def installVisitableView
|
60
|
+
view.addSubview(visitableView)
|
61
|
+
#view.insertSubview(visitableView, atIndex: 0)
|
62
|
+
self.edgesForExtendedLayout = UIRectEdgeAll # TODO document fix for fullscre
|
63
|
+
NSLayoutConstraint.activateConstraints([
|
64
|
+
visitableView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor),
|
65
|
+
visitableView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor),
|
66
|
+
visitableView.topAnchor.constraintEqualToAnchor(view.topAnchor),
|
67
|
+
visitableView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
|
68
|
+
])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitableView < UIView
|
3
|
+
module ActivityIndicator
|
4
|
+
def activityIndicatorView
|
5
|
+
@activityIndicatorView ||= begin
|
6
|
+
view = UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleMedium)
|
7
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
8
|
+
view.color = UIColor.grayColor
|
9
|
+
view.hidesWhenStopped = true
|
10
|
+
view
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def showActivityIndicator
|
15
|
+
if !isRefreshing
|
16
|
+
activityIndicatorView.startAnimating
|
17
|
+
bringSubviewToFront(activityIndicatorView)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def hideActivityIndicator
|
22
|
+
activityIndicatorView.stopAnimating
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def installActivityIndicatorView
|
28
|
+
addSubview(activityIndicatorView)
|
29
|
+
NSLayoutConstraint.activateConstraints([
|
30
|
+
activityIndicatorView.centerXAnchor.constraintEqualToAnchor(centerXAnchor),
|
31
|
+
activityIndicatorView.centerYAnchor.constraintEqualToAnchor(centerYAnchor)
|
32
|
+
])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Turbo
|
2
|
+
class VisitableView < UIView
|
3
|
+
module Constraints
|
4
|
+
def addFillConstraintsForSubview(view)
|
5
|
+
NSLayoutConstraint.activateConstraints([
|
6
|
+
view.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
|
7
|
+
view.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
|
8
|
+
view.topAnchor.constraintEqualToAnchor(topAnchor),
|
9
|
+
view.bottomAnchor.constraintEqualToAnchor(bottomAnchor)
|
10
|
+
])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|