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
@@ -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
|