motion-turbo-ios 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +48 -0
  3. data/lib/motion-turbo-ios.rb +15 -0
  4. data/lib/turbo/logging.rb +25 -0
  5. data/lib/turbo/path_configuration/path_configuration.rb +55 -0
  6. data/lib/turbo/path_configuration/path_configuration_decoder.rb +25 -0
  7. data/lib/turbo/path_configuration/path_configuration_loader.rb +62 -0
  8. data/lib/turbo/path_configuration/path_rule.rb +34 -0
  9. data/lib/turbo/session/navigation_delegate_methods.rb +43 -0
  10. data/lib/turbo/session/session.rb +184 -0
  11. data/lib/turbo/session/session_delegate_methods.rb +30 -0
  12. data/lib/turbo/session/visit_delegate_methods.rb +67 -0
  13. data/lib/turbo/session/visitable_delegate_methods.rb +51 -0
  14. data/lib/turbo/session/web_view_delegate_methods.rb +46 -0
  15. data/lib/turbo/turbo_error.rb +30 -0
  16. data/lib/turbo/visit/cold_boot_visit.rb +109 -0
  17. data/lib/turbo/visit/javascript_visit.rb +107 -0
  18. data/lib/turbo/visit/visit.rb +91 -0
  19. data/lib/turbo/visit/visit_options.rb +36 -0
  20. data/lib/turbo/visit/visit_proposal.rb +13 -0
  21. data/lib/turbo/visit/visit_response.rb +29 -0
  22. data/lib/turbo/visitable/visitable.rb +58 -0
  23. data/lib/turbo/visitable/visitable_view.rb +20 -0
  24. data/lib/turbo/visitable/visitable_view_controller.rb +71 -0
  25. data/lib/turbo/visitable_view/activity_indicator.rb +36 -0
  26. data/lib/turbo/visitable_view/constraints.rb +14 -0
  27. data/lib/turbo/visitable_view/refresh_control.rb +61 -0
  28. data/lib/turbo/visitable_view/screenshots.rb +56 -0
  29. data/lib/turbo/visitable_view/scroll_view.rb +21 -0
  30. data/lib/turbo/visitable_view/web_view.rb +28 -0
  31. data/lib/turbo/web_view/script_message.rb +77 -0
  32. data/lib/turbo/web_view/script_message_handler.rb +16 -0
  33. data/lib/turbo/web_view/web_view_bridge.rb +154 -0
  34. metadata +90 -0
