diff --git a/Caravel.podspec b/Caravel.podspec index c30c8b8..8d43eb0 100644 --- a/Caravel.podspec +++ b/Caravel.podspec @@ -15,8 +15,10 @@ Pod::Spec.new do |s| # summary should be tweet-length, and the description more in depth. # + version = "0.3.3" + s.name = "Caravel" - s.version = "0.3.2" + s.version = version s.summary = "A Swift event bus for UIWebView and JS" s.description = <<-DESC @@ -73,7 +75,7 @@ Pod::Spec.new do |s| # Supports git, hg, bzr, svn and HTTP. # - s.source = { :git => "https://github.com/coshx/caravel.git", :tag => "v0.3.2" } + s.source = { :git => "https://github.com/coshx/caravel.git", :tag => "v#{version}" } # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # diff --git a/README.md b/README.md index 8c97c6d..346b6bc 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ - `Int` - `Double` - `String` - - `Array` - - `Object` + - `Array` (available as a `NSArray`) + - `Object` (available as a `NSDictionary`) ## Installation diff --git a/caravel-test/Base.lproj/Main.storyboard b/caravel-test/Base.lproj/Main.storyboard index 66f4392..56d9679 100644 --- a/caravel-test/Base.lproj/Main.storyboard +++ b/caravel-test/Base.lproj/Main.storyboard @@ -52,7 +52,7 @@ - + @@ -195,7 +195,7 @@ - + @@ -244,7 +244,7 @@ - + @@ -293,7 +293,7 @@ - + @@ -342,7 +342,7 @@ - + @@ -391,7 +391,7 @@ - + @@ -440,7 +440,7 @@ - + diff --git a/caravel-test/EventDataController.swift b/caravel-test/EventDataController.swift index 3e5200f..9f9e9d1 100644 --- a/caravel-test/EventDataController.swift +++ b/caravel-test/EventDataController.swift @@ -107,6 +107,57 @@ public class EventDataController: UIViewController { self._raise("Dictionary - bad type") } } + + bus.register("ComplexArray") { name, data in + if let a = data as? NSArray { + if a.count != 3 { + self._raise("ComplexArray - bad length") + } + if a[0] as! Int != 87 { + self._raise("ComplexArray - bad first element") + } + if let d = a[1] as? NSDictionary { + if d.valueForKey("name") as! String != "Bruce Willis" { + self._raise("ComplexArray - bad second element") + } + } else { + self._raise("ComplexArray - bad typed second element") + } + if a[2] as! String != "left-handed" { + self._raise("ComplexArray - bad third element") + } + } else { + self._raise("ComplexArray - bad type") + } + } + + bus.register("ComplexDictionary") { name, data in + if let d = data as? NSDictionary { + if d.valueForKey("name") as! String != "John Malkovich" { + self._raise("ComplexDictionary - bad first pair") + } + + if let a = d.valueForKey("movies") as? NSArray { + if a.count != 2 { + self._raise("ComplexDictionary - bad length") + } + if a[0] as! String != "Dangerous Liaisons" { + self._raise("ComplexDictionary - bad first element in array") + } + if a[1] as! String != "Burn after reading" { + self._raise("ComplexDictionary - bad second element in array") + } + } else { + self._raise("ComplexDictionary - bad typed second element") + } + + if d.valueForKey("kids") as! Int != 2 { + self._raise("ComplexDictionary - bad third pair") + } + } else { + self._raise("ComplexDictionary - bad type") + } + } } _webView.loadRequest(NSURLRequest(URL: NSBundle.mainBundle().URLForResource("event_data", withExtension: "html")!)) diff --git a/caravel-test/html/basic_triggering.html b/caravel-test/html/basic_triggering.html index c9f714d..0e629a0 100644 --- a/caravel-test/html/basic_triggering.html +++ b/caravel-test/html/basic_triggering.html @@ -1,5 +1,7 @@

Basic triggering

+

You should see:

Received From iOS!

+ \ No newline at end of file diff --git a/caravel-test/html/event_name.html b/caravel-test/html/event_name.html index a536d29..27b3ce6 100644 --- a/caravel-test/html/event_name.html +++ b/caravel-test/html/event_name.html @@ -1,5 +1,7 @@

Event name

+

You should only see

You should see me

