├── .gitignore ├── Example ├── TLMetaResolverExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── TLMetaResolverExample.xcscheme └── TLMetaResolverExample │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── FirstViewController.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ └── SecondViewController.swift ├── LICENSE ├── Pod ├── Assets │ ├── .gitkeep │ ├── TLMetaParser.js │ ├── iconMask.png │ └── iconMask@2x.png └── Classes │ ├── .gitkeep │ ├── TLBundleExtension.swift │ ├── TLMetaResolver.swift │ └── TLNativeAppActivity.swift ├── README.md └── TLMetaResolver.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Note: if you ignore the Pods directory, make sure to uncomment 30 | # `pod install` in .travis.yml 31 | # 32 | # Pods/ 33 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9BAA7F101B4AF5780001D4B8 /* TLBundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAA7F0C1B4AF5780001D4B8 /* TLBundleExtension.swift */; }; 11 | 9BAA7F111B4AF5780001D4B8 /* TLMetaResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAA7F0D1B4AF5780001D4B8 /* TLMetaResolver.swift */; }; 12 | 9BAA7F121B4AF5780001D4B8 /* TLNativeAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAA7F0E1B4AF5780001D4B8 /* TLNativeAppActivity.swift */; }; 13 | 9BAA7F191B4AF58A0001D4B8 /* iconMask.png in Resources */ = {isa = PBXBuildFile; fileRef = 9BAA7F151B4AF58A0001D4B8 /* iconMask.png */; }; 14 | 9BAA7F1A1B4AF58A0001D4B8 /* iconMask@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9BAA7F161B4AF58A0001D4B8 /* iconMask@2x.png */; }; 15 | 9BAA7F1B1B4AF58A0001D4B8 /* TLMetaParser.js in Resources */ = {isa = PBXBuildFile; fileRef = 9BAA7F171B4AF58A0001D4B8 /* TLMetaParser.js */; }; 16 | 9BAB57491ABB52F20052A188 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB57481ABB52F20052A188 /* AppDelegate.swift */; }; 17 | 9BAB574E1ABB52F20052A188 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BAB574C1ABB52F20052A188 /* Main.storyboard */; }; 18 | 9BAB57501ABB52F20052A188 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9BAB574F1ABB52F20052A188 /* Images.xcassets */; }; 19 | 9BAB57531ABB52F20052A188 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9BAB57511ABB52F20052A188 /* LaunchScreen.xib */; }; 20 | 9BAB576A1ABB54050052A188 /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB57681ABB54050052A188 /* FirstViewController.swift */; }; 21 | 9BAB576B1ABB54050052A188 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB57691ABB54050052A188 /* SecondViewController.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 9BAA7F0C1B4AF5780001D4B8 /* TLBundleExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLBundleExtension.swift; sourceTree = ""; }; 26 | 9BAA7F0D1B4AF5780001D4B8 /* TLMetaResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLMetaResolver.swift; sourceTree = ""; }; 27 | 9BAA7F0E1B4AF5780001D4B8 /* TLNativeAppActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLNativeAppActivity.swift; sourceTree = ""; }; 28 | 9BAA7F151B4AF58A0001D4B8 /* iconMask.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconMask.png; sourceTree = ""; }; 29 | 9BAA7F161B4AF58A0001D4B8 /* iconMask@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconMask@2x.png"; sourceTree = ""; }; 30 | 9BAA7F171B4AF58A0001D4B8 /* TLMetaParser.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = TLMetaParser.js; sourceTree = ""; }; 31 | 9BAB57431ABB52F20052A188 /* TLMetaResolverExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TLMetaResolverExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 9BAB57471ABB52F20052A188 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 9BAB57481ABB52F20052A188 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 9BAB574D1ABB52F20052A188 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 35 | 9BAB574F1ABB52F20052A188 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 36 | 9BAB57521ABB52F20052A188 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 37 | 9BAB57681ABB54050052A188 /* FirstViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 38 | 9BAB57691ABB54050052A188 /* SecondViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 9BAB57401ABB52F20052A188 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 9BAA7F0A1B4AF5780001D4B8 /* Classes */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 9BAA7F0C1B4AF5780001D4B8 /* TLBundleExtension.swift */, 56 | 9BAA7F0D1B4AF5780001D4B8 /* TLMetaResolver.swift */, 57 | 9BAA7F0E1B4AF5780001D4B8 /* TLNativeAppActivity.swift */, 58 | ); 59 | name = Classes; 60 | path = ../../Pod/Classes; 61 | sourceTree = ""; 62 | }; 63 | 9BAA7F131B4AF58A0001D4B8 /* Assets */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 9BAA7F151B4AF58A0001D4B8 /* iconMask.png */, 67 | 9BAA7F161B4AF58A0001D4B8 /* iconMask@2x.png */, 68 | 9BAA7F171B4AF58A0001D4B8 /* TLMetaParser.js */, 69 | ); 70 | name = Assets; 71 | path = ../../Pod/Assets; 72 | sourceTree = ""; 73 | }; 74 | 9BAB573A1ABB52F20052A188 = { 75 | isa = PBXGroup; 76 | children = ( 77 | 9BAB57451ABB52F20052A188 /* TLMetaResolverExample */, 78 | 9BAB57441ABB52F20052A188 /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 9BAB57441ABB52F20052A188 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 9BAB57431ABB52F20052A188 /* TLMetaResolverExample.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 9BAB57451ABB52F20052A188 /* TLMetaResolverExample */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 9BAA7F0A1B4AF5780001D4B8 /* Classes */, 94 | 9BAB57681ABB54050052A188 /* FirstViewController.swift */, 95 | 9BAB57691ABB54050052A188 /* SecondViewController.swift */, 96 | 9BAB57481ABB52F20052A188 /* AppDelegate.swift */, 97 | 9BAB574C1ABB52F20052A188 /* Main.storyboard */, 98 | 9BAB574F1ABB52F20052A188 /* Images.xcassets */, 99 | 9BAB57511ABB52F20052A188 /* LaunchScreen.xib */, 100 | 9BAB57461ABB52F20052A188 /* Supporting Files */, 101 | ); 102 | path = TLMetaResolverExample; 103 | sourceTree = ""; 104 | }; 105 | 9BAB57461ABB52F20052A188 /* Supporting Files */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 9BAA7F131B4AF58A0001D4B8 /* Assets */, 109 | 9BAB57471ABB52F20052A188 /* Info.plist */, 110 | ); 111 | name = "Supporting Files"; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | 9BAB57421ABB52F20052A188 /* TLMetaResolverExample */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = 9BAB57621ABB52F20052A188 /* Build configuration list for PBXNativeTarget "TLMetaResolverExample" */; 120 | buildPhases = ( 121 | 9BAB573F1ABB52F20052A188 /* Sources */, 122 | 9BAB57401ABB52F20052A188 /* Frameworks */, 123 | 9BAB57411ABB52F20052A188 /* Resources */, 124 | ); 125 | buildRules = ( 126 | ); 127 | dependencies = ( 128 | ); 129 | name = TLMetaResolverExample; 130 | productName = TLMetaResolverExample; 131 | productReference = 9BAB57431ABB52F20052A188 /* TLMetaResolverExample.app */; 132 | productType = "com.apple.product-type.application"; 133 | }; 134 | /* End PBXNativeTarget section */ 135 | 136 | /* Begin PBXProject section */ 137 | 9BAB573B1ABB52F20052A188 /* Project object */ = { 138 | isa = PBXProject; 139 | attributes = { 140 | LastUpgradeCheck = 0620; 141 | ORGANIZATIONNAME = "Bruno Berisso"; 142 | TargetAttributes = { 143 | 9BAB57421ABB52F20052A188 = { 144 | CreatedOnToolsVersion = 6.2; 145 | }; 146 | }; 147 | }; 148 | buildConfigurationList = 9BAB573E1ABB52F20052A188 /* Build configuration list for PBXProject "TLMetaResolverExample" */; 149 | compatibilityVersion = "Xcode 3.2"; 150 | developmentRegion = English; 151 | hasScannedForEncodings = 0; 152 | knownRegions = ( 153 | en, 154 | Base, 155 | ); 156 | mainGroup = 9BAB573A1ABB52F20052A188; 157 | productRefGroup = 9BAB57441ABB52F20052A188 /* Products */; 158 | projectDirPath = ""; 159 | projectRoot = ""; 160 | targets = ( 161 | 9BAB57421ABB52F20052A188 /* TLMetaResolverExample */, 162 | ); 163 | }; 164 | /* End PBXProject section */ 165 | 166 | /* Begin PBXResourcesBuildPhase section */ 167 | 9BAB57411ABB52F20052A188 /* Resources */ = { 168 | isa = PBXResourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 9BAA7F191B4AF58A0001D4B8 /* iconMask.png in Resources */, 172 | 9BAB574E1ABB52F20052A188 /* Main.storyboard in Resources */, 173 | 9BAB57531ABB52F20052A188 /* LaunchScreen.xib in Resources */, 174 | 9BAB57501ABB52F20052A188 /* Images.xcassets in Resources */, 175 | 9BAA7F1B1B4AF58A0001D4B8 /* TLMetaParser.js in Resources */, 176 | 9BAA7F1A1B4AF58A0001D4B8 /* iconMask@2x.png in Resources */, 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXResourcesBuildPhase section */ 181 | 182 | /* Begin PBXSourcesBuildPhase section */ 183 | 9BAB573F1ABB52F20052A188 /* Sources */ = { 184 | isa = PBXSourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 9BAB576B1ABB54050052A188 /* SecondViewController.swift in Sources */, 188 | 9BAA7F121B4AF5780001D4B8 /* TLNativeAppActivity.swift in Sources */, 189 | 9BAA7F111B4AF5780001D4B8 /* TLMetaResolver.swift in Sources */, 190 | 9BAA7F101B4AF5780001D4B8 /* TLBundleExtension.swift in Sources */, 191 | 9BAB57491ABB52F20052A188 /* AppDelegate.swift in Sources */, 192 | 9BAB576A1ABB54050052A188 /* FirstViewController.swift in Sources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXSourcesBuildPhase section */ 197 | 198 | /* Begin PBXVariantGroup section */ 199 | 9BAB574C1ABB52F20052A188 /* Main.storyboard */ = { 200 | isa = PBXVariantGroup; 201 | children = ( 202 | 9BAB574D1ABB52F20052A188 /* Base */, 203 | ); 204 | name = Main.storyboard; 205 | sourceTree = ""; 206 | }; 207 | 9BAB57511ABB52F20052A188 /* LaunchScreen.xib */ = { 208 | isa = PBXVariantGroup; 209 | children = ( 210 | 9BAB57521ABB52F20052A188 /* Base */, 211 | ); 212 | name = LaunchScreen.xib; 213 | sourceTree = ""; 214 | }; 215 | /* End PBXVariantGroup section */ 216 | 217 | /* Begin XCBuildConfiguration section */ 218 | 9BAB57601ABB52F20052A188 /* Debug */ = { 219 | isa = XCBuildConfiguration; 220 | buildSettings = { 221 | ALWAYS_SEARCH_USER_PATHS = NO; 222 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 223 | CLANG_CXX_LIBRARY = "libc++"; 224 | CLANG_ENABLE_MODULES = YES; 225 | CLANG_ENABLE_OBJC_ARC = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_CONSTANT_CONVERSION = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INT_CONVERSION = YES; 232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 233 | CLANG_WARN_UNREACHABLE_CODE = YES; 234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 235 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 236 | COPY_PHASE_STRIP = NO; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu99; 239 | GCC_DYNAMIC_NO_PIC = NO; 240 | GCC_OPTIMIZATION_LEVEL = 0; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "DEBUG=1", 243 | "$(inherited)", 244 | ); 245 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 248 | GCC_WARN_UNDECLARED_SELECTOR = YES; 249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 250 | GCC_WARN_UNUSED_FUNCTION = YES; 251 | GCC_WARN_UNUSED_VARIABLE = YES; 252 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 253 | MTL_ENABLE_DEBUG_INFO = YES; 254 | ONLY_ACTIVE_ARCH = YES; 255 | SDKROOT = iphoneos; 256 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 257 | TARGETED_DEVICE_FAMILY = "1,2"; 258 | }; 259 | name = Debug; 260 | }; 261 | 9BAB57611ABB52F20052A188 /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_EMPTY_BODY = YES; 273 | CLANG_WARN_ENUM_CONVERSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 | CLANG_WARN_UNREACHABLE_CODE = YES; 277 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 278 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 279 | COPY_PHASE_STRIP = NO; 280 | ENABLE_NS_ASSERTIONS = NO; 281 | ENABLE_STRICT_OBJC_MSGSEND = YES; 282 | GCC_C_LANGUAGE_STANDARD = gnu99; 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 290 | MTL_ENABLE_DEBUG_INFO = NO; 291 | SDKROOT = iphoneos; 292 | TARGETED_DEVICE_FAMILY = "1,2"; 293 | VALIDATE_PRODUCT = YES; 294 | }; 295 | name = Release; 296 | }; 297 | 9BAB57631ABB52F20052A188 /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | INFOPLIST_FILE = TLMetaResolverExample/Info.plist; 302 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 303 | PRODUCT_NAME = "$(TARGET_NAME)"; 304 | }; 305 | name = Debug; 306 | }; 307 | 9BAB57641ABB52F20052A188 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | INFOPLIST_FILE = TLMetaResolverExample/Info.plist; 312 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 313 | PRODUCT_NAME = "$(TARGET_NAME)"; 314 | }; 315 | name = Release; 316 | }; 317 | /* End XCBuildConfiguration section */ 318 | 319 | /* Begin XCConfigurationList section */ 320 | 9BAB573E1ABB52F20052A188 /* Build configuration list for PBXProject "TLMetaResolverExample" */ = { 321 | isa = XCConfigurationList; 322 | buildConfigurations = ( 323 | 9BAB57601ABB52F20052A188 /* Debug */, 324 | 9BAB57611ABB52F20052A188 /* Release */, 325 | ); 326 | defaultConfigurationIsVisible = 0; 327 | defaultConfigurationName = Release; 328 | }; 329 | 9BAB57621ABB52F20052A188 /* Build configuration list for PBXNativeTarget "TLMetaResolverExample" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 9BAB57631ABB52F20052A188 /* Debug */, 333 | 9BAB57641ABB52F20052A188 /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | /* End XCConfigurationList section */ 339 | }; 340 | rootObject = 9BAB573B1ABB52F20052A188 /* Project object */; 341 | } 342 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample.xcodeproj/xcshareddata/xcschemes/TLMetaResolverExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TLMetaResolverExample 4 | // 5 | // Created by Bruno Berisso on 3/19/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 114 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/FirstViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.swift 3 | // TLMetaResolver 4 | // 5 | // Created by Bruno Berisso on 2/18/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FirstViewController: UITableViewController { 12 | 13 | private var urlList: [NSURL]! 14 | private var selectedRow: Int! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | navigationController?.navigationBarHidden = true 20 | 21 | urlList = [ 22 | "http://swaggy-life.tumblr.com/", 23 | "http://www.nachorater.com/", 24 | "http://mrk.tv/", 25 | "https://medium.com", 26 | "https://www.kickstarter.com" 27 | ].map { NSURL(string: $0)! } 28 | 29 | let alert = UIAlertController(title: "How it works", 30 | message: "Select one row to load the given page on a web view in the next screen. Each row has a particular set of meta tags for you to test", preferredStyle: .Alert) 31 | 32 | alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (a: UIAlertAction!) -> Void in 33 | alert.dismissViewControllerAnimated(true, completion: .None) 34 | })) 35 | 36 | dispatch_after(1, dispatch_get_main_queue()) { () -> Void in 37 | self.presentViewController(alert, animated: true, completion: .None) 38 | } 39 | } 40 | } 41 | 42 | 43 | extension FirstViewController { 44 | 45 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 46 | selectedRow = indexPath.row 47 | performSegueWithIdentifier("ShowPageUrl", sender: self) 48 | } 49 | 50 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 51 | let secondController = segue.destinationViewController as! SecondViewController 52 | secondController.pageUrl = urlList[selectedRow] 53 | } 54 | } -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.tryolabs.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/TLMetaResolverExample/SecondViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TLMetaResolver 4 | // 5 | // Created by Bruno Berisso on 2/12/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | 13 | class NativeAppActivity: TLNativeAppActivity { 14 | 15 | var onPerform: (()->()) -> () 16 | 17 | init(nativeAppInfo: TLNativeAppInfo, onPerform: (didFinish: ()->()) -> ()) { 18 | self.onPerform = onPerform 19 | super.init(nativeAppInfo: nativeAppInfo) 20 | } 21 | 22 | override func performActivity() { 23 | onPerform { 24 | self.activityDidFinish(true) 25 | } 26 | } 27 | } 28 | 29 | 30 | class SecondViewController: UIViewController, UIWebViewDelegate { 31 | 32 | @IBOutlet weak var webView: UIWebView! 33 | 34 | var pageUrl: NSURL! 35 | 36 | private var timer: NSTimer! 37 | private var resolvingMetaTags: Bool! 38 | private var appInfo: TLNativeAppInfo! 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | navigationController?.navigationBarHidden = false 44 | 45 | let spinner = UIActivityIndicatorView() 46 | spinner.startAnimating() 47 | spinner.activityIndicatorViewStyle = .Gray 48 | 49 | let spinnerBarItem = UIBarButtonItem(customView: spinner) 50 | navigationItem.rightBarButtonItem = spinnerBarItem 51 | 52 | resolvingMetaTags = false 53 | webView.loadRequest(NSURLRequest(URL: pageUrl)) 54 | } 55 | 56 | func webViewDidFinishLoad(webView: UIWebView) { 57 | if (timer != nil) { 58 | timer.invalidate() 59 | } 60 | timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: Selector("resolveMetaTags"), userInfo: nil, repeats: false) 61 | } 62 | 63 | func resolveMetaTags () { 64 | 65 | if !resolvingMetaTags { 66 | resolvingMetaTags = true 67 | 68 | webView.resolveMetaTags({ (appInfo: TLNativeAppInfo?) -> () in 69 | if appInfo != nil { 70 | self.appInfo = appInfo! 71 | } 72 | 73 | let actionBarItem = UIBarButtonItem(barButtonSystemItem: .Action, target: self, action: Selector("didSelecteAction")) 74 | actionBarItem.enabled = appInfo != nil 75 | self.navigationItem.rightBarButtonItem = actionBarItem 76 | }) 77 | } 78 | } 79 | 80 | func didSelecteAction () { 81 | 82 | let nativeActivity = NativeAppActivity(nativeAppInfo: self.appInfo) { (didFinish) -> () in 83 | 84 | #if arch(i386) || arch(x86_64) 85 | 86 | let alertController = UIAlertController(title: "Open \(self.appInfo.name)", message: "Url: \(self.appInfo.url)", preferredStyle: .Alert) 87 | 88 | alertController.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (_) -> Void in 89 | didFinish() 90 | })) 91 | 92 | self.presentViewController(alertController, animated: true, completion: nil) 93 | 94 | #else 95 | 96 | if (UIApplication.sharedApplication().canOpenURL(self.appInfo.url)) { 97 | UIApplication.sharedApplication().openURL(self.appInfo.url) 98 | didFinish() 99 | } else { 100 | 101 | let alertController = UIAlertController(title: "Oops!", message: "You don't have this app installed. Do you want to install it now?", preferredStyle: .Alert) 102 | 103 | alertController.addAction(UIAlertAction(title: "Yes", style: .Cancel, handler: { (action) -> Void in 104 | let itunesUrl = NSURL(string: "http://itunes.apple.com/app/id\(self.appInfo.appId)")!; 105 | UIApplication.sharedApplication().openURL(itunesUrl) 106 | didFinish() 107 | })) 108 | 109 | alertController.addAction(UIAlertAction(title: "No", style: .Default, handler: { (_) -> Void in 110 | didFinish() 111 | })) 112 | 113 | self.presentViewController(alertController, animated: true, completion: nil) 114 | } 115 | #endif 116 | } 117 | 118 | let activityController = UIActivityViewController(activityItems: [self.pageUrl], applicationActivities: [nativeActivity]) 119 | 120 | if UIDevice.currentDevice().userInterfaceIdiom == .Phone { 121 | self.presentViewController(activityController, animated: true, completion: nil) 122 | } else { 123 | let popover = UIPopoverController(contentViewController: activityController) 124 | popover.presentPopoverFromBarButtonItem(navigationItem.rightBarButtonItem!, permittedArrowDirections: .Any, animated: true) 125 | } 126 | 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tryolabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pod/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLMetaResolver/85c653f1717710426eceaf237ba6944c450b5fc2/Pod/Assets/.gitkeep -------------------------------------------------------------------------------- /Pod/Assets/TLMetaParser.js: -------------------------------------------------------------------------------- 1 | 2 | //Returns a JSON string with the fields: 3 | //- url. The url to open the native app 4 | //- appId. The Apple id of the native app 5 | // 6 | //The information in the JSON corresponds to the first provider found. This means that if the page provides information for Twitter, Facebook and Apple Smart Banner. 7 | //The first combination of 'url' and 'app id' found is the one returned. The format of the information vary from one provider to another. 8 | //Twitter use several meta tags indentified by the 'name' attribute and with the 'content' attribute set to the corresponding value. 9 | //Facebook (AppLink) use a similar approach but use the 'property' attribute instead of the 'name'. 10 | //Apple Smart Banner use the 'name' attribute to indentify the meta tag and put all the information on the 'content' attribute instead of having multiple tags. 11 | 12 | function parseMetaTags(isIPad) { 13 | 14 | var metaTags = document.getElementsByTagName('meta'); 15 | var device = isIPad ? 'ipad' : 'iphone'; 16 | 17 | var metaInfo = {}; 18 | 19 | //iterate over all the 'meta' tags in the page 20 | for (var i = 0; i < metaTags.length; i++) { 21 | 22 | var url = null; 23 | var appId = null; 24 | var property = metaTags[i].getAttribute('property'); 25 | 26 | //check if it is a Facebook meta tag (AppLink) reading the 'property' attribute 27 | if (property && property.substring(0, 'al:'.length) === 'al:') { 28 | 29 | if (!url && (property === 'al:ios:url' || property === 'al:' + device + ':url')) 30 | url = metaTags[i].getAttribute('content'); 31 | if (!appId && (property === 'al:ios:app_store_id' || property === 'al:' + device + ':app_store_id')) 32 | appId = metaTags[i].getAttribute('content'); 33 | 34 | } else { 35 | 36 | var meta_name = metaTags[i].getAttribute('name'); 37 | 38 | //check if it is a Apple Smart Banner meta tag 39 | if (meta_name && meta_name === 'apple-itunes-app') { 40 | 41 | var content_string = metaTags[i].getAttribute('content'); 42 | if (content_string) { 43 | 44 | //this tag has all the values encoded in the 'content' attribute so the parsing for this values is in other function. 45 | var appleMetaInfo = parseAppleMetaTag(content_string); 46 | if (appleMetaInfo.url && appleMetaInfo.appId) 47 | metaInfo = appleMetaInfo; 48 | } 49 | 50 | //Check if it is a Twitter meta tag. 51 | } else if (!url && meta_name === 'twitter:app:url:' + device) { 52 | url = metaTags[i].getAttribute('content'); 53 | } else if (!url && meta_name === 'twitter:app:id:' + device) { 54 | appId = metaTags[i].getAttribute('content'); 55 | } 56 | } 57 | 58 | if (url && !metaInfo.url) 59 | metaInfo.url = url; 60 | if (appId && !metaInfo.appId) 61 | metaInfo.appId = appId; 62 | 63 | //Once we get a complete value, break the loop and return it 64 | if (metaInfo.url && metaInfo.appId) 65 | break; 66 | } 67 | 68 | if (Object.keys(metaInfo).length != 0) 69 | return JSON.stringify(metaInfo); 70 | else 71 | return null 72 | } 73 | 74 | function parseAppleMetaTag(metaTagContent) { 75 | 76 | //Remove spaces and split by ',' 77 | var tokens = metaTagContent.replace(/\s+/g,'').split(','); 78 | var metaInfo = {}; 79 | 80 | for (var i = 0; i < tokens.length; i++) { 81 | 82 | var token = tokens[i]; 83 | if (token.indexOf('app-id') == 0) { 84 | metaInfo.appId = token.substring(token.indexOf('=') + 1); 85 | } else { 86 | if (token.indexOf('app-argument') == 0) 87 | metaInfo.url = token.substring(token.indexOf('=') + 1); 88 | } 89 | } 90 | 91 | return metaInfo; 92 | } -------------------------------------------------------------------------------- /Pod/Assets/iconMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLMetaResolver/85c653f1717710426eceaf237ba6944c450b5fc2/Pod/Assets/iconMask.png -------------------------------------------------------------------------------- /Pod/Assets/iconMask@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLMetaResolver/85c653f1717710426eceaf237ba6944c450b5fc2/Pod/Assets/iconMask@2x.png -------------------------------------------------------------------------------- /Pod/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/TLMetaResolver/85c653f1717710426eceaf237ba6944c450b5fc2/Pod/Classes/.gitkeep -------------------------------------------------------------------------------- /Pod/Classes/TLBundleExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLBundleExtension.swift 3 | // TLMetaResolver 4 | // 5 | // Created by Bruno Berisso on 3/19/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension NSBundle { 13 | 14 | //This class is used only to get the bundle for the framework, in the case that we are running in one. 15 | private class InnerClass {} 16 | 17 | class func metaResolverBundle () -> NSBundle? { 18 | 19 | if let useAppBundle = NSProcessInfo.processInfo().environment["use_app_bundle"] where useAppBundle == "YES" { 20 | return NSBundle.mainBundle() 21 | } else { 22 | if let bundlePath = NSBundle(forClass: InnerClass.self).pathForResource("TLMetaResolver", ofType: "bundle") { 23 | return NSBundle(path: bundlePath) 24 | } else { 25 | return nil 26 | } 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Pod/Classes/TLMetaResolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLMetaResolver.swift 3 | // TLMetaResolver 4 | // 5 | // Created by Bruno Berisso on 2/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /** 13 | This class is the result of the parsing of the meta tags. It contains the basic info to handle the content on the loaded page: 14 | 15 | - name The name of the app as is in iTunes 16 | 17 | - url The url to open the app on the devices 18 | 19 | - icon The small version of the icon available in iTunes 20 | 21 | This object can be used to create a TLNativeAppActivity that handle the presentation and basic interaction to fire the native app. 22 | */ 23 | public class TLNativeAppInfo: NSObject { 24 | 25 | public private(set) var name: String 26 | public private(set) var appId: String 27 | public private(set) var url: NSURL 28 | public private(set) var icon: UIImage 29 | 30 | private init(name: String, appId: String, url: NSURL, icon: UIImage) { 31 | self.name = name 32 | self.appId = appId 33 | self.url = url 34 | self.icon = icon 35 | } 36 | } 37 | 38 | 39 | /** 40 | The closure called when the resolve process complete 41 | 42 | :param: activity An activity instance of TLNativeAppActivity representing the native app declared in the page as able to handle this content 43 | */ 44 | public typealias TLMetaResolverComplete = (TLNativeAppInfo?) -> () 45 | 46 | /** 47 | The closure called when a fetch operation fail 48 | 49 | :param: error The error object that cause the fetch to fail 50 | */ 51 | public typealias TLMetaResolverFetchError = (NSError?) -> () 52 | 53 | /** 54 | The closure called when the fetch succeed 55 | 56 | :param: responseData The data resulting of a fetch operation 57 | */ 58 | public typealias TLMetaResolverFetchSuccess = (NSData?) -> () 59 | 60 | /** 61 | A closure representing a fetch operation. 62 | 63 | :param: url The url to fetch 64 | :param: successHandler A closure called when the fetch succeed 65 | :param: errorHandler A closure called when the fetch fail 66 | */ 67 | public typealias TLMetaResolverFetchURL = (NSURL, TLMetaResolverFetchSuccess, TLMetaResolverFetchError) -> () 68 | 69 | 70 | //Direct access to the meta tag info JSON returned from the parser and iTunes lookup 71 | private extension NSDictionary { 72 | 73 | var url: NSURL { 74 | let key = "url" 75 | return NSURL(string:self[key] as! String)! 76 | } 77 | 78 | var appId: String { 79 | let key = "appId" 80 | return self[key] as! String 81 | } 82 | 83 | var appName: String { 84 | let key = "trackName" 85 | return self[key] as! String 86 | } 87 | 88 | var iconUrl: NSURL { 89 | let key = "artworkUrl60" 90 | return NSURL(string: self[key] as! String)! 91 | } 92 | 93 | var firstResult: NSDictionary? { 94 | let key = "results" 95 | let resultsList = self[key] as! Array 96 | return resultsList.count > 0 ? resultsList[0] as? NSDictionary : nil 97 | } 98 | } 99 | 100 | 101 | /** 102 | This extension add two functions to the UIWebView class that start the process of parsing the information on the meta tags of the loaded html document and return an instance of TLNativeAppActivity or nil. 103 | 104 | The tags are parsed by a JavaScript script loaded from a file and evaluated in the context of the document. This script return an iTunes app id that is used to get the app name and icon from iTunes and an url to open the native app. 105 | 106 | The functions are really one function and one homonymous with less parameters that make the use simpler. 107 | 108 | On every case all the possible error cases are handled. A message is loged to the system with NSLog() and the callback is called with nil. 109 | 110 | Note: this is the documentation for the iTunes Search API https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html 111 | */ 112 | extension UIWebView { 113 | 114 | /** 115 | Try to resolve the meta tags that follow the "Twitter - App Card", "Facebook - AppLink" or "Apple - Smart Banner" convention on the loaded html document to a TLNativeAppActivity that represent an installed native app capable of handle the current content. To get the native app information (name and icon) a request to iTunes Search API is made using the shared NSURLSession instance. 116 | 117 | :param: onComplete A complete handler of type TLMetaResolverComplete that receive as argument an objet of type TLNativeAppActivity? 118 | */ 119 | @objc public func resolveMetaTags (onComplete: TLMetaResolverComplete) { 120 | resolveMetaTags(nil, nil, onComplete) 121 | } 122 | 123 | /** 124 | Try to resolve the meta tags that follow the "Twitter - App Card", "Facebook - AppLink" or "Apple - Smart Banner" convention on the loaded html document to a TLNativeAppActivity that represent an installed native app capable of handle the current content. 125 | 126 | :param: fetchUrl An optional closuere that will be called to fetch the native app information from iTunes Search API, if nil is passed the shared NSURLSeesion is used. 127 | :param: fetchImage An optional closure that will be called to fetch the native app icon from iTunes, if nil is passed the shared NSURLSeesion is used. 128 | :param: onComplete A complete handler of type TLMetaResolverComplete that receive as argument an objet of type TLNativeAppActivity? 129 | */ 130 | @objc public func resolveMetaTags (fetchUrl: TLMetaResolverFetchURL?, _ fetchImage: TLMetaResolverFetchURL?, _ onComplete: TLMetaResolverComplete) { 131 | 132 | //Get the js parser 133 | if let parserJs = metaTagsParserJS() { 134 | 135 | //run it 136 | if let metaInfoString = self.stringByEvaluatingJavaScriptFromString(parserJs) { 137 | 138 | if metaInfoString == "" { 139 | //No meta tags to parse so, silently, return nil 140 | onComplete(nil) 141 | } else { 142 | 143 | //transform the string result to raw data 144 | if let metaInfoData = metaInfoString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) { 145 | 146 | //get a JSON out of that data 147 | do { 148 | let metaInfoJSON = try NSJSONSerialization.JSONObjectWithData(metaInfoData, options: NSJSONReadingOptions.AllowFragments) 149 | 150 | //Try to fit it in a dictionary 151 | if let metaInfo = metaInfoJSON as? NSDictionary { 152 | var fetchUrlImp: TLMetaResolverFetchURL 153 | var fetchImageImp: TLMetaResolverFetchURL 154 | 155 | //Check if a custom implementation of the 'fetch' closures was provide, if not set the default one 156 | if (fetchUrl == nil) { 157 | fetchUrlImp = defatulFetchUrl() 158 | } else { 159 | fetchUrlImp = fetchUrl! 160 | } 161 | 162 | if (fetchImage == nil) { 163 | fetchImageImp = defaultFetchImage() 164 | } else { 165 | fetchImageImp = fetchImage! 166 | } 167 | 168 | //Try to create the activity 169 | createActivityWithInfo(metaInfo, fetchUrlImp, fetchImageImp, onComplete) 170 | 171 | } else { 172 | NSLog("Malformed JSON, can't read it as a Dictionary") 173 | onComplete(nil) 174 | } 175 | } catch let error as NSError { 176 | NSLog("Can't parse meta info json: %@", error.localizedDescription) 177 | onComplete(nil) 178 | } 179 | 180 | } else { 181 | NSLog("Can't parse meta info string") 182 | onComplete(nil) 183 | } 184 | } 185 | 186 | } else { 187 | NSLog("Parser script crash for some reason") 188 | onComplete(nil) 189 | } 190 | } else { 191 | //Don't log anything because it is handled on the 'metaTagsParserJS' function 192 | onComplete(nil) 193 | } 194 | } 195 | 196 | /** 197 | Return the JavaScript script to be evaluated on the context of the loaded document to parse the meta tags. On success the script return a JSON dictionary with two keys: 198 | 199 | - appId: The Apple app id with the native app is registered in iTunes 200 | - url: The url to open the native app 201 | 202 | :returns: A optional string containing the script or nil if somthing go wrong 203 | */ 204 | private func metaTagsParserJS () -> String? { 205 | 206 | if let parserPath = NSBundle.metaResolverBundle()?.pathForResource("TLMetaParser", ofType: "js") { 207 | do { 208 | let parserJs = try String(contentsOfFile: parserPath, encoding: NSUTF8StringEncoding) 209 | //The script 'main' function receives as argument wether we are running on an iPad or iPhone to choose the correct meta tags 210 | let isIPad = UIDevice.currentDevice().userInterfaceIdiom == .Phone ? "false" : "true" 211 | return parserJs + ";parseMetaTags(\(isIPad))" 212 | } catch let error as NSError { 213 | NSLog("Can't get parser content: %@", error.localizedDescription) 214 | return nil 215 | } 216 | } else { 217 | NSLog("Can't find the JavaScript file") 218 | return nil 219 | } 220 | } 221 | 222 | /** 223 | Return the default implementation for fething any URL. 224 | */ 225 | private func defatulFetchUrl () -> (TLMetaResolverFetchURL) { 226 | return { 227 | (url: NSURL, successHandler: TLMetaResolverFetchSuccess, errorHandler: TLMetaResolverFetchError) -> () in 228 | 229 | NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { 230 | (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in 231 | 232 | if (error == nil) { 233 | successHandler(data) 234 | } else { 235 | errorHandler(error) 236 | } 237 | }).resume() 238 | 239 | } 240 | } 241 | 242 | /** 243 | Return the default implementation for fetching any image. If the resulting data can't be parsed to a valid UIImage create a NSError object and pass it to the errorHandler closure 244 | */ 245 | private func defaultFetchImage () -> (TLMetaResolverFetchURL) { 246 | return { 247 | (url: NSURL, successHandler: TLMetaResolverFetchSuccess, errorHandler: TLMetaResolverFetchError) -> () in 248 | 249 | NSURLSession.sharedSession().downloadTaskWithURL(url, completionHandler: { 250 | (location: NSURL?, response: NSURLResponse?, error: NSError?) -> Void in 251 | 252 | guard error == nil else { 253 | errorHandler(error!) 254 | return 255 | } 256 | 257 | guard let location = location else { 258 | let error = NSError(domain: "TLMetaResolver", code: 1, userInfo: [NSLocalizedDescriptionKey: "No temp file to read from."]) 259 | errorHandler(error) 260 | return 261 | } 262 | 263 | guard let data = NSData(contentsOfURL: location) else { 264 | let error = NSError(domain: "TLMetaResolver", code: 1, userInfo: [NSLocalizedDescriptionKey: "Can't read temp file at url: \(location)"]) 265 | errorHandler(error) 266 | return 267 | } 268 | 269 | successHandler(data) 270 | 271 | }).resume() 272 | } 273 | } 274 | 275 | /** 276 | Create the TLNativeAppActivity with the information extracted form the meta tags. This function perform the calls to iTunes Search API and download the image to be used. On success the 'onComplete' closure is called with the created TLNativeAppActivity or nil if it fail. 277 | */ 278 | private func createActivityWithInfo(appInfo: NSDictionary, _ fetchUrl: TLMetaResolverFetchURL, _ fetchImage: TLMetaResolverFetchURL, _ onComplete: TLMetaResolverComplete) { 279 | 280 | let itunesUrl = NSURL(string: "https://itunes.apple.com/lookup?id=\(appInfo.appId)")! 281 | 282 | fetchUrl(itunesUrl, { 283 | //Fetch success handler 284 | (data: NSData?) -> () in 285 | 286 | guard let data = data else { 287 | NSLog("No data provided.") 288 | return 289 | } 290 | 291 | //get a JSON out of that data 292 | do { 293 | let itunesInfoJSON: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) 294 | 295 | if let itunesResults = itunesInfoJSON as? NSDictionary { 296 | //The response from iTunes is a list of matching records, because we search for app id is safe to pick the first one in the list if ther are any results. 297 | if let itunesAppInfo = itunesResults.firstResult { 298 | fetchImage(itunesAppInfo.iconUrl, { 299 | //Fetch image success handler 300 | (data: NSData?) -> () in 301 | 302 | if let data = data, let image = UIImage(data: data) { 303 | let nativeAppInfo = TLNativeAppInfo(name: itunesAppInfo.appName, appId: appInfo.appId, url: appInfo.url, icon: image) 304 | self.performOnMain { onComplete(nativeAppInfo) } 305 | } else { 306 | NSLog("Can't parse image data or it's not a valid image") 307 | self.performOnMain { onComplete(nil) } 308 | } 309 | 310 | }, { 311 | //Fetch image error hanlder 312 | (error: NSError?) -> () in 313 | 314 | NSLog("Can't fetch image" + (error.map { ": " + $0.description } ?? "") + ".") 315 | self.performOnMain { onComplete(nil) } 316 | }) 317 | } else { 318 | NSLog("Can't find the provided app id on iTunes: %@", appInfo.appId) 319 | self.performOnMain { onComplete(nil) } 320 | } 321 | 322 | } else { 323 | NSLog("Bad response form iTunes, object is not a dictionary") 324 | self.performOnMain { onComplete(nil) } 325 | } 326 | } catch let error as NSError { 327 | NSLog("Can't parse iTunes response: %@", error.description) 328 | self.performOnMain { onComplete(nil) } 329 | } 330 | }, { 331 | //Fetch error handler 332 | (error: NSError?) -> () in 333 | 334 | NSLog("Can't get info from iTunes" + (error.map { ": " + $0.description } ?? "") + ".") 335 | self.performOnMain { onComplete(nil) } 336 | }) 337 | } 338 | 339 | func performOnMain (closure: () -> ()) { 340 | dispatch_async(dispatch_get_main_queue(), closure) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Pod/Classes/TLNativeAppActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLNativeAppActivity.swift 3 | // TLMetaResolver 4 | // 5 | // Created by Bruno Berisso on 2/13/15. 6 | // Copyright (c) 2015 Bruno Berisso. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /** 13 | This is a subclass of UIActivity that open a native app using the supported custom scheme. The icon is showed in grayscale with the sice adjusted to the running devices. 14 | */ 15 | public class TLNativeAppActivity: UIActivity { 16 | 17 | var url: NSURL 18 | var name: String 19 | var icon : UIImage? 20 | 21 | /** 22 | Create a new TLNativeAppActivity with the given parameters. 23 | 24 | :param: appUrl The url used to open the native app. When this activity is permormed a call to 'UIApplication.sharedApplication().openURL()' is performed with this value. 25 | :param: appName The name of the app used, mostly, to show the activity title in a UIActivityViewController 26 | :param: appIcon The image to use as the activity icon. This image should be a square image of any size, preferible of 76 points of bigger because it will be scaled to that size on iPad (and to 60 points on iPhone) 27 | 28 | :returns: A new instance of TLNativeAppActivity that can be shown in a UIActivityViewController 29 | */ 30 | @objc public init(nativeAppInfo: TLNativeAppInfo) { 31 | url = nativeAppInfo.url 32 | name = nativeAppInfo.name 33 | 34 | //Scale the image to the correct size for an activity icon, according to the documentation 35 | let scale: CGFloat = UIDevice.currentDevice().userInterfaceIdiom == .Phone ? 60 : 76 36 | let scaledImage = nativeAppInfo.icon.imageByScaleToSize(CGSizeMake(scale, scale)) 37 | 38 | //Transform it to grayscale 39 | let scaledGrayImage = scaledImage.convertToGrayscale() 40 | 41 | //Mask it so it has the correct shape 42 | let iconMask = UIImage(named: "iconMask", inBundle: NSBundle.metaResolverBundle(), compatibleWithTraitCollection: nil)! 43 | icon = scaledGrayImage.imageByApplyingMask(iconMask) 44 | } 45 | 46 | func _activityImage() -> UIImage? { 47 | return icon 48 | } 49 | 50 | override public func activityType() -> String? { 51 | return NSBundle.mainBundle().bundleIdentifier! + "open.\(name)" 52 | } 53 | 54 | override public func activityTitle() -> String? { 55 | return "Open in \(name)" 56 | } 57 | 58 | override public func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool { 59 | return true 60 | } 61 | 62 | override public func performActivity() { 63 | 64 | #if arch(i386) || arch(x86_64) 65 | NSLog("App URL: \(url)") 66 | #else 67 | UIApplication.sharedApplication().openURL(url) 68 | #endif 69 | 70 | activityDidFinish(true) 71 | } 72 | } 73 | 74 | private extension UIImage { 75 | 76 | func imageByApplyingMask (maskImage: UIImage) -> UIImage? { 77 | 78 | let maskRef = maskImage.CGImage 79 | let mask = CGImageMaskCreate( 80 | CGImageGetWidth(maskRef), 81 | CGImageGetHeight(maskRef), 82 | CGImageGetBitsPerComponent(maskRef), 83 | CGImageGetBitsPerPixel(maskRef), 84 | CGImageGetBytesPerRow(maskRef), 85 | CGImageGetDataProvider(maskRef), nil, false) 86 | 87 | return CGImageCreateWithMask(CGImage, mask).map { maskedImageRef in 88 | let scale = UIScreen.mainScreen().scale 89 | return UIImage(CGImage: maskedImageRef, scale: scale, orientation: .Up) 90 | } 91 | } 92 | 93 | func imageByScaleToSize (newSize: CGSize) -> UIImage { 94 | //UIGraphicsBeginImageContext(newSize); 95 | // In next line, pass 0.0 to use the current device's pixel scaling factor (and thus account for Retina resolution). 96 | // Pass 1.0 to force exact pixel size. 97 | UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0); 98 | drawInRect(CGRectMake(0, 0, newSize.width, newSize.height)) 99 | 100 | let newImage = UIGraphicsGetImageFromCurrentImageContext(); 101 | UIGraphicsEndImageContext(); 102 | return newImage; 103 | } 104 | 105 | func convertToGrayscale () -> UIImage { 106 | UIGraphicsBeginImageContextWithOptions(size, false, scale); 107 | let imageRect = CGRectMake(0, 0, size.width, size.height); 108 | 109 | let ctx = UIGraphicsGetCurrentContext(); 110 | 111 | // Draw a white background 112 | CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0); 113 | CGContextFillRect(ctx, imageRect); 114 | 115 | // Draw the luminosity on top of the white background to get grayscale 116 | drawInRect(imageRect, blendMode: .Luminosity, alpha: 1.0) 117 | 118 | // Apply the source image's alpha 119 | drawInRect(imageRect, blendMode: .DestinationIn, alpha: 1.0) 120 | 121 | let grayscaleImage = UIGraphicsGetImageFromCurrentImageContext(); 122 | UIGraphicsEndImageContext(); 123 | 124 | return grayscaleImage; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLMetaResolver 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/TLMetaResolver.svg?style=flat)](http://cocoadocs.org/docsets/TLMetaResolver) 4 | [![License](https://img.shields.io/cocoapods/l/TLMetaResolver.svg?style=flat)](http://cocoadocs.org/docsets/TLMetaResolver) 5 | [![Platform](https://img.shields.io/cocoapods/p/TLMetaResolver.svg?style=flat)](http://cocoadocs.org/docsets/TLMetaResolver) 6 | [![Laguage](https://img.shields.io/badge/language-Swift-orange.svg)](https://developer.apple.com/swift/) 7 | 8 | TLMetaResolver is an extension to UIWebView writen in Swift that adds the ability to parse the meta tags in the loaded web page and extract information about a native app that can be deep linked from that page. This method is used for Twitter and Facebook to deep link to a native app from a posted web page. The meta tags definitions handled for TLMetaResolver are: 9 | 10 | - [Twitter App Cards](https://dev.twitter.com/cards/types/app) 11 | - [Facebook App Link](http://applinks.org/documentation/) 12 | - [Apple Smart Banner](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html) 13 | 14 | ## How it works 15 | 16 | TLMetaResolver adds a funtion to UIWebView that evaluate a JavaScript script in the context of the loaded web page. This script returns an _app id_ and _url_. The _app id_ is the id of the native app on iTunes, the _url_ is a special url used to fire the native app. 17 | 18 | With the _app id_ the extension perform a search on iTunes calling the [iTunes Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) to get the app name and icon url. Then the icon image is downloaded and a TLNativeAppInfo object is created and returned in a callback. This object can be used to create a TLNativeAppActivity to be presented in a UIActivityViewController. When the activity is performed a call to ``UIApplication.sharedApplication().openURL()`` is made with the url pointing to the native app. 19 | 20 | ## Usage 21 | 22 | You need to call one of the ``resolveMetaTags()`` functions once your page is loaded and provide a closure conforming the ``TLMetaResolverComplete`` type. Check the example project for a possible implementation, you can ``pod try TLMetaResolver``. 23 | 24 | One key point to remember is that ``webViewDidFinishLoad`` function of UIWebViewDelegate can be called many times so you should handle that case to avoid unnecesary calls to ``resolveMetaTags()``. TLMetaResolver don't perform any special consideration at that level. 25 | 26 | There are two version of ``resolveMetaTags()`` that have slightly different parameters: 27 | 28 | ```swift 29 | func resolveMetaTags (onComplete: TLMetaResolverComplete) 30 | ``` 31 | 32 | Both versions has a parameter of type ``TLMetaResolvercomplete`` that is a callback that is fired when the process finish. 33 | 34 | ```swift 35 | func resolveMetaTags (fetchUrl: TLMetaResolverFetchURL?, _ fetchImage: TLMetaResolverFetchURL?, _ onComplete: TLMetaResolverComplete) 36 | ``` 37 | 38 | The long version has two extra parameters that are closures used to issue the requests to iTunes Search API (``fetchUrl``) and the app icon download (``fetchImage``). You can provide the implementation for one of this, both or none (that is the case of the short version of ``resolverMetaTags()``). For the closures you don't provide a default implementation is provided using ``NSURLSession.sharedSession()``. 39 | 40 | ## Installation 41 | 42 | TLMetaResolver is available through [CocoaPods](http://cocoapods.org). To install 43 | it, simply add the following line to your Podfile: 44 | 45 | pod "TLMetaResolver" 46 | 47 | You can also do a quick check with: 48 | 49 | pod try TLMetaResolver 50 | 51 | Or opt to clone the repo and integrate the code and assets under the Pod/ directory to your project as you like. 52 | 53 | ## Requirements 54 | 55 | iOS >= 8.0 56 | 57 | ## Author 58 | 59 | BrunoBerisso, bruno@tryolabs.com 60 | 61 | ## License 62 | 63 | TLMetaResolver is available under the MIT license. See the LICENSE file for more info. 64 | 65 | -------------------------------------------------------------------------------- /TLMetaResolver.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint TLMetaResolver.podspec' to ensure this is a 3 | # valid spec and remove all comments before submitting the spec. 4 | # 5 | # Any lines starting with a # are optional, but encouraged 6 | # 7 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 8 | # 9 | 10 | Pod::Spec.new do |s| 11 | s.name = "TLMetaResolver" 12 | s.version = "0.1.4" 13 | s.summary = "TLMetaResolver is an extension to UIWebView that adds the ability to parse the meta tags in the loaded web page." 14 | s.homepage = "https://github.com/tryolabs/TLMetaResolver" 15 | s.license = 'MIT' 16 | s.author = { "BrunoBerisso" => "bruno@tryolabs.com" } 17 | s.source = { :git => "https://github.com/tryolabs/TLMetaResolver.git", :tag => s.version.to_s } 18 | 19 | s.platform = :ios, '8.0' 20 | s.requires_arc = true 21 | s.ios.deployment_target = '8.0' 22 | 23 | s.source_files = 'Pod/Classes/**/*' 24 | s.resource_bundles = { 25 | 'TLMetaResolver' => ['Pod/Assets/**/*'] 26 | } 27 | end 28 | --------------------------------------------------------------------------------