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.
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: []