+ \ No newline at end of file diff --git a/caravel-test/html/initialization.html b/caravel-test/html/initialization.html index 66c2d52..fb1d0a0 100644 --- a/caravel-test/html/initialization.html +++ b/caravel-test/html/initialization.html @@ -1,5 +1,7 @@

Initialization

+

You should see

Before
After

+ \ No newline at end of file diff --git a/caravel-test/html/multiple_subscribers.html b/caravel-test/html/multiple_subscribers.html index a007dfa..590a2ca 100644 --- a/caravel-test/html/multiple_subscribers.html +++ b/caravel-test/html/multiple_subscribers.html @@ -1,5 +1,7 @@

Multiple subscribers

+

You should see:

First!
Second!

+ \ No newline at end of file diff --git a/caravel-test/html/two_buses.html b/caravel-test/html/two_buses.html index 1f27e13..1c21eb3 100644 --- a/caravel-test/html/two_buses.html +++ b/caravel-test/html/two_buses.html @@ -1,5 +1,7 @@

Two buses

+

You should see

You should see me first and only once
You should see me after and only once

+ \ No newline at end of file diff --git a/caravel-test/html/two_events.html b/caravel-test/html/two_events.html index 0cf7b0b..177e977 100644 --- a/caravel-test/html/two_events.html +++ b/caravel-test/html/two_events.html @@ -1,5 +1,7 @@

Two events

+

You should only see

You should see me and only me