@@ -0,0 +1,61 @@
1
+ module Turbo
2
+ class VisitableView < UIView
3
+ module RefreshControl
4
+ def refreshControl
5
+ @refreshControl ||= begin
6
+ refreshControl = UIRefreshControl.alloc.init
7
+ refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEventValueChanged)
8
+ refreshControl
9
+ end
10
+ end
11
+
12
+ def allowsPullToRefresh
13
+ return @allowsPullToRefresh if defined?(@allowsPullToRefresh)
14
+ @allowsPullToRefresh = true
15
+ end
16
+
17
+ def allowsPullToRefresh=(allowsPullToRefresh)
18
+ @allowsPullToRefresh = allowsPullToRefresh
19
+ if allowsPullToRefresh
20
+ installRefreshControl
21
+ else
22
+ removeRefreshControl
23
+ end
24
+ end
25
+
26
+ def isRefreshing
27
+ refreshControl.refreshing?
28
+ end
29
+
30
+ def refresh(sender)
31
+ visitable.visitableViewDidRequestRefresh if visitable
32
+ end
33
+
34
+ private
35
+
36
+ def installRefreshControl
37
+ scrollView = webView.scrollView if webView
38
+ return unless scrollView && allowsPullToRefresh
39
+ # TODO
40
+ #if !targetEnvironment(macCatalyst)
41
+ scrollView.addSubview(refreshControl)
42
+
43
+ # Infer refresh control's default height from its frame, if given.
44
+ # Otherwise fallback to 60 (the default height).
45
+ refreshControlHeight = CGRectGetHeight(refreshControl.frame) > 0 ? CGRectGetHeight(refreshControl.frame) : 60
46
+ NSLayoutConstraint.activateConstraints([
47
+ refreshControl.centerXAnchor.constraintEqualToAnchor(centerXAnchor),
48
+ refreshControl.topAnchor.constraintEqualToAnchor(safeAreaLayoutGuide.topAnchor),
49
+ refreshControl.heightAnchor.constraintEqualToConstant(refreshControlHeight)
50
+ ])
51
+ #endif
52
+ end
53
+
54
+ def removeRefreshControl
55
+ refreshControl.endRefreshing
56
+ #refreshControl.removeFromSuperview
57
+ webView.scrollView.refreshControl = nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ module Turbo
2
+ class VisitableView < UIView
3
+ module Screenshots
4
+ def screenshotContainerView
5
+ @screenshotContainerView ||= begin
6
+ view = UIView.alloc.initWithFrame(CGRectZero)
7
+ view.translatesAutoresizingMaskIntoConstraints = false
8
+ view.backgroundColor = backgroundColor
9
+ view
10
+ end
11
+ end
12
+
13
+ attr_reader :screenshotView
14
+
15
+ def isShowingScreenshot
16
+ screenshotContainerView.superview != nil
17
+ end
18
+
19
+ def updateScreenshot
20
+ return unless webView
21
+ return if isShowingScreenshot
22
+ screenshot = webView.snapshotViewAfterScreenUpdates(false)
23
+ return unless screenshot
24
+
25
+ screenshotView.removeFromSuperview if screenshotView
26
+ screenshot.translatesAutoresizingMaskIntoConstraints = false
27
+ screenshotContainerView.addSubview(screenshot)
28
+
29
+ NSLayoutConstraint.activateConstraints([
30
+ screenshot.centerXAnchor.constraintEqualToAnchor(screenshotContainerView.centerXAnchor),
31
+ screenshot.topAnchor.constraintEqualToAnchor(screenshotContainerView.topAnchor),
32
+ screenshot.widthAnchor.constraintEqualToConstant(screenshot.bounds.size.width),
33
+ screenshot.heightAnchor.constraintEqualToConstant(screenshot.bounds.size.height)
34
+ ])
35
+ @screenshotView = screenshot
36
+ end
37
+
38
+ def showScreenshot
39
+ if !isShowingScreenshot && !isRefreshing
40
+ addSubview(screenshotContainerView)
41
+ addFillConstraintsForSubview(screenshotContainerView)
42
+ showOrHideWebView
43
+ end
44
+ end
45
+
46
+ def hideScreenshot
47
+ screenshotContainerView.removeFromSuperview
48
+ showOrHideWebView
49
+ end
50
+
51
+ def clearScreenshot
52
+ screenshotView.removeFromSuperview if screenshotView
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ module Turbo
2
+ class VisitableView < UIView
3
+ module ScrollView
4
+ private
5
+
6
+ def hiddenScrollView
7
+ @hiddenScrollView ||= begin
8
+ scrollView = UIScrollView.alloc.initWithFrame(CGRectZero)
9
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
10
+ scrollView.scrollsToTop = false
11
+ scrollView
12
+ end
13
+ end
14
+
15
+ def installHiddenScrollView
16
+ insertSubview(hiddenScrollView, atIndex: 0)
17
+ addFillConstraintsForSubview(hiddenScrollView)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module Turbo
2
+ class VisitableView < UIView
3
+ module WebView
4
+ attr_reader :webView, :visitable
5
+
6
+ def activateWebView(webView, forVisitable: visitable)
7
+ @webView = webView
8
+ @visitable = visitable
9
+ #addSubview(webView)
10
+ insertSubview(webView, atIndex: 0)
11
+ addFillConstraintsForSubview(webView)
12
+ installRefreshControl
13
+ showOrHideWebView
14
+ end
15
+
16
+ def deactivateWebView
17
+ removeRefreshControl
18
+ webView.removeFromSuperview if webView
19
+ @webView = nil
20
+ @visitable = nil
21
+ end
22
+
23
+ def showOrHideWebView
24
+ webView.hidden = isShowingScreenshot if webView
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ module Turbo
2
+ class ScriptMessage
3
+ NAMES = {
4
+ page_loaded: "pageLoaded",
5
+ page_load_failed: "pageLoadFailed",
6
+ error_raised: "errorRaised",
7
+ visit_proposed: "visitProposed",
8
+ visit_started: "visitStarted",
9
+ visit_request_started: "visitRequestStarted",
10
+ visit_request_completed: "visitRequestCompleted",
11
+ visit_request_failed: "visitRequestFailed",
12
+ visit_request_finished: "visitRequestFinished",
13
+ visit_rendered: "visitRendered",
14
+ visit_completed: "visitCompleted",
15
+ form_submission_started: "formSubmissionStarted",
16
+ form_submission_finished: "formSubmissionFinished",
17
+ page_invalidated: "pageInvalidated",
18
+ log: "log"
19
+ }
20
+
21
+ def self.parse(message)
22
+ body = message.body
23
+ return unless body
24
+
25
+ rawName = body["name"]
26
+ return unless rawName
27
+
28
+ name = NAMES.key(rawName)
29
+ return unless name
30
+
31
+ data = body["data"]
32
+ return unless data
33
+
34
+ return new(name, data)
35
+ end
36
+
37
+ attr_reader :name, :data
38
+
39
+ def initialize(name, data)
40
+ @name = name
41
+ @data = data
42
+ end
43
+
44
+ def identifier
45
+ identifier = data["identifier"]
46
+ identifier if identifier.is_a?(String)
47
+ end
48
+
49
+ # Milliseconds since unix epoch as provided by JavaScript Date.now()
50
+ def timestamp
51
+ #data["timestamp"] as? TimeInterval ?? 0
52
+ timestamp = data["timestamp"]
53
+ timestamp.to_i || 0
54
+ end
55
+
56
+ def date
57
+ NSDate.alloc.initWithTimeIntervalSince1970(timestamp / 1000.0)
58
+ end
59
+
60
+ def restorationIdentifier
61
+ restorationIdentifier = data["restorationIdentifier"]
62
+ restorationIdentifier if restorationIdentifier.is_a?(String)
63
+ end
64
+
65
+ def location
66
+ NSURL.alloc.initWithString(data["location"]) if data["location"]
67
+ end
68
+
69
+ def options
70
+ if options = data["options"]
71
+ VisitOptions.alloc.initFromHash(options)
72
+ else
73
+ nil
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ module Turbo
2
+ # This class prevents retain cycle caused by WKUserContentController
3
+ class ScriptMessageHandler
4
+
5
+ attr_accessor :delegate
6
+
7
+ def initWithDelegate(delegate)
8
+ self.delegate = delegate
9
+ self
10
+ end
11
+
12
+ def userContentController(userContentController, didReceiveScriptMessage: message)
13
+ delegate.scriptMessageHandlerDidReceiveMessage(message) if delegate
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,154 @@
1
+ module Turbo
2
+ # The WebViewBridge is an internal class used for bi-directional communication
3
+ # with the web view/JavaScript
4
+ class WebViewBridge
5
+ attr_accessor :webView, :delegate, :pageLoadDelegate, :visitDelegate#, :navigationDelegate
6
+ def initWithWebView(webView)
7
+ @webView = webView
8
+ setup
9
+
10
+ self
11
+ end
12
+
13
+ private
14
+
15
+ def setup
16
+ messageHandlerName = "turbo"
17
+ webView.configuration.userContentController.addUserScript(userScript)
18
+ scriptMessageHandler = ScriptMessageHandler.alloc.initWithDelegate(self)
19
+ webView.configuration.userContentController.addScriptMessageHandler(scriptMessageHandler, name: messageHandlerName)
20
+ end
21
+
22
+ def userScript
23
+ url = self.class.bundle.URLForResource("turbo", withExtension: "js")
24
+ source = NSString.stringWithContentsOfURL(url, encoding: NSUTF8StringEncoding, error: nil)
25
+ WKUserScript.alloc.initWithSource(source, injectionTime: WKUserScriptInjectionTimeAtDocumentEnd, forMainFrameOnly: true)
26
+ end
27
+
28
+ def self.bundle
29
+ @bundle ||= NSBundle.bundleForClass(self)
30
+ end
31
+
32
+ public
33
+
34
+ def visitLocation(location, withOptions: options, restorationIdentifier: restorationIdentifier)
35
+ raise unless options.is_a? Turbo::VisitOptions
36
+ callJavaScriptFunction("window.turboNative.visitLocationWithOptionsAndRestorationIdentifier",
37
+ withArguments: [
38
+ location.absoluteString,
39
+ options.encode,
40
+ restorationIdentifier]
41
+ )
42
+ end
43
+
44
+ def clearSnapshotCache
45
+ callJavaScriptFunction("window.turboNative.clearSnapshotCache", withArguments: [])
46
+ end
47
+
48
+ def cancelVisitWithIdentifier(identifier)
49
+ callJavaScriptFunction("window.turboNative.cancelVisitWithIdentifier", withArguments: [identifier])
50
+ end
51
+
52
+ # JavaScript Evaluation
53
+
54
+ def callJavaScriptFunction(functionExpression, withArguments: arguments)
55
+ callJavaScriptFunction(functionExpression, withArguments: arguments, completionHandler: nil)
56
+ end
57
+
58
+ def callJavaScriptFunction(functionExpression, withArguments: arguments, completionHandler: completionHandler)
59
+ script = scriptForCallingJavaScriptFunction(functionExpression, withArguments: arguments)
60
+ unless script
61
+ NSLog("Error encoding arguments for JavaScript function `%@'", functionExpression)
62
+ return
63
+ end
64
+
65
+ debugLog("[Bridge] → #{functionExpression} #{arguments}")
66
+
67
+ webView.evaluateJavaScript(script, completionHandler: -> (result, error) {
68
+ debugLog("[Bridge] = #{functionExpression} evaluation complete")
69
+
70
+ if result
71
+ if error = result["error"]
72
+ stack = result["stack"]
73
+ NSLog("Error evaluating JavaScript function `%@': %@\n%@", functionExpression, error, stack)
74
+ else
75
+ completionHandler.call(result["value"]) if completionHandler
76
+ end
77
+ elsif error
78
+ delegate.webView(self, didFailJavaScriptEvaluationWithError: error) if delegate
79
+ end
80
+ })
81
+ end
82
+
83
+ def scriptForCallingJavaScriptFunction(functionExpression, withArguments: arguments)
84
+ encodedArguments = encodeJavaScriptArguments(arguments)
85
+ return unless encodedArguments
86
+
87
+ script = "(function(result) {\n" +
88
+ " try {\n" +
89
+ " result.value = " + functionExpression + "(" + encodedArguments + ")\n" +
90
+ " } catch (error) {\n" +
91
+ " result.error = error.toString()\n" +
92
+ " result.stack = error.stack\n" +
93
+ " }\n" +
94
+ " return result\n" +
95
+ "})({})"
96
+ return script
97
+ end
98
+
99
+ def encodeJavaScriptArguments(arguments)
100
+ arguments = arguments.map {|v| v.nil? ? NSNull.alloc.init() : v }
101
+
102
+ data = NSJSONSerialization.dataWithJSONObject(arguments, options: 0, error: nil)
103
+ if data
104
+ dataString = NSString.alloc.initWithData(data, encoding: NSUTF8StringEncoding)
105
+ return dataString[1..-2]
106
+ end
107
+ return nil
108
+ end
109
+
110
+ def scriptMessageHandlerDidReceiveMessage(message)
111
+ message = ScriptMessage.parse(message)
112
+ return unless message
113
+
114
+ if message.name.to_sym != :log
115
+ debugLog("[Bridge] ← #{message.name} #{message.data}")
116
+ end
117
+
118
+ case message.name.to_sym
119
+ when :page_loaded
120
+ pageLoadDelegate.webView(self, didLoadPageWithRestorationIdentifier: message.restorationIdentifier) if pageLoadDelegate
121
+ when :page_load_failed
122
+ delegate.webView(self, didFailInitialPageLoadWithError: TurboError.pageLoadFailure) if delegate
123
+ when :form_submission_started
124
+ delegate.webView(self, didStartFormSubmissionToLocation: message.location) if delegate
125
+ when :form_submission_finished
126
+ delegate.webView(self, didFinishFormSubmissionToLocation: message.location) if delegate
127
+ when :page_invalidated
128
+ delegate.webViewDidInvalidatePage(self) if delegate
129
+ when :visit_proposed
130
+ delegate.webView(self, didProposeVisitToLocation: message.location, withOptions: message.options) if delegate
131
+ when :visit_started
132
+ visitDelegate.webView(self, didStartVisitWithIdentifier: message.identifier, hasCachedSnapshot: message.data["hasCachedSnapshot"]) if visitDelegate
133
+ when :visit_request_started
134
+ visitDelegate.webView(self, didStartRequestForVisitWithIdentifier: message.identifier, date: message.date) if visitDelegate
135
+ when :visit_request_completed
136
+ visitDelegate.webView(self, didCompleteRequestForVisitWithIdentifier: message.identifier) if visitDelegate
137
+ when :visit_request_failed
138
+ visitDelegate.webView(self, didFailRequestForVisitWithIdentifier: message.identifier, statusCode: message.data["statusCode"]) if visitDelegate
139
+ when :visit_request_finished
140
+ visitDelegate.webView(self, didFinishRequestForVisitWithIdentifier: message.identifier, date: message.date) if visitDelegate
141
+ when :visit_rendered
142
+ visitDelegate.webView(self, didRenderForVisitWithIdentifier: message.identifier) if visitDelegate
143
+ when :visit_completed
144
+ visitDelegate.webView(self, didCompleteVisitWithIdentifier: message.identifier, restorationIdentifier: message.restorationIdentifier) if visitDelegate
145
+ when :error_raised
146
+ error = message.data["error"] || "<unknown error>"
147
+ debugLog("JavaScript error: #{error}")
148
+ when :log
149
+ msg = message.data["message"]
150
+ debugLog("[Bridge] ← log: #{msg}") if msg.is_a?(String)
151
+ end
152
+ end
153
+ end
154
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: motion-turbo-ios
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Havens
8
+ - Petrik de Heus
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2023-06-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ description: Turbo for RubyMotion apps
29
+ email:
30
+ - email@andrewhavens.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - lib/motion-turbo-ios.rb
37
+ - lib/turbo/logging.rb
38
+ - lib/turbo/path_configuration/path_configuration.rb
39
+ - lib/turbo/path_configuration/path_configuration_decoder.rb
40
+ - lib/turbo/path_configuration/path_configuration_loader.rb
41
+ - lib/turbo/path_configuration/path_rule.rb
42
+ - lib/turbo/session/navigation_delegate_methods.rb
43
+ - lib/turbo/session/session.rb
44
+ - lib/turbo/session/session_delegate_methods.rb
45
+ - lib/turbo/session/visit_delegate_methods.rb
46
+ - lib/turbo/session/visitable_delegate_methods.rb
47
+ - lib/turbo/session/web_view_delegate_methods.rb
48
+ - lib/turbo/turbo_error.rb
49
+ - lib/turbo/visit/cold_boot_visit.rb
50
+ - lib/turbo/visit/javascript_visit.rb
51
+ - lib/turbo/visit/visit.rb
52
+ - lib/turbo/visit/visit_options.rb
53
+ - lib/turbo/visit/visit_proposal.rb
54
+ - lib/turbo/visit/visit_response.rb
55
+ - lib/turbo/visitable/visitable.rb
56
+ - lib/turbo/visitable/visitable_view.rb
57
+ - lib/turbo/visitable/visitable_view_controller.rb
58
+ - lib/turbo/visitable_view/activity_indicator.rb
59
+ - lib/turbo/visitable_view/constraints.rb
60
+ - lib/turbo/visitable_view/refresh_control.rb
61
+ - lib/turbo/visitable_view/screenshots.rb
62
+ - lib/turbo/visitable_view/scroll_view.rb
63
+ - lib/turbo/visitable_view/web_view.rb
64
+ - lib/turbo/web_view/script_message.rb
65
+ - lib/turbo/web_view/script_message_handler.rb
66
+ - lib/turbo/web_view/web_view_bridge.rb
67
+ homepage: https://github.com/p8/motion-turbo-ios
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.1.6
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Turbo for RubyMotion apps
90
+ test_files: []