+ \ No newline at end of file diff --git a/caravel-test/js/basic_triggering.js b/caravel-test/js/basic_triggering.js index 104b2cf..d826da6 100644 --- a/caravel-test/js/basic_triggering.js +++ b/caravel-test/js/basic_triggering.js @@ -1,5 +1,5 @@ Caravel.getDefault().register("From iOS", function(name, data) { - $('body').append('

Received From iOS!'); + $('body').append('

Received From iOS!

'); }); Caravel.getDefault().post("From JS"); diff --git a/caravel-test/js/event_data.js b/caravel-test/js/event_data.js index dc87f2b..728612e 100644 --- a/caravel-test/js/event_data.js +++ b/caravel-test/js/event_data.js @@ -3,6 +3,9 @@ function ok(name) { } function fail(name, data) { + if ((data instanceof Array) || (data instanceof Object)) { + data = JSON.stringify(data); + } $('body').append('

Failed for ' + name + ': received ' + data + '

'); } @@ -70,9 +73,54 @@ Caravel.getDefault().register("Dictionary", function(name, data) { } }); +Caravel.getDefault().register("ComplexArray", function(name, data) { + var expectedData = [{name: "Alice", age: 24}, {name: "Bob", age: 23}]; + var customFail = function() { + fail(name, data); + }; + + if (data.length == 2) { + if (data[0].name == expectedData[0].name && data[0].age == expectedData[0].age) { + if (data[1].name == expectedData[1].name && data[1].age == expectedData[1].age) { + ok(name); + } else { + customFail(); + } + } else { + customFail(); + } + } else { + customFail(); + } +}); + +Caravel.getDefault().register("ComplexDictionary", function(name, data) { + var expectedData = {name: "Cesar", address: { street: "Parrot", city: "Perigueux" }, games: ["Fifa", "Star Wars"]}; + var customFail = function() { + fail(name, data); + }; + + if (data.name == expectedData.name) { + if (data.address.street == expectedData.address.street && data.address.city == expectedData.address.city) { + if (data.length == expectedData.length && data.games[0] == expectedData.games[0] && data.games[1] == expectedData.games[1]) { + ok(name); + } else { + customFail(); + } + } else { + customFail(); + } + } else { + customFail(); + } +}); + Caravel.getDefault().post("Int", 987); Caravel.getDefault().post("Float", 19.89); Caravel.getDefault().post("Double", 15.15); Caravel.getDefault().post("String", "Napoleon"); Caravel.getDefault().post("Array", [3, 1, 4]); -Caravel.getDefault().post("Dictionary", { "movie": "Once upon a time in the West", "actor": "Charles Bronson" }); \ No newline at end of file +Caravel.getDefault().post("Dictionary", { "movie": "Once upon a time in the West", "actor": "Charles Bronson" }); + +Caravel.getDefault().post("ComplexArray", [87, {"name": "Bruce Willis"}, "left-handed" ]); +Caravel.getDefault().post("ComplexDictionary", {name: "John Malkovich", movies: ["Dangerous Liaisons", "Burn after reading"], kids: 2}); \ No newline at end of file diff --git a/caravel/ArgumentParser.swift b/caravel/ArgumentParser.swift index 8c89dad..2c80e7a 100644 --- a/caravel/ArgumentParser.swift +++ b/caravel/ArgumentParser.swift @@ -10,53 +10,25 @@ import Foundation /** * @class ArgumentParser - * @brief Parses JS input to a list of arguments + * @brief Parses JS input to a list of arguments. Expected pattern: busName=*&eventName=*&eventData=*. Data are optional */ internal class ArgumentParser { - internal class func parse(input: String) -> [String] { - var outcome = [String]() - var prev: Character? - var buffer = String() + internal class func parse(input: String) -> (busName: String, eventName: String, eventData: String?) { + var queryPairs = input.componentsSeparatedByString("&") + var outcome: (busName: String, eventName: String, eventData: String?) = (busName: "", eventName: "", eventData: nil) - if count(input) == 0 { // No arg - return outcome - } - - for current in input { - if current == "@" { - if prev != nil && prev != "\\" { - // Arguments are split using "@" symbol - // Existing "@" have been escaped before - outcome.append(buffer) - buffer = "" - } else if prev != nil && prev == "\\" { - // Escaped "@" symbol - // Let's unescape it - var i = 0, size = count(buffer) - var s = "" - - for c in buffer { - if i < size - 1 { - s.append(c) - } - i++ - } - - buffer = s - buffer.append(current) - } - // A "@" symbol cannot be first as it has been escaped, so no else condition + for p in queryPairs { + var keyValue = p.componentsSeparatedByString("=") + if keyValue[0] == "busName" { + outcome.busName = keyValue[1].stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)! + } else if keyValue[0] == "eventName" { + outcome.eventName = keyValue[1].stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)! } else { - buffer.append(current) + outcome.eventData = keyValue[1].stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding) } - - prev = current } - // Add latest buffer - outcome.append(buffer) - return outcome } } \ No newline at end of file diff --git a/caravel/Caravel.swift b/caravel/Caravel.swift index f10a3d1..8a0bf66 100644 --- a/caravel/Caravel.swift +++ b/caravel/Caravel.swift @@ -83,24 +83,20 @@ public class Caravel: NSObject, UIWebViewDelegate { /** * Caravel expects the following pattern: - * caravel@bus_name@event_name@extra_arg + * caravel://host.com?busName=*&eventName=*&eventData=* * * Followed argument types are supported: * int, float, double, string */ public func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { - if let lastPathComponent: String = request.URL?.lastPathComponent { - - // The last path of the URL needs to contains at least the "caravel" word - if count(lastPathComponent) > count("caravel") && (lastPathComponent as NSString).substringToIndex(count("caravel")) == "caravel" { - var args = ArgumentParser.parse(lastPathComponent) - var busName = args[1] - var eventName = args[2] + if let scheme: String = request.URL?.scheme { + if scheme == "caravel" { + var args = ArgumentParser.parse(request.URL!.query!) // All buses are notified about that incoming event. Then, they need to investigate first if they // are potential receivers - if _name == busName { - if eventName == "CaravelInit" { // Reserved event name. Triggers whenReady + if _name == args.busName { + if args.eventName == "CaravelInit" { // Reserved event name. Triggers whenReady objc_sync_enter(_initializationLock) _isInitialized = true @@ -114,14 +110,14 @@ public class Caravel: NSObject, UIWebViewDelegate { } else { var eventData: AnyObject? = nil - if args.count > 3 { // Arg is optional - eventData = DataSerializer.deserialize(args[3]) + if let d = args.eventData { // Data are optional + eventData = DataSerializer.deserialize(d) } for s in _subscribers { - if s.name == eventName { + if s.name == args.eventName { dispatch_async(dispatch_get_main_queue()) { - s.callback(eventName, eventData) + s.callback(args.eventName, eventData) } } } diff --git a/caravel/DataSerializer.swift b/caravel/DataSerializer.swift index ad0eb41..9183301 100644 --- a/caravel/DataSerializer.swift +++ b/caravel/DataSerializer.swift @@ -10,7 +10,7 @@ import Foundation /** * @class DataSerializer - * @brief Serializes iOS data for JS + * @brief Serializes data to JS format and parses data coming from JS */ internal class DataSerializer { @@ -32,12 +32,13 @@ internal class DataSerializer { output = "\(f)" case .String: var s = input as! String + // As string is going to be unwrapped from quotes, when passed to JS, all quotes need to be escaped s = s.stringByReplacingOccurrencesOfString("\"", withString: "\\\"", options: NSStringCompareOptions(), range: nil) s = s.stringByReplacingOccurrencesOfString("'", withString: "\'", options: NSStringCompareOptions(), range: nil) output = "\"\(s)\"" case .Array, .Dictionary: // Array and Dictionary are serialized to JSON. - // They should wrap only basic data (same types than supported ones) + // They should wrap only "basic" data (same types than supported ones) var json = NSJSONSerialization.dataWithJSONObject(input, options: NSJSONWritingOptions(), error: NSErrorPointer())! var s = NSString(data: json, encoding: NSUTF8StringEncoding)! output = s as String @@ -48,7 +49,7 @@ internal class DataSerializer { internal static func deserialize(input: String) -> AnyObject { if count(input) > 0 { - if input[0] == "[" || input[0] == "{" { + if input[0] == "[" || input[0] == "{" { // Array or Dictionary, matching JSON format var json = input.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! return NSJSONSerialization.JSONObjectWithData(json, options: NSJSONReadingOptions(), error: NSErrorPointer())! } diff --git a/caravel/UIWebViewDelegateMediator.swift b/caravel/UIWebViewDelegateMediator.swift index b3a78cc..42f572a 100644 --- a/caravel/UIWebViewDelegateMediator.swift +++ b/caravel/UIWebViewDelegateMediator.swift @@ -22,10 +22,10 @@ internal class UIWebViewDelegateMediator: NSObject, UIWebViewDelegate { /** * All the subscribers. They are sorted by webview's hash */ - private lazy var _webViews: [Int: [UIWebViewDelegate]] = [Int: [UIWebViewDelegate]]() + private lazy var _webViewSubscribers: [Int: [UIWebViewDelegate]] = [Int: [UIWebViewDelegate]]() private func iterateOverDelegates(webView: UIWebView, callback: (UIWebViewDelegate) -> Void) { - var array = UIWebViewDelegateMediator._singleton._webViews[webView.hash]! + var array = UIWebViewDelegateMediator._singleton._webViewSubscribers[webView.hash]! for e in array { callback(e) @@ -38,16 +38,16 @@ internal class UIWebViewDelegateMediator: NSObject, UIWebViewDelegate { var delegates = [UIWebViewDelegate]() delegates.append(webView.delegate!) - _singleton._webViews[webView.hash] = delegates + _singleton._webViewSubscribers[webView.hash] = delegates webView.delegate = _singleton } else if webView.delegate == nil { // No delegate, just initialize - _singleton._webViews[webView.hash] = [UIWebViewDelegate]() + _singleton._webViewSubscribers[webView.hash] = [UIWebViewDelegate]() webView.delegate = _singleton } - _singleton._webViews[webView.hash]!.append(subscriber) + _singleton._webViewSubscribers[webView.hash]!.append(subscriber) } // About methods below: diff --git a/caravel/js/Gruntfile.js b/caravel/js/Gruntfile.js index 4615136..8d8000a 100644 --- a/caravel/js/Gruntfile.js +++ b/caravel/js/Gruntfile.js @@ -1,7 +1,7 @@ module.exports = function (grunt) { 'use strict'; - var version = '0.3.2'; + var version = '0.3.3'; // Project configuration grunt.initConfig({ diff --git a/caravel/js/caravel.coffee b/caravel/js/caravel.coffee index 102a87c..88de9d9 100644 --- a/caravel/js/caravel.coffee +++ b/caravel/js/caravel.coffee @@ -12,12 +12,12 @@ class Caravel _post: (eventName, data) -> # TODO: Improve that code using an AJAX request iframe = document.createElement 'iframe' - src = "caravel@#{@name}@#{eventName}" + src = "caravel://host.com?busName=#{encodeURIComponent(@name)}&eventName=#{encodeURIComponent(eventName)}" if data? if data instanceof Array or data instanceof Object - src += "@#{JSON.stringify(data)}" + src += "&eventData=#{encodeURIComponent(JSON.stringify(data))}" else - src += "@#{data}" + src += "&eventData=#{encodeURIComponent(data)}" iframe.setAttribute 'src', src document.documentElement.appendChild iframe iframe.parentNode.removeChild iframe diff --git a/caravel/js/caravel.min.js b/caravel/js/caravel.min.js index 9d0b76a..e9f13a6 100644 --- a/caravel/js/caravel.min.js +++ b/caravel/js/caravel.min.js @@ -1,2 +1,2 @@ -/** Caravel 0.3.1 - https://github.com/coshx/caravel */ -var Caravel;Caravel=function(){function Caravel(name){this.name=name,this.subscribers=[]}return Caravel["default"]=null,Caravel.buses=[],Caravel.prototype._post=function(eventName,data){var iframe,src;return iframe=document.createElement("iframe"),src="caravel@"+this.name+"@"+eventName,null!=data&&(src+=data instanceof Array||data instanceof Object?"@"+JSON.stringify(data):"@"+data),iframe.setAttribute("src",src),document.documentElement.appendChild(iframe),iframe.parentNode.removeChild(iframe)},Caravel.prototype.getName=function(){return this.name},Caravel.prototype.post=function(name,data){return this._post(name,data)},Caravel.prototype.register=function(name,callback){return this.subscribers.push({name:name,callback:callback})},Caravel.prototype.raise=function(name,data){var e,i,len,parsedData,ref,results;for(parsedData=data instanceof Array||data instanceof Object||"string"==typeof data||data instanceof String?data:JSON.parse(data),ref=this.subscribers,results=[],i=0,len=ref.length;len>i;i++)e=ref[i],e.name===name?results.push(e.callback(name,parsedData)):results.push(void 0);return results},Caravel.getDefault=function(){return null==Caravel["default"]&&(Caravel["default"]=new Caravel("default"),Caravel["default"].post("CaravelInit")),Caravel["default"]},Caravel.get=function(name){var b,i,len,ref;for(ref=Caravel.buses,i=0,len=ref.length;len>i;i++)if(b=ref[i],b.getName()===name)return b;return b=new Caravel(name),Caravel.buses.push(b),b.post("CaravelInit"),b},Caravel}(); \ No newline at end of file +/** Caravel 0.3.3 - https://github.com/coshx/caravel */ +var Caravel;Caravel=function(){function Caravel(name){this.name=name,this.subscribers=[]}return Caravel["default"]=null,Caravel.buses=[],Caravel.prototype._post=function(eventName,data){var iframe,src;return iframe=document.createElement("iframe"),src="caravel://host.com?busName="+encodeURIComponent(this.name)+"&eventName="+encodeURIComponent(eventName),null!=data&&(src+=data instanceof Array||data instanceof Object?"&eventData="+encodeURIComponent(JSON.stringify(data)):"&eventData="+encodeURIComponent(data)),iframe.setAttribute("src",src),document.documentElement.appendChild(iframe),iframe.parentNode.removeChild(iframe)},Caravel.prototype.getName=function(){return this.name},Caravel.prototype.post=function(name,data){return this._post(name,data)},Caravel.prototype.register=function(name,callback){return this.subscribers.push({name:name,callback:callback})},Caravel.prototype.raise=function(name,data){var e,i,len,parsedData,ref,results;for(parsedData=data instanceof Array||data instanceof Object||"string"==typeof data||data instanceof String?data:JSON.parse(data),ref=this.subscribers,results=[],i=0,len=ref.length;len>i;i++)e=ref[i],e.name===name?results.push(e.callback(name,parsedData)):results.push(void 0);return results},Caravel.getDefault=function(){return null==Caravel["default"]&&(Caravel["default"]=new Caravel("default"),Caravel["default"].post("CaravelInit")),Caravel["default"]},Caravel.get=function(name){var b,i,len,ref;for(ref=Caravel.buses,i=0,len=ref.length;len>i;i++)if(b=ref[i],b.getName()===name)return b;return b=new Caravel(name),Caravel.buses.push(b),b.post("CaravelInit"),b},Caravel}(); \ No newline at end